ドワンゴ Advent calendar 21日目。
最近は業務でもswiftを使うことが多くなり、Objective-Cの複雑怪奇な構文が懐かしく感じる今日このごろです。
大昔にObjective-Cで利用できるDeferredライブラリを作成したのですが、swiftから使うには少し使い勝手が悪かったので書きなおしてみることにしました。
STDeferred
以前作ったライブラリについて簡単に紹介しておくと、STDeferredというライブラリで、いわゆるPromiseパターンをObjective-Cで実装したものです。同様のパターンを実装した優れたライブラリとして、PromiseKitやBolts、SwiftTask等があります。
Blocksを使ったコールバックによる非同期処理を普通に書くと、いわゆるコールバック地獄に陥りやすいです。例えばUIViewAnimationを連続して行うときなどは
[UIView animateWithDuration:0.5 animations:^{
view.center = CGPointMake(0, 100);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
view.center = CGPointMake(100, 100);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
view.center = CGPointMake(100, 200);
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.5 animations:^{
view.center = CGPointMake(200, 200);
} completion:nil];
}];
}];
}];
といった形でどんどんネストが深くなり、可読性が悪くなっていきます。
これに対して、STDeferredを利用すると、
// UIView+STDeferred.h
@interface UIView (STDeferred)
+ (STDeferred *)dfd_animateWithDuration:(NSTimeInterval)interval animations:(void (^)(void))animations;
@end
// UIView+STDeferred.m
@implementation UIView (STDeferred)
+ (STDeferred *)dfd_animateWithDuration:(NSTimeInterval)interval animations:(void (^)(void))animations
{
STDeferred *deferred = [STDeferred deferred];
[UIView animateWithDuration:interval animations:animations completion:^(BOOL finished) {
[deferred resolve:@(finished)];
}];
return deferred;
}
@end
[UIView dfd_animateWithDuration:0.5 animations:^{ view.center = CGPointMake(0, 100); }]
.pipe(^id(id finished) {
return [UIView dfd_animateWithDuration:0.5 animations:^{ view.center = CGPointMake(100, 100); }];
}, nil)
.pipe(^id(id finished) {
return [UIView dfd_animateWithDuration:0.5 animations:^{ view.center = CGPointMake(100, 200); }];
}, nil)
.pipe(^id(id finished) {
return [UIView dfd_animateWithDuration:0.5 animations:^{ view.center = CGPointMake(200, 200); }];
}, nil);
と言った形で処理を実行順にメソッドチェーンで繋げるため、処理の流れが非常に追いやすくなります。
しかし、STDeferredでは戻り値の型にid型しか渡せず、複数値渡したい場合はNSArrayを突っ込んだり、各Block内で引数チェックが必要だったりといくつか不便な面も多くありました。
Swift版STDeferred
というわけで作ったのがSwift版のSTDeferredです。先ほどのコードと同じ処理を書いてみましょう。
import STDeferred
extension UIView {
public class func dfd_animateWithDuration(duration: NSTimeInterval, animations: Void -> Void) -> Deferred<Bool, NSError> {
return Deferred<Bool, NSError> { (resolve, _, _) in
UIView.animateWithDuration(duration, animations: animations) { resolve($0) }
}
}
}
UIView.dfd_animateWithDuration(0.5) { view.center = CGPointMake(0, 100) }
.then { _ in UIView.dfd_animateWithDuration(0.5) { view.center = CGPointMake(100, 100) } }
.then { _ in UIView.dfd_animateWithDuration(0.5) { view.center = CGPointMake(100, 200) } }
.then { _ in UIView.dfd_animateWithDuration(0.5) { view.center = CGPointMake(200, 200) } }
先ほどよりシンプルに書けました。この例だとわかりづらいですが、Swiftがちゃんと型推論してくれるので、各チェーンの戻り値の型が次のチェーンの引数の型として設定されています。
もう少しちゃんと使ってみる
もう少し、型推論があると嬉しいパターンを見てみます。
キーワード検索APIと記事取得APIがあり、最初にキーワード検索を行った後、最初の1件目の記事を取得するという実装をしてみましょう。
まずはAPIを利用するためのAPIService
クラスとエラーのenumを実装します。(あ、Alamofireを使います)
import Alamofire
enum APIError: ErrorType {
case RequestFailure(error: NSError)
case ParseFailure
case Unknown
}
class APIService {
static let sharedInstance = APIService()
private init() {
}
private enum Router: URLRequestConvertible {
static let baseURLString = "http://hogehoge.com/"
case Search(keyword: String)
case Article(id: String)
var URLRequest: NSMutableURLRequest {
let (path, parameters) = { Void -> (String, [String: AnyObject]?) in
switch self {
case .Search(let keyword):
return ("/search", ["keyword": keyword])
case .Article(let id):
return ("/article/\(id)", nil)
}
}()
let URL = NSURL(string: Router.baseURLString)!
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
let encoding = ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
}
次に検索結果と記事本体のモデルを実装します。各モデルはJSONからインスタンスを生成するためのJSONResponseDeserializable
プロトコルを実装しています。
protocol JSONResponseDeserializable {
static func parseJSONObject(json: AnyObject) throws -> Self
}
struct SearchResult: JSONResponseDeserializable {
let articleIds: [String]
static func parseJSONObject(json: AnyObject) throws -> SearchResult {
guard let array = json as? [String] else {
throw APIError.ParseFailure
}
return SearchResult(articleIds: array)
}
}
struct Article: JSONResponseDeserializable {
let title: String
let body: String
static func parseJSONObject(json: AnyObject) throws -> Article {
guard let dict = json as? [String: AnyObject] else {
throw APIError.ParseFailure
}
guard let title = dict["title"] as? String, body = dict["body"] as? String else {
throw APIError.ParseFailure
}
return Article(title: title, body: body)
}
}
次にAlamofireでJSONを取得して、取得したJSONからモデルを生成する一連の処理のdeferredオブジェクトを返すdfd_JSON
メソッドをAPIService
に作成します。
private func dfd_JSON<T: JSONResponseDeserializable>(req: URLRequestConvertible) -> Deferred<T, APIError> {
return Deferred<T, APIError> { resolve, reject, _ in
request(req).responseJSON { (response) in
switch response.result {
case .Success(let value):
do {
resolve(try T.parseJSONObject(value))
} catch let error {
reject(error as! APIError)
}
case .Failure(let error):
reject(.RequestFailure(error: error))
}
}
}
}
そして各APIのインターフェイスとして、search
メソッドとgetArticle
メソッドを作成します。
func search(keyword: String) -> Deferred<SearchResult, APIError> {
return dfd_JSON(Router.Search(keyword: keyword))
}
func getArticle(id: String) -> Deferred<Article, APIError> {
return dfd_JSON(Router.Article(id: id))
}
これで完成です。あとはメソッドチェーンでつなぐことでAPIを連続で呼び出すことができます。
チェーンした次のブロックの引数にも型が反映されているので、キャストや型チェックをすることなく利用することができるようになっています。
APIService.sharedInstance.search("hoge")
.then { APIService.sharedInstance.getArticle($0.articleIds.first!) }
.success {
print("title=\($0.title)\nbody=\($0.body)")
}
.failure {
print("error=\($0!)")
}
おわりに
すでに冒頭で紹介した各種ライブラリやRxSwiftといったより強力なライブラリが多くありますが、ライブラリとしては単一ファイルのシンプルなものなので、小規模なプロジェクトで簡単な非同期処理のメソッドチェーンを扱いたいといった場合にお使いいただけるといいんじゃないかなーと思います。
ソースはgithubにおいてあります。↓