synblog

the round pegs in the square holes

SwiftでFlickrの人気写真を見るアプリを100行で作ったよ

Swiftのビッグウェーブに乗って自分も何かアプリを一つ作ってみようと思い、FlickrAPIを叩いて人気写真一覧を表示するアプリを作ってみました。 この記事は @himara2 さんの記事「SwiftでTiqav APIを叩くビューワアプリを100行でつくったよ」に触発されて書いたものです。TableViewのサンプルは見かけたので、このアプリではCollectionViewで実装したのと、通信をする際に今までのObjective-Cで多くの人が使っているAFNetworkingを使ってみたのでそこらへんが参考になれば幸いです。ソースコードgithubにあげたので、おかしなところがあればこちらにpull requestください。

アプリの仕様

  1. アプリを起動するとFlickr人気写真一覧を取得するAPIにリクエスト
  2. グリッドビューで取得した写真を表示
  3. 上のナビゲーションバーのセグメントをタップするとレイアウトを切り替え
  4. 写真をタップすると写真詳細Viewへ画面遷移

Movie

Screenshot

実装の要点

  • Flickr APIへGETリクエスト
    • AFHTTPRequestOperationManagerのGET()メソッドを使う
  • 写真をグリッド表示
    • UICollectionViewを使う
  • 画像の非同期読み込み処理
    • AFNetworkingのUIImageView拡張(UIImageView+AFNetworking)を使う

ソースコード

Githubにあげました。Xcode6でビルドできます。落としたあと、pod installしてから開いてください。

実装は主にこのファイル( ViewController.swift )にあります。

// ViewController.swift

import UIKit

// レイアウトタイプをenumで定義
enum LayoutType: Int {
    case Grid = 0
    case List = 1
}

class ViewController: UICollectionViewController {
    
    var photos:Dictionary[] = []
    var layoutType = LayoutType.Grid
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        getFlickrPhotos()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }
    
    // Flickr APIにリクエスト
    func getFlickrPhotos() {
        let manager :AFHTTPRequestOperationManager = AFHTTPRequestOperationManager()
        let url :String = "https://api.flickr.com/services/rest/"
        let parameters :Dictionary = [
            "method"         : "flickr.interestingness.getList",
            "api_key"        : "86997f23273f5a518b027e2c8c019b0f",
            "per_page"       : "300",
            "format"         : "json",
            "nojsoncallback" : "1",
            "extras"         : "url_q,url_z",
        ]
        SVProgressHUD.show()
        manager.GET(url, parameters: parameters, success: requestSuccess, failure: requestFailure)
    }

    func requestSuccess (operation :AFHTTPRequestOperation!, responseObject :AnyObject!) -> Void {
        SVProgressHUD.dismiss()
        self.photos = responseObject.objectForKey("photos").objectForKey("photo") as Array
        self.collectionView.reloadData()
        NSLog("requestSuccess \(responseObject)")
    }
    
    func requestFailure (operation :AFHTTPRequestOperation!, error :NSError!) -> Void {
        SVProgressHUD.dismiss()
        NSLog("requestFailure: \(error)")
    }
    
    // CollectionView関連のメソッド
    override func collectionView(collectionView: UICollectionView!, numberOfItemsInSection section: Int) -> Int {
        return self.photos.count;
    }
    
    override func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
        var photoCell: PhotoCell = self.collectionView.dequeueReusableCellWithReuseIdentifier("PhotoCell", forIndexPath: indexPath) as PhotoCell
        var photoInfo = photos[indexPath.item] as Dictionary
        var photoUrl = (self.layoutType == LayoutType.Grid) ? photoInfo["url_q"] : photoInfo["url_z"]
        // UIImageView+AFNetworkingの画像を非同期で読んでくれるメソッドを実行
        photoCell.photoImageView.setImageWithURL(NSURL.URLWithString(photoUrl))
        photoCell.photoInfo = photoInfo
        
        return photoCell;
    }
    
    func collectionView(collectionView: UICollectionView!, layout collectionViewLayout: UICollectionViewLayout!, sizeForItemAtIndexPath indexPath: NSIndexPath!) -> CGSize {
        var itemSize :CGSize = (self.layoutType == LayoutType.Grid) ? CGSizeMake(80, 80) : CGSizeMake(320, 150)
        
        return itemSize
    }
    
    // SegmentedControlの値が変わった時に呼ばれるメソッド
    @IBAction func segmentedControlDidChanged(control : UISegmentedControl) {
        switch control.selectedSegmentIndex {
        case 0:
            self.layoutType = LayoutType.Grid
        case 1:
            self.layoutType = LayoutType.List
        default:
            self.layoutType = LayoutType.Grid
        }
        
        self.collectionView.reloadData()
    }
    
    // 画面遷移時にデータを受け渡すための実装
    override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
        if segue.identifier == "ShowPhoto" {
            var photoCell : PhotoCell = sender as PhotoCell
            var photoViewController = segue.destinationViewController as PhotoViewController
            photoViewController.photoInfo = photoCell.photoInfo
        }
    }
}

やってみての気付き

  • CocoaPodsが問題なく使える
  • Objective-Cで書かれた既存のライブラリが問題なく(簡単に)使える
    • 少なくとも今回使ったAFNetworkingやSVProgressHUDは問題なく使えた
    • Objective-Cのライブラリを使う手順は下↓に書きます
  • 探してないけど #pragma mark - に相当するものはなくなったのだろうか。寂しい
  • 今までクラスを作る度に生成されていた .h .m と2つのファイルが .swift の一つのファイルにまとまりスッキリする
  • コード自体もより簡潔になってスッキリする
    • 慣れないところは前のObjective-Cのほうがいいなと思ってしまうけど、Swiftに慣れたらすぐ忘れそう
  • たまにObjective-Cとの違いに戸惑う
    • initメソッドを実装していないと怒られるとか、Dictionaryに型指定が必要とか

Objective-Cで書かれた既存のライブラリを使う方法

このApple公式ドキュメント「Swift and Objective-C in the Same Project」に書かれている。要は「ProjectName-Bridging-Header.h」ファイルを作って、そこに使いたいObjective-Cのヘッダーファイルをインポートすればよい。

  1. Objective-C Fileを新規で一つ作る
  2. すると公式ドキュメントにあるようにダイアログで聞かれるので「YES」を選択する
  3. ProjectName-Bridging-Header.h ファイルが作られる
  4. ここに既存のライブラリのヘッダーファイルをインポートするコードを書いていく → こんな感じ
  5. Swift側で読み込んだObjective-Cのライブラリが使えるようになっている

感想

Swiftを使ってみる前は、今までのUIKitとかの資産はどうなるんだろう、ちゃんと使えるのかな、という疑問があったのですが、UIKitの資産は既に問題なく使えるようになっていました。それだけでなく、既存のOSSなどのObjective-Cソースも上記の方法で簡単に使えることが分かりました。「ProjectName-Bridging-Header.h」を作ってそこでファイルをインポートするだけで、今まで [SVProgressHUD show]; とか書いていたのが SVProgressHUD.show() として使えるようになります。SwiftとObjective-Cの共存もできますし、移行は想像以上にスムーズにできそうだと感じました。発表されたばかりで、大きな問題なくもうiPhoneアプリが作れてしまう状態になっているのに驚きです。それよりも、今作ってもまだアプリのリリースができないことと、現在はXcode自体が落ちたりエラーをはかずによく分からないところでアプリが落ちたりするので、もうちょっと安定してからがっつり取り組んだほうが効率がよいかもしれません。

参考

以下の記事を参考にさせていただきました。ありがとうございます。