话说 Swift 3.0 已经推出了一段时间了,PromiseKit 也已经升级至 4.1.7(5.0仍然是 alpha 版)和 Swift 3.0。PromiseKit 支持 O-C 和 Swift,但如果你真的需要在你的 O-C 项目中使用 PromiseKit ,仍然不是那么容易。
很简单:pod “PromiseKit”, “~> 4.0” 就可以了。
不幸的是,PromiseKit 对 CocoaPods 的支持并不是那么友好。如果你真的决定用 CocoaPods 去安装 PromiseKit,很可能会出现各种无法理解的链接问题,项目根本就无法编译了。
我们还有另一种选择,就是手动安装 PromiseKit。
下载 PromiseKit 源文件(或者用 git clone 命令)。解压缩,将 PromiseKit-master 拷贝到你的项目文件夹。将 PromiseKit.xcodeproj 拖进 Xcode 的项目导航窗口。
打开项目 target 的 Build Phases,在 Embeded Frameworks,点击 + 按钮,添加 PromiseKit.framework。安装完成。
PromiseKit 只是 Promise 设计模式的一种实现方式。并不能为我们的 app 带来任何”看得到“的好处,而仅仅是为了满足我们对“代码美学”的追求。因此,使用不使用 PromiseKit 完全取决于你自己的喜好。有的程序员不喜欢 Promise,因为他们觉得原有的异步编程中的回调块或委托模型完全就够用了,没有必要再用 Promise 进行重构。但毋庸置疑的一点是,Promise 确实能够让我们的异步编程代码显得更加优雅,可读性和维护性也更高。
所谓 Promise 设计模式,是借用生活中的“承诺”一词来描述异步编程中的回调模型。承诺是一种对未来的期许,它有两个特点,一,它描述的是未来的一种状态或动作,而不是目前的;二,承诺有一定的不确定性,承诺可以兑现(fullfill),也可能被拒绝(rejectd),拒绝的原因是各种各样的,有可能是承诺者不想兑现了,有可能是因为条件无法满足。举例,如果我们编写一个方法从网络获取图片,网络操作一般都是异步的,所以完全适用于承诺模式。所以这个方法就是一个承诺。它不用直接返回一个 Image,而是返回一个承诺对象,只有当网络请求到需要的数据时,这个承诺才会兑现(返回一个 Image 对象给调用者),否则承诺被拒绝(网络错误或者图片不存在),无论兑现还是拒绝,这个承诺对象都会标记为已解决(resolve),已解决的承诺会被销毁。如果既不兑现,也不拒绝,则这个承诺会一直有效(即未解决)。
假设我们有这样一个方法:
-(void)jsonPostUrl:(NSString*)url params:(NSDictionary<NSString *,id>*)params success:(nullable void (^)(NSURLSessionDataTask *task, NSDictionary* responseDic))success
failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure{
[[self postUrl:url params:params requestType:@"json" success:^(NSURLSessionDataTask *task, id responseObject) {
if([responseObject isKindOfClass:[NSDictionary class]]){
NSDictionary* dic = (NSDictionary*)responseObject;
NSNumber* code = dic[@"code"];
if(![code isEqual:@0]){
// [self.view makeToast:dic[@"msg"] duration:2 position:CSToastPositionCenter];
if(failure){
NSError *err = [NSError errorWithDomain:@"ServiceError" code:code.integerValue userInfo:@{NSLocalizedDescriptionKey: dic[@"msg"]}];
failure(task,err);
}
}else if(success!=nil){
success(task,dic);
}
}
} failure:failure]resume];
}
这样的代码相信很多程序员都编写过,你没有必要太认真了,只需要注意 3 个地方:
这段代码是异步的,因此需要提供很多回调块,以便当操作完成或失败时调用。这样的代码确实结构性不是很好,因为你阅读的时候不得不在异步块中跳来跳去,特别是当异步块还嵌套其它异步块时,这样的代码能让人看哭。
我们来看看怎样用 Promisekit 来重构它。首先肯定是需要导入头文件:#import <PromiseKit/PromiseKit.h>
然后新建一个方法:
-(AnyPromise*)jsonPostUrl:(NSString*)url params:(NSDictionary<NSString *,id>*)params{
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[[self postUrl:url params:params requestType:@"json" success:^(NSURLSessionDataTask *task, id responseObject) {
if([responseObject isKindOfClass:[NSDictionary class]]){
NSDictionary* dic = (NSDictionary*)responseObject;
NSNumber* code = dic[@"code"];
if(![code isEqual:@0]){
// [self.view makeToast:dic[@"msg"] duration:2 position:CSToastPositionCenter];
NSError *err = [NSError errorWithDomain:@"ServiceError" code:code.integerValue userInfo:@{NSLocalizedDescriptionKey: dic[@"msg"]}];
resolve(err);
}else{
resolve(dic);
}
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
resolve(error);
}] resume];
}];
}
代码和之前有了几个不同:
你可以将它和重构前的方法进行对比,发现方法中的代码基本上还是很像的,无非是将调用 success 和 failure 的地方改成用 resolve 了。resolve 参数不一定只有一个(这里我们简单化了,其实 PromiseKit 规定 resolve 最多可以带 3 个 id 类型参数),如果第一个参数类似为 NSError,表示承诺被拒绝,否则表示承诺兑现。
这样看来,PromiseKit 也没有什么特别的嘛。关键还是要看这个方法的调用。未重构之前我们需要这样调用这个方法:
-(void)loadAlbums:(NSString*)radioId pageNum:(int)pageNum pageSize:(int)pageSize success:(void (^)(NSArray<AlbumModel>* array))success failure:(void (^)(NSError* error))failure{
……
[self jsonPostUrl:url params:params success:^(NSURLSessionDataTask *task, NSDictionary* responseDic) {
NSError* error;
AlbumListResultModel *result=[[AlbumListResultModel alloc]initWithDictionary:responseDic error:&error];
if(error == nil){
if( result.code == 0){
dispatch_async(dispatch_get_main_queue(), ^{
if(success) success(result.data);
});
}else{
dispatch_async(dispatch_get_main_queue(), ^{
NSError* err = [NSError errorWithDomain:@"ServiceError" code:result.code userInfo:@{NSLocalizedDescriptionKey: result.msg}];
if(failure)failure(err);
});
}
}else{
dispatch_async(dispatch_get_main_queue(), ^{
if(failure)failure(error);
});
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if(failure)failure(error);
});
}];
}
这你应该很熟悉了,没有什么特别的。
而现在变成了:
-(AnyPromise*)loadAlbums:(NSString*)radioId pageNum:(int)pageNum pageSize:(int)pageSize{
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
……
[self jsonPostUrl:url params:params].then(^(NSDictionary* responseDic){
NSError* error;
AlbumListResultModel *result=[[AlbumListResultModel alloc]initWithDictionary:responseDic error:&error];
if(error == nil){
if( result.code == 0){
resolve(result.data);
}else{
NSError* err = [NSError errorWithDomain:@"ServiceError" code:result.code userInfo:@{NSLocalizedDescriptionKey: result.msg}];
resolve(err);
}
}else{
resolve(error);
}
});
}];
}
是不是代码变得漂亮了?一个 then 方法取代了原来的异步块回调。这里也不需要对错误进行处理,因为这个方法还是一个承诺,我们直接将错误传递给调用者,让调用者处理:
[self loadAlbums:radioId pageNum:pageNum pageSize:pageSize].then(^(NSArray<AlbumModel>* array){
if(pageNum <= 1){
[_models removeAllObjects];
}
if(array!=nil){
[_models addObjectsFromArray: array];
}
}).catch(^(NSError* error){
[self showHint:error.localizedDescription];
});
同样,重构前是这样调用的:
[self loadAlbums:radioId pageNum:pageNum pageSize:pageSize success:^(NSArray<AlbumModel> *array) {
if(pageNum <= 1){
[_models removeAllObjects];
}
if(array!=nil){
[_models addObjectsFromArray: array];
}
} failure:^(NSError *error) {
[self showHint:error.localizedDescription];
}];
PromiseKit 支持指定 then 块的执行线程。你可以用 thenOn 方法指定代码块应该在哪个线程中进行(比如主线程):
NSString* radioId = [[AccountAdditionalModel currentAccount] radioId];
[self loadAlbums:radioId pageNum:pageNum pageSize:pageSize].thenOn(dispatch_get_main_queue(),^(NSArray<AlbumModel>* array){
if(pageNum <= 1){
[_models removeAllObjects];
}
if(array!=nil){
[_models addObjectsFromArray: array];
}
}).catch(^(NSError* error){
dispatch_async(dispatch_get_main_queue(), ^{
[self showHint:error.localizedDescription];
});
});
但是,catch 块并没有一个 catchOn 方法。我们只能自己调用 GCD API 了。
此外还有一个 always/alwaysOn 操作,用于执行无论 fulfill 还是 reject 都需要执行的动作。
Promise 最强的地方就是链式调用了,因此 PromiseKit 也支持链式调用。因为 then 操作返回的仍然是一个 AnyPromise,所以我们可以写出这样的代码:
[self loadAlbums:radioId pageNum:pageNum pageSize:pageSize].then(^(NSArray<AlbumModel>* array){
if(pageNum <= 1){
[_models removeAllObjects];
}
if(array!=nil){
[_models addObjectsFromArray: array];
}
}).thenOn(dispatch_get_main_queue(),^(NSArray<AlbumModel>* array){
[_tableView reloadData];
}).catch(^(NSError* error){
dispatch_async(dispatch_get_main_queue(), ^{
[self showHint:error.localizedDescription];
});
});
很显然,我们将原来的 then 块中的代码分成两部分进行了,因为 tableview 的 reloadData 是 UI 操作,需要在主线程中操作,所以单独分离出来放在 thenOn 块中执行。