【问题标题】:Refactoring into ReactiveCocoa重构为 ReactiveCocoa
【发布时间】:2013-05-27 02:41:16
【问题描述】:

所以我最近刚开始使用 ReactiveCocoa,我认为最好的学习方法就是直接开始并开始重构我拥有的一些现有代码。我想得到一些批评,并确保我朝着正确的方向前进。

所以在我正在重构的应用程序中,我有很多这样的代码:

[self.ff getArrayFromUri:@"/States?sort=name asc" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) {
    if(!theErr) {
       //do something with theObj
    }
    else {
       //handle the error
    }
}];

我目前在 ReactiveCocoa 中对此进行了重构,如下所示:

-(void)viewDidLoad {
 //ReactiveCocoa
RACCommand *command = [RACCommand command];
RACSubject *subject = [RACSubject subject];
[[[command addSignalBlock:^RACSignal *(id value) {
    NSError *err;
    NSArray *array = [self.ff getArrayFromUri:@"/States" error:&err];
    err ? [subject sendError:err] : [subject sendNext:array];
    return [RACSignal empty];
}]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]];

[subject subscribeNext:^(NSArray *x) {
    [self performSegueWithIdentifier:kSomeSegue sender:x];
} error:^(NSError *error) {
    NSLog(@"the error = %@", error.localizedDescription);
}];

self.doNotLocation = [UIButton buttonWithType:UIButtonTypeCustom];
[self.doNotLocation setBackgroundImage:[UIImage imageNamed:@"BlackButton_small.png"] forState:UIControlStateNormal];
[[self.doNotLocation rac_signalForControlEvents:UIControlEventTouchUpInside] executeCommand:command];
RAC(self.doNotLocation.enabled) = RACAbleWithStart(command, canExecute);
RAC([UIApplication sharedApplication],networkActivityIndicatorVisible) = RACAbleWithStart(command, executing);    }

这是关于我应该如何处理它,使用 RACSubject,还是有更好的方法?这整个概念对我来说是新的,因为到目前为止我唯一的编程语言是 Java 和 Objective-C,所以这种函数式反应式思维方式让我有点失望。

【问题讨论】:

    标签: ios objective-c refactoring reactive-cocoa


    【解决方案1】:

    很遗憾,您提供的代码示例存在一些问题:

    1. 传递给-addSignalBlock: 的块返回一个空信号。这应该是一个警告标志,因为几乎没有垃圾返回值。在这种情况下,这意味着块同步执行其工作。为避免阻塞主线程,您应该创建一个异步工作的信号,然后返回它。
    2. -switchToLatest-deliverOn: 没有做任何事情。大多数信号运算符仅在订阅了结果信号时才起作用。在这种情况下,它会消失在以太中。

    我们可以同时解决这两个问题。 -addSignalBlock: 返回一个块中返回的信号的信号。如果我们返回一些有意义的东西,它可以在该方法之外处理。

    首先,这个需要加到顶部:

    @weakify(self);
    

    下面使用@strongify(self)时,会阻止a retain cycle。这是必要的,因为RACCommand 的寿命与self 一样长。

    现在,创建内部信号:

    RACSignal *requestSignals = [command addSignalBlock:^(id value) {
        return [RACSignal start:^(BOOL *success, NSError **err) {
            @strongify(self);
    
            NSArray *array = [self.ff getArrayFromUri:@"/States" error:err];
            *success = (array != nil);
    
            return array;
        }];
    }];
    

    在块中,这只是创建一个信号,该信号将调用-getArrayFromUri:error: 并将其结果或错误(如果发生)传回。 +start: 将确保工作在后台进行。

    在所有这些中,我们得到requestSignals,它是这些已创建信号的一个信号。这样可以完全替代原来使用的RACSubject

    RACSignal *arrays = [[[requestSignals
        map:^(RACSignal *request) {
            return [request catch:^(NSError *error) {
                NSLog(@"the error = %@", error);
                return [RACSignal empty];
            }];
        }]
        flatten]
        deliverOn:RACScheduler.mainThreadScheduler];
    

    首先,我们将每个内部信号转换为记录错误,然后忽略错误。 (有点复杂,不过以后有RAC运营商might be added来做。)

    然后我们flatten信号的信号。结果arrays 是一个通过所有 内部信号值的信号。这就是我们必须忽略错误的原因——如果其中任何一个达到这一点,我们将永远停止从内部信号中获取所有值。

    最后,我们“提升”选择器来调用:

    [self rac_liftSelector:@selector(performSegueWithIdentifier:sender:) withObjects:kSomeSegue, arrays];
    

    每当arrays 发送一个新值(这将是从网络返回的NSArray)时,这将重新发送-performSegueWithIdentifier:sender:。你可以把它想象成随着时间的推移调用一个方法。这是better than a subscription,因为它简化了副作用和内存管理。

    【讨论】:

    • 感谢您的回复,非常感谢。在倒数第二个示例中,您提供的代码有一个问题。 [request catch:] 表示它需要返回 RACSignal 而不是 void。我只是错过了什么吗?
    • 啊,对不起,你完全正确。您可以只返回[RACSignal empty](表示错误信号应替换为立即成功的信号)。我会更新我的答案。
    • 好的,所以我已经更新了所有内容,它正在工作,除了我得到了很大的延迟(大约 3 秒),而且它似乎阻塞了主线程(所有用户交互都没有响应)从我点击按钮到prepareForSegue:sender: 被调用。但是,如果我执行RACSignal starWithScheduler:block 并为调度程序传递[RACScheduler mainThreadScheduler],它会按我的预期执行。那么我在这里错过了什么?老实说,这些东西让我觉得自己像个白痴,也许我应该在处理这些东西之前回到“Haskell for pre-K”。
    • @terrylewis 您是否使用上述代码的最新版本(信号块内带有+start:)?我还在扁平信号中添加了-deliverOn:,以确保在主线程上执行 segue。对不起所有的复杂性!命令的使用和理解通常比框架的其他部分更复杂。
    • 是的,我有最新版本的代码。这是我目前正在使用的内容的要点,link
    【解决方案2】:

    根据我使用该框架的经验,我发现没有理由直接使用RACSubject,尤其是对于像这样的一次性信号。 RACSubjects 表示可变信号,在这种情况下您不需要,实际上会增加代码的复杂性。你最好在该命令块内返回一个普通信号(通过+[RACSignal createSignal:]),然后让生成的请求代码构成信号的主体:

    [[[command addSignalBlock:^RACSignal *(id value) {
        //
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            //Request code here
                return nil;
        }];
    }]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 
    

    或者,更好的是,您可以重构 getArrayFromUri:error: 以返回一个信号并摆脱该三元语句:

     [[[command addSignalBlock:^RACSignal *(id value) {
         return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            //...
            [[self getArrayFromUri:@"/States"]subscribeError:^(NSError *error) {
                [subscriber sendError:error];
            } completed:^{
                [subscriber sendNext:array];
            }];
                return nil;
            }];
      }]switchToLatest]deliverOn:RACScheduler.mainThreadScheduler];
    

    至于下一行的订阅问题,这些都可以认为是信号的副作用,所以我们可以明确地使它们如此,将do:的相应变体应用于命令的信号:

        [[[command addSignalBlock:^RACSignal *(id value) {
            return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) {
                    NSLog(@"the error = %@", error.localizedDescription);
                    [subscriber sendError:err];
                }] doNext:^(NSArray *array) {
                    [subscriber sendNext:array];
                    [self performSegueWithIdentifier:kSomeSegue sender:array];
                }] subscribeCompleted:^{
                    [subscriber sendCompleted];
                }];
                return [RACDisposable disposableWithBlock:^{
                     // Cleanup
                }];
            }];
        }]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 
    

    最后,由于命令与信号的工作方式不同,最外层的运算符不会被评估(感谢@jspahrsummers),因此您可以删除它们。

    [command addSignalBlock:^RACSignal *(id value) {
                return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
                    [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) {
                        NSLog(@"the error = %@", error.localizedDescription);
                        [subscriber sendError:err];
                    }] doNext:^(NSArray *array) {
                        [subscriber sendNext:array];
                        [self performSegueWithIdentifier:kSomeSegue sender:array];
                    }] subscribeCompleted:^{
                        [subscriber sendCompleted];
                    }];
                    return [RACDisposable disposableWithBlock:^{
                         // Cleanup
                    }];
                }];
            }]; 
    

    【讨论】:

    • 不幸的是,我无法重构 getArrayFromUri:error,因为它是第三方框架,我只能访问标题。但是,我确实将所有内容都切换到了您拥有它的方式,并且它的工作原理是一样的。学习这个感觉就像从头开始一样。
    • 是的,这个框架确实有一个非常陡峭的学习曲线,但是一旦你开始了解它的工作原理,它就会非常强大。
    • @CodaFi switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]] 在您提供的所有示例中都是无操作的,因为之后不会发生订阅。与您的 -do… 方法相同。
    • 是的,我明白(可能应该在帖子正文中重复)。
    • 在您的最终代码示例的do 案例中,我的意思是网络请求永远不会触发,因为那里没有订阅。
    猜你喜欢
    • 2013-06-11
    • 1970-01-01
    • 1970-01-01
    • 2015-11-19
    • 2015-12-23
    • 1970-01-01
    • 2014-11-18
    • 2015-11-17
    • 1970-01-01
    相关资源
    最近更新 更多