Help us understand the problem. What is going on with this article?

WebAPIを利用する際のOptional Bindingやクロージャを駆使したSwiftらしいコードの書き方の解説

More than 3 years have passed since last update.

このドキュメントはSwift1.0以前のベータ版時に書かれたものです。現状のSwift仕様とは大きく乖離している可能性があります

はじめに

久しぶりに訪れた梅雨晴れの朝に油断をして、傘を持たずに外に出てしまった人は多かったのではないでしょうか。梅雨明けはまだまだずっと先で、夏が待ち遠しい。というかずっと春であって欲しいyimajoです(ちなみに上の写真は私ではありません)。

今週もWantedly社で開催された第2回Swift 勉強会 - LT会に参加し、「WebAPIを利用する際のOptional Bindingやクロージャを駆使したSwiftらしいコードの書き方の解説」というタイトルでLTしてきたのでその内容を公開しておきます。

発表資料は「WebAPIを利用する際のOptional Bindingやクロージャを駆使したSwiftらしいコードの書き方」はSlideShareにアップロードしていて、LTで説明したコードはGitHubに置いてあります。

LTで話したかったこと

前回第1回の勉強会や懇談会では下記の事柄について分からないという声がありました

  • OptionalやOptional Binding
  • クロージャ
  • JSONのパース部分

Swiftを学ぶ上で皆が通る箇所ではないかと思いますが、実際WebAPIを利用したコードを書いてみるとココらへんのことがよく分かり、さらによりSwift風にコードを書くにはどうすればいいのか、という話題があれば結構盛り上がるのかなと思いました。

なのでAPIClientを作るという前提で、実際のコーディングをどのようにすればいいのかを説明しました。

4.jpg

この例は認証のいらないQiitaの特定タグの投稿取得APIを利用し、TableViewControllerがAPIClientを通してそのデータを表示するような仕様にしました。Qiitaの投稿取得APIのレスポンスは辞書の配列になっていて、必須のパラメータと必須でないパラメータがあるのがポイントです。

通信用のインターフェースの設計

Qiitaの特定タグの投稿取得APIを呼び出すAPIClientのメソッドは次のようにしました。

class func items(tagName:String,
                 success:((QiitaItemEntity[]) -> Void)!,
                 failure:((NSError?) -> Void)!) 

引数1つ目は特定タグのStringで必須、引数2つ目の引数は通信とJSONのパース成功時のクロージャでQiitaItemEntity[]を返し、引数3つ目は失敗時はNSErrorを引数で返します。クロージャは変数のように?でOptionalにでき、nilを渡すことが出来るようにしています。

ちなみに、クロージャにnilを渡された側はnilチェックを行いながらクロージャを実行したい場合、次のような一行に書けるのもSwiftの良い所ですね。

//failureがnilなら実行しないのでクラッシュしない
failure?(NSError())

通信用のインターフェースの実装

実装は次のようにしました

//クラス変数(定数)は使えないのでプロパティのgetterでAPIのBASE URLを返す
//getterなのにgetが省略できているのは何故かちょっと良くわからないのですがそういうものなんでしょう
class var baseUrlString: String { return "https://qiita.com/api/v1" }

//URLを組み立ててNSURLを返すだけ。APIのエンドポイントとして使う
class func itemsURL(tagName:String) -> NSURL {
    //tagNameは必須なのでここではnilチェックなどをする責任を持たない
    return NSURL(string: "\(self.baseUrlString)/tags/\(tagName)/items")
}

//Qiitaの特定タグの記事一覧を取得するAPIをコールし、
//成功した場合のクロージャか失敗した場合のクロージャで値を取得できるようにする
class func items(tagName:String,
                 success:((QiitaItemEntity[]) -> Void)!,
                 failure:((NSError?) -> Void)!) {

    let url = itemsURL(tagName)
    let request = NSURLRequest(URL: url)

    NSURLConnection.sendAsynchronousRequest(request,
        queue: NSOperationQueue.mainQueue(),
        completionHandler:{(response: NSURLResponse!,
                     data: NSData!,
                    error: NSError!) in
            //入力値のエラーチェックは先に行っておき失敗クロージャに返す
            if error != nil {
                failure?(error)
                return
            }
            //NSJSONSerializationにnil渡すと例外になるためチェックをしている
            //TODO: オリジナルのNSErrorを作ればいいかもしれないがとりあえずnilを返しておく
            if data == nil {
                failure?(nil)
                return
            }

            var jsonError: NSError?
            //キャストしつつOptional BindingするのがSwiftっぽい気がする
            if let anyObjects = NSJSONSerialization.JSONObjectWithData(data,
                    options:NSJSONReadingOptions.AllowFragments,
                    error:&jsonError) as? NSDictionary[] {

                //このブロックではJSONが想定通りの配列と辞書になっていることが確定する

                var items = QiitaItemEntity[]()
                //anyObjectsの全ての要素がNSDictionaryだからfor inできる
                for dictionary : NSDictionary in anyObjects {
                    if let title = dictionary["title"] as? String {
                        if let urlString = dictionary["url"] as? String {

                            let qiita = QiitaItemEntity(title: title, urlString: urlString)
                            qiita.gistUrlString = dictionary["gist_url"] as? String
                            //配列の+=はapendされるっぽい
                            items += qiita
                        }
                    }
                }

                success?(items)

            } else if jsonError != nil {
                //想定外1(APIサーバーが仕様通りのJSONを返していないなど疑う)
                failure?(jsonError)
            } else {
                //想定外2(APIサーバーがメンテナンスモードでJSONを返せていないなどを疑う)
                //TODO: これもオリジナルのNSErrorを作ればいいかもしれないがとりあえずnilを返しておく
                failure?(nil)
            }
        }
    )
}

一番Swiftっぽい部分はJSONをJSONObjectとして返すメソッドを利用する部分ですね

if let anyObjects = NSJSONSerialization.JSONObjectWithData(data,
                             options:NSJSONReadingOptions.AllowFragments,
                               error:&jsonError) as? NSDictionary[] {

    //このブロックに突入できるのはanyObjectsがnilでない場合

} else if jsonError != nil{
    //異常系の処理1
} else {
    //異常系の処理2
}

解説すると、as?によってJSONからパースされたJSONObjectの構造がNSDictionaryの配列であればnilでないものを返し、NSDictionaryの配列でなければ、nilを返すキャストを使っています。

nilでなければ束縛変数anyObjectsに変数を代入しつつ、このブロック(blocksのことではない)に突入できるようになることで、正常系の処理をまずコーディングし、あとから異常系の処理を追加していくスタイルになるため、Swift風にコーディングしていくだけで可読性が高くなるのではないかと感じました。

もともと、NSJSONSerialization.JSONObjectWithData:options:error:メソッドはObjective-Cでは戻り値がidになりますが、Objective-Cで書かれるコードではNSArrayやNSDcitionaryにキャストして利用する事が多いため、SwiftでもついNSArrayにして使ってしまう人が多いでしょうが、NSArrayはNSDictionary[]に自然に(強制せず)キャストすることはできないような気がします。

分かりにくいかもしれないのでもう少し説明をしてみると、NSDictionary[]は実際はArray<NSDictionary>のはずで、AnyObject[]はArray<AnyObject>なのでコンパイラの警告無く普通にキャストすることができます。次のように一時的な変数に渡したコードにすると話しが分かりやすいかもしれません。

//変数を一時的に移しておく(Swiftのメリットが薄くなる)
let anyObjects[] = NSJSONSerialization.JSONObjectWithData(data,
                             options:NSJSONReadingOptions.AllowFragments,
                               error:&jsonError) as? AnyObject[]

if let downCastArray = anyObjects as? NSDictionary[] {
    //このブロックに突入できるのはdownCastArrayがnilでない(NSDictionary[]の)とき
    //anyObjectsが使えるのもこのスコープだけ

} else if jsonError != nil {
    //異常系の処理1
} else {
    //異常系の処理2
}

このコードは分かりやすいですが、let anyObjects[]はもう利用しないため、前のコードのようにキャストしつつ利用する変数をOptional Bindingで制限を加えるととてもSwiftっぽいコードと言えるのではないでしょうか。

まとめ。Swiftっぽいコードを書くために

  • クラス定数はプロパティのgetterで返す
  • 必須じゃないものはクロージャでさえ?を付ける
  • NSArrayはできるだけ使わないようにしてみる
  • 型が想定外になりそうなときはas?でキャストしつつOptional Binding

以上が私の現段階のSwiftへの理解です

その他、勉強会後に話したこと

AppleのFoundation Frameworkにあるメソッドの引数には結構!が付いている件

Appleにより公開されているプレリリースドキュメントを見ると、Foundation FrameworkをSwiftで使えるようにAppleがヘッダーファイルにしていて引数に!をつけているのですが、私の勝手な考察では、これらは何らかのルールでAppleが自動生成しているものであって、仕様上厳密にチェックし安全を確認した上で!をつけているのではない気がしています。

例えば今回の話にでてきたNSJSONSerialization.JSONObjectWithData:options:error:メソッドのdataは宣言に!が付いていて、引数にnilを入れられるのですが、入れるとObjC時代と変わらず例外が起こります。

引数にnilを入れてほしくないのであれば!や?のどちらも付けないで欲しい所ですがそうしていません。全然腑に落ちないですが、これはリリースを優先して機械的に!を付けているためじゃないかと想像できるわけです。

なので、現時点では!の正しい使い方についてFoundation Frameworkにあるメソッドの引数から理解しようとするのは、実はOptional利用のための理解を妨げてしまうのではないかと思います。

最後に

Swift2.0に期待

yimajo
株式会社キュリオシティソフトウェアの代表です。iOSアプリを作っています。最近はRxSwift研究読本書いてます。 https://swift.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした