PromiseKit O-C 指南

Posted 颐和园

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PromiseKit O-C 指南相关的知识,希望对你有一定的参考价值。

话说 Swift 3.0 已经推出了一段时间了,PromiseKit 也已经升级至 4.1.7(5.0仍然是 alpha 版)和 Swift 3.0。PromiseKit 支持 O-C 和 Swift,但如果你真的需要在你的 O-C 项目中使用 PromiseKit ,仍然不是那么容易。

使用 CocoaPods 安装

很简单: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 重构你的代码

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 个地方:

  1. 这个方法有一个成功回调块 success。当网络请求返回正确的结果(JSON 对象的 code 为 0 )时回调 success。
  2. 这个方法有一个失败回调块 failure。当网络请求出现错误时回调 failure。
  3. 方法中调用了 postUrl:… 方法,那是另外一个辅助方法,用 AFHTTP 实现网络请求,具体实现就不用细说了,和你曾经实现过的没有什么不一样。

这段代码是异步的,因此需要提供很多回调块,以便当操作完成或失败时调用。这样的代码确实结构性不是很好,因为你阅读的时候不得不在异步块中跳来跳去,特别是当异步块还嵌套其它异步块时,这样的代码能让人看哭。

我们来看看怎样用 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];
    }];

}

代码和之前有了几个不同:

  1. 方法省去了 success 和 failure 两个异步块参数。异步回调块是毁坏我们代码的元凶祸首。
  2. 方法返回了一个 AnyPromise 对象。这是 PromiseKit 提供的主要的类,用于实现 Promise 模式。承诺不会执行,当然也不需要实现,但在将来特定时候会执行的(当然需要在未来实现,特别是调用这个方法时实现)动作。也就是说。success 和 failure 的功能由 AnyPromise 替代了。
  3. 方法中调用了 AnyPromise 的 promiseWithResolverBlock 方法来构造和返回 AnyPromise。这个方法会提供一个 resolve 参数。你可以把它仍然看成是一个回调块。只不过这个回调块同时充当了 success 和 failure。如果 resolve()传递了一个 NSError,则表明这个承诺被拒绝(即 failure),否则承诺会被兑现(success)。

你可以将它和重构前的方法进行对比,发现方法中的代码基本上还是很像的,无非是将调用 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 块中执行。

以上是关于PromiseKit O-C 指南的主要内容,如果未能解决你的问题,请参考以下文章

PromiseKit:无法在 then 处理程序之间调用自定义代码

使用 PromiseKit 进行单元测试代码

PromiseKit 首先围绕代码,而不是函数调用

带有可选承诺的 PromiseKit

PromiseKit 4 - 包装委托

Vue3官网-高级指南(十七)响应式计算`computed`和侦听`watchEffect`(onTrackonTriggeronInvalidate副作用的刷新时机`watch` pre)(代码片段