Think Big Act Local

iPhone開発を軸にブレブレの記事を書いていきます。

SwiftでTiqav APIを叩くビューワアプリを100行でつくったよ

2015/03/30 追記
記載しているコードは古くて動かない可能性があります。
最新の環境で動くコードは以下に置いてあります: https://github.com/himaratsu/SwiftTiqavViewer

--

以下の記事を読んで触発されてつくってみました。

つくったもの

Tiqav viewer

f:id:himaratsu:20140604190931p:plain

Tiqavとは

「レス画像検索No.1/画像会話なら ちくわぶ」

言葉に応じた画像を検索できるサービスです。
登録されている画像を返すAPIが公開されているのでSwiftで使ってみました。

アプリの概要

  • Storyboard 使用
  • NSURLConnection で tiqav api を叩いて結果をパース
  • UITableView (+Custome Cell) に結果を表示

ファイルの構成

  • AppDelegate.swift
    • 自動生成のまま
  • ViewController.swift
    • 以下で紹介する
  • Main.storyboard
    • ViewContoller.swiftにUITableView, UITableViewCellを追加
  • TiqavCell.swift
    • カスタムUITableViewCell. IBOutletをもたせただけ

コードの要点

通信 (NSURLConnection)

NSURLConnectionを使ってWebAPIにアクセスする方法です。

func reload() {
    // Thanks to tiqav api! ( http://dev.tiqav.com/ )
    let URL = NSURL(string: "http://api.tiqav.com/search/random.json")
    let req = NSURLRequest(URL: URL)
    let connection: NSURLConnection = NSURLConnection(request: req, delegate: self, startImmediately: false)
        
    // NSURLConnectionを使ってアクセス
    NSURLConnection.sendAsynchronousRequest(Req,
        queue: NSOperationQueue.mainQueue(),
        completionHandler: self.fetchResponse)
}

// responseを処理する
func fetchResponse(res: NSURLResponse!, data: NSData!, error: NSError!) {
    // responseをjsonに変換
    let json: NSArray = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil) as NSArray

    tiqavs = []

    for img in json {
        let imgId = img["id"] as String
        let ext = img["ext"] as String
            
        let imageUrl = (baseUrl + imgId + "." + ext) as String
        tiqavs.append(imageUrl)
    }
    
    // tableviewの更新
    dispatch_async(dispatch_get_main_queue(), {
        self.tableView.reloadData()
    })
}

NSJSONSerialization.JSONObjectWithDataAnyObject! を返すので、as NSArray などとして型を指定する必要があります。
本来であれば array か dictionary かを判定して型を指定すれば良いはずです。

ここで用いているAPIのレスポンス例は以下のような感じです。

({
    ext = jpg;
    height = 300;
    id = 1b8;
    "source_url" = "http://jan.2chan.net/jun/b/src/1260209767367.jpg";
    width = 400;
})

非同期での画像の読み込み

以下のページの1つ目のやり方をswiftで実装してます。

var q_global: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
var q_main: dispatch_queue_t  = dispatch_get_main_queue();

// imageUrl に画像のURLが入っている
// 非同期でimageUrlの内容を取得
dispatch_async(q_global, {
    var imageURL: NSURL = NSURL.URLWithString(imageUrl)
    var imageData: NSData = NSData(contentsOfURL: imageURL)
    
    var image: UIImage = UIImage(data: imageData)
    
    // 更新はmain threadで
    dispatch_async(q_main, {
        cell.tiqavImageView.image = image;
        cell.layoutSubviews()
    })
})

画像を非同期で取得してメインスレッドでUI更新。
GCDはそのまま素直に使えるようでスンナリ書けました。

delegate の実装宣言

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSURLConnectionDelegate {
    ...
}

こんな感じで書きます。
UITableViewDataSource, UITableViewDelegate の接続は今回はStoryboardでやってます。

コード全文

ViewController.swift

import UIKit

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, NSURLConnectionDelegate {
    
    @IBOutlet var tableView : UITableView!
    
    let baseUrl = "http://img.tiqav.com/"
    var tiqavs = [String]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.title = "tiqav images"
        self.reload()
    }
    
    func reload() {
        // Thanks to tiqav api! ( http://dev.tiqav.com/ )
        let URL = NSURL(string: "http://api.tiqav.com/search/random.json")
        let Req = NSURLRequest(URL: URL)
        let connection: NSURLConnection = NSURLConnection(request: Req, delegate: self, startImmediately: false)
        
        NSURLConnection.sendAsynchronousRequest(Req,
            queue: NSOperationQueue.mainQueue(),
            completionHandler: self.fetchResponse)
    }
    
    func fetchResponse(res: NSURLResponse!, data: NSData!, error: NSError!) {
        let json: NSArray = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil) as NSArray
        
        tiqavs = [String]()
        
        for img in json {
            let imgId = img["id"] as String
            let ext = img["ext"] as String
            
            let imageUrl = (baseUrl + imgId + "." + ext) as String
            tiqavs.append(imageUrl)
        }
        
        dispatch_async(dispatch_get_main_queue(), {
            self.tableView.reloadData()
            })
    }
    
    func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
        return 120
    }
    
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return tiqavs.count
    }
    
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell: TiqavCell = self.tableView.dequeueReusableCellWithIdentifier("Cell") as TiqavCell
        
        var imageUrl = tiqavs[indexPath.row] as String
        
        cell.tiqavUrlLabel.text = imageUrl;
        cell.tiqavImageView.image = nil;
        
        var q_global: dispatch_queue_t = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        var q_main: dispatch_queue_t  = dispatch_get_main_queue();
        
        dispatch_async(q_global, {
            var imageURL: NSURL = NSURL.URLWithString(imageUrl)
            var imageData: NSData = NSData(contentsOfURL: imageURL)
            
            var image: UIImage = UIImage(data: imageData)
            
            dispatch_async(q_main, {
                cell.tiqavImageView.image = image;
                cell.layoutSubviews()
            })
        })
        return cell;
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        var text: String = tiqavs[indexPath.row]
        
        // show alert
        let alert = UIAlertController(title: "taped", message: text, preferredStyle: .Alert)
        alert.addAction(UIAlertAction(title: "close", style: .Default, handler: nil))
        self.presentViewController(alert, animated: true, completion: nil)
    }

    @IBAction func reloadBtnTouched(sender : AnyObject) {
        self.reload()
        self.tableView.scrollRectToVisible(CGRect(x:0 , y: 0, width: 1,height:1), animated: true)
    }
}

TiqavCell.swift

import UIKit

class TiqavCell: UITableViewCell {
    @IBOutlet var tiqavImageView : UIImageView!
    @IBOutlet var tiqavUrlLabel : UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        tiqavUrlLabel.backgroundColor = UIColor(red: 0.95, green: 0.95, blue: 0.95, alpha: 0.8)
    }
}

ソースコード

GitHubにあげました

ここまでのSwift

  • Xcodeの補完がちゃんと効けば気持ちよく書けそう
    • インデキシングに時間がかかってるだけという説を信じてる
  • Objective-Cとあまり変わらないので慣れると書きやすい
  • ! と ? の仕様が分かってない
  • heightForRowAtIndexPath でstaticのcellを作って高さを返すのが何故かできない
  • dispatchとか使っての非同期処理はそのまま書けそう

まとめ

最初の記法だけ覚えればObjective-Cの知識でほぼ書けそうです。
Swift各地で盛り上がっててなんか祭り感あって楽しいです。

関連書籍

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)

GitHub実践入門 ~Pull Requestによる開発の変革 (WEB+DB PRESS plus)