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自体が落ちたりエラーをはかずによく分からないところでアプリが落ちたりするので、もうちょっと安定してからがっつり取り組んだほうが効率がよいかもしれません。

参考

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

UIScrollView(UITableView, UICollectionView)の上にUIGestureRecognizerをおくと、元のUIScrollViewのgestureが効かなくなってしまう問題

  1. scrollViewを置いてあるUIViewControllerにUIGestureRecognizerDeleageを追加
  2. gestureRecognizerのdelegateにselfを代入
  3. 以下メソッドを実装
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

「Appleが未だ実現できていない機能を先に作れ」対決形式で行われたiOS勉強に行ってきました #yxcm

2014.2.25 ヤフーで開催されたiOSの勉強会『ヤフー vs クラスメソッド「iOS 炎の7番勝負」』を聞きにいってきました。その感想ポエム日記です。

http://connpass.com/event/5159/

どの発表も面白くて為になりました。行ってよかったです。まだ内定者とは思えないようなしゃべりが達者で知識が豊富な内定者達や、この日のために作ったクラスメソッドさんの投票システムなど見所がたくさんありましたが、個人的に特に印象に残ったのが、最後のヤフーの大将issayさんの発表でした。issayさんは開発イベントで最優秀賞などすごい賞をいつも受賞しておられるアイデアマンで、その発想がどこからいつも生まれてくるのかとても知りたいと思っていました。今回の発表ではその秘訣の一部を教えてもらえたような気がします。

開発者ならば誰しも今までにないアプリを作りたいと思うものですが、その一つの道としてPaul Grahamの「普通のやつらの上を行け」という言葉を引き合いにだして、

Appleが未だ実現できていない機能を先に作れ

とおっしゃっていました。Appleが実装したAPIをそのまま使ったアイデアはもう誰かが考えている!なので、Appleが提供していない機能を考え、それを独自に開発し、それを使ったアプリが世界初のアプリを作る道であると。これには目から鱗が落ちました。今までの自分は既にある機能を使うことばかり考え、こういう発想をしたことがなかったです。今後、アプリなどを考えて行く際にぜひ使っていきたいと思います。

issayさんは実際にこの考え方を実践したデモを披露してくれました。そこではAppleCore Imageが提供していない視線のトラッキングや、頭のy軸・z軸方向の回転の検出を独自開発し、それを使って顔の表情・視線でアプリを操作するというもの。これだけでも非常に面白いデモでした。また、会場からでた「表情認識以外に注目している技術は何か?」との質問には「カメラは汎用的なセンサで色々な事ができる可能性があるので注目している」とのお答えでした。なるほど。一流のアイデアマンの発想法が少しでも知れた気がしてとても為になったLTでした。

issayさんのスライドとデモ動画