※ 以下の内容は、Xcode6.3.2 (Swift 1.2)
を前提とした話となっております。なお私自身iOS/Swiftでの開発は記事執筆時点で1ヶ月ほどの経験しかないので何かお作法的にまずい部分や、間違っている部分がありましたらコメント等でご指摘いただけると幸いです。
RxSwift/RxCocoa,SwiftyJSONの導入
RxSwift/RxCocoaはともに、CocoaPodsを使うことにより簡単に導入することができると思います。詳しくはRxSwiftのGitHub Repositoryに載っていますのでそちらを参照してください。
また、今回は例としてQiitaのAPIクライアントを実装しますのでJSONを簡単に取り扱えるように、SwiftyJSONも導入します。
シンプルなAPIクライアント実装
さて今回ご紹介するのはRxSwiftのExamplesに載っているようなシンプルなAPIクライアントの一例です。
ここではQiitaの GET /api/v2/tags
を叩くAPIクライアントの実装を提示します。まずはAPIのレスポンスを突っ込むデータ構造を定義します。Qiita APIのドキュメントを参考に以下のように定義できると思います。
class Tag {
let followersCount: Int
let iconUrl: String?
let id: String
let itemsCount: Int
init(id: String, itemsCount: Int, followersCount: Int, iconUrl: String?) {
self.id = id
self.itemsCount = itemsCount
self.followersCount = followersCount
self.iconUrl = iconUrl
}
}
extension Tag: Printable {
var description: String {
return "Tag{id:\(id),itemsCount:\(itemsCount),followersCount:\(followersCount),iconUrl:\(iconUrl)}"
}
}
extension Tag: Equatable {}
func ==(lhs: Tag, rhs: Tag) -> Bool {
return lhs.id == rhs.id
&& lhs.itemsCount == rhs.itemsCount
&& lhs.followersCount == rhs.followersCount
&& lhs.iconUrl == rhs.iconUrl
}
次にAPIクライアントの実装ですが、基本的にはGETリクエストを投げるとJSONのレスポンスが返ってくるのでそれをTagに変換してあげるという形になるかと思います。実際にアプリケーションに組み込むときはコード中で強制的に unwrap
している箇所についてケアをする必要があるかと思いますが、概ね以下のようなコードになると思います。
import RxSwift
import RxCocoa
import SwiftyJSON
class QiitaApiClient {
private let context = Context.sharedInstance
private let apiUrl = "http://qiita.com/api/v2/"
private let convertFailureError = NSError(domain: "failed to convert", code: -1, userInfo: nil)
private let connectionError = { (statusCode: Int) in NSError(domain: "connection error: status code=\(statusCode)", code: -1, userInfo: nil) }
func getTags() -> Observable<[Tag]> {
let request = NSURLRequest(URL: NSURL(string: apiUrl + "tags")!)
return context.urlSession.rx_response(request)
>- mapOrDie {
(data: NSData!, response: NSURLResponse!) -> RxResult<[Tag]> in
if let res = response as? NSHTTPURLResponse {
if res.statusCode != 200 { return failure(self.connectionError(res.statusCode)) }
else { return success(self.json2Tags(JSON(data: data))) }
} else {
return failure(self.convertFailureError)
}
}
>- observeSingleOn(context.mainScheduler)
}
private func json2Tags(json: JSON) -> [Tag] {
return json.array!
.map(json2Tag)
}
private func json2Tag(json: JSON) -> Tag {
return Tag(
id: json["id"].string!,
itemsCount: json["items_count"].number?.integerValue,
followersCount: json["followers_count"].number?.integerValue,
iconUrl: json["iconUrl"].string
)
}
}
ここで Scheduler
や NSURLSession
などを引き渡すために以下のような Context
クラスを利用しています。
import RxSwift
class Context {
static let sharedInstance = Context()
let urlSession = NSURLSession.sharedSession()
let backgroundWorkScheduler: ImmediateScheduler
let mainScheduler: SerialDispatchQueueScheduler
private init() {
let operationQueue = NSOperationQueue()
operationQueue.maxConcurrentOperationCount = 2
operationQueue.qualityOfService = NSQualityOfService.UserInitiated
backgroundWorkScheduler = OperationQueueScheduler(operationQueue: operationQueue)
mainScheduler = MainScheduler.sharedInstance
}
}
さて、この実装では client クラスがリクエストを生成したり、ステータスコードを見て処理をエラーハンドリングしたり、NSData
から Tag
へと変換を行ったりと様々な責務を持ってしまっています。簡単なアプリケーションならばこのままでも十分かもしれませんが、理想的にはひとつのクラスは単一の責務を持つようにしたいところですね。
Request
や Result
などを別クラスに切り出したAPIクライアント実装も色々手元で試してはいるので、その内容については今後また記事を書く時間ができたら投稿させていただきたいと思っています。
RxSwift/RxCocoaを用いるとコールバックを渡すパターンではなくなるため、APIクライアント側にコールバックを受け取る引数を作らずにすみ、すっきりしますね。また、データの変形やフィルタリングも map
や filter
などをはじめとした様々なコンビネーターを利用して自在に行うことができるため良いのではないかと、私自身は考えています。