如何使用 Reactive Cocoa 以正确的方式链接信号?

How to chain signals in a proper way with Reactive Cocoa?

我在新的 iOS 应用程序中使用 ReactiveCocoa。我是反应式编程的新手,所以我仍在尝试了解什么是链接信号的正确方法。 现在我有以下 "login with Twitter" 按钮的流程。

ALTUserManager class 有以下方法来管理整个登录阶段,方法是调用显示 Twitter 登录面板并执行所有 OAuth 操作的库中的一些函数:

- (RACSignal *)loginTwitter:(UIViewController *)vc {
    RACSignal *loginSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [[ALTTwitter sharedInstance]isLoggedIn:^(BOOL loggedIn) {
            if(loggedIn){
                [subscriber sendCompleted];
            }
            else{
                [[ALTTwitter sharedInstance]login:vc andSuccess:^{
                    [subscriber sendCompleted];
                } failure:^(NSString *error) {
                    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
                    userInfo[NSLocalizedDescriptionKey] = error;
                    [subscriber sendError:[NSError errorWithDomain:@"" code:1 userInfo:userInfo]];
                }];
            }
        }];
        return nil;
    }];
    return loginSignal;
}

我使用的是 MVVM 模式,因此在我的 ViewModel 中,我在其 init 方法中添加了以下命令:

self.twitterLoginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
    return [[ALTUserManager sharedInstance] loginTwitter:nil];
}];

在我的视图控制器中,我正在处理表示逻辑,我在显示进度 HUD 时阻塞了界面,并最终报告错误,如果一切正常,则跳过登录屏幕:

self.twBtn.rac_command = self.viewModel.twitterLoginCommand;
[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
    NSLog(@"%@", x);
    [x subscribeCompleted:^{
        NSLog(@"%@", @"completed");
        [ALTAlert wait:@""];
        [[self.viewModel appLoginWithTwitter] subscribeNext:^(id x) {
            NSLog(@"%@", x);
        } error:^(NSError *error) {
            [ALTAlert dismiss];
            [ALTAlert error:error.localizedDescription];
        } completed:^{
            [ALTAlert dismiss];
            @strongify(self);
            [self goToChart];
        }];
    }];
}];
[self.viewModel.twitterLoginCommand.errors subscribeNext:^(NSError *error) {
    NSLog(@"Login error: %@", error);
    [ALTAlert dismiss];
    [ALTAlert error:error.localizedDescription];
}];

我很确定这可以用更好的方式重写。我主要关心的是 [x subscribeCompleted] 行。什么是正确的方法? 谢谢!

更新 我尝试将所有逻辑移动到 RACCommand 内的 ViewModel,但我仍然需要捕获 RACCommand 内发生的错误。 订阅 errors 信号不是一个选项,因为 RACCommand 仍然会 return completed 事件,从而使我的表示逻辑无法判断是否一切正常或不是。 我还没有尝试在 RACCommand 中设置一个 BOOL 并在出现错误时产生副作用并在视图中观察它。但无论如何,这种方法似乎有点老套。

不确定您是否看过设计指南,但这些指南向您展示了一些如何避免 -subscribeNext:error:completed: 模式的解决方案。具体来说 these:

  • The RAC() or RACChannelTo() macros can be used to bind a signal to a property, instead of performing manual updates when changes occur.
  • The -rac_liftSelector:withSignals: method can be used to automatically invoke a selector when one or more signals fire.
  • Operators like -takeUntil: can be used to automatically dispose of a subscription when an event occurs (like a "Cancel" button being pressed in the UI).

您可以使用 then 帮助程序稍微简化嵌套,这将简化错误处理并防止单独的 twitterLoginCommand.errors 订阅:

[self.viewModel.twitterLoginCommand.executionSignals subscribeNext:^(id x) {
    [x then:^{
        NSLog(@"%@", @"completed");
        [ALTAlert wait:@""];
        return [self.viewModel appLoginWithTwitter];
    }] subscribeNext:^(id x) {
        NSLog(@"%@", x);
    } error:^(NSError *error) {
        [ALTAlert dismiss];
        [ALTAlert error:error.localizedDescription];
    } completed:^{
        [ALTAlert dismiss];
        @strongify(self);
        [self goToChart];
    }];
}];

不过这有点奇怪。因为如果 twitterLoginCommandappLoginWithTwitter 信号完成之前再次触发,您可能会进入奇怪的状态。考虑到应用程序的其余部分,这可能是不可能的,但仅孤立地查看此代码块是我会担心的事情。

更好的做法可能是将 then 块移动到 RACCommand 中,以确保这种情况永远不会发生(因为 RACCommand 不会再次执行,直到前一个完成了执行。)虽然没有看到更多的代码,但我不能说这是否是一个合理的改变。

这是一件很难进一步清理的事情,因为它本身就有副作用。如果你为 ALTAlert class 创建一个反应桥,你可以清理很多订阅,就像你可以说 "look at this signal of signals, and make your state reflect it." 然后你可以只传递执行信号而不是不得不担心在这里做一些更粗暴的事情。

那么你唯一真正的副作用是 goToChart,你可以做一些更简单的事情:

[[[self.viewModel.twitterLoginCommand.executionSignals flattenMap:^(id x) {
    return [x materialize];
}] filter:^(RACEvent *event) {
    return event.eventType == RACEventTypeCompleted;
}] subscribeNext:^(id x) {
    @strongify(self);
    [self goToChart];
}];