0

As I'm slowly trying to wrap my head around ReactiveCocoa I wrote this piece of code and I'm fairly sure there's a better way to solve my problem. I'd appreciate input on how to improve / redesign my situation.

@weakify(self);

[RACObserve(self, project) subscribeNext:^(MyProject *project) {
    @strongify(self);
    self.tasks = nil;
    [[[project tasks] takeUntilBlock:^BOOL(NSArray *tasks) {
        if ([tasks count] > 0) {
            MyTask *task = (MyTask *)tasks[0];
            BOOL valid = ![task.projectID isEqualToString:self.project.objectID];
            return valid;
        }
        return NO;
    }] subscribeNext:^(NSArray *tasks) {
        self.tasks = tasks;
    }];
}];

What this does:

I have a View Controller with a property called project of type MyProject and a property tasks of type NSArray. A project has a tasks signal that returns an array of MyTasks. The project can be changed at any time from the outside. I want my view controller to respond and refresh itself when said case occurs.

Problem I'm trying to solve:

I used to [[project tasks] subscribeNext:...] within the first block, until I realized that if the webrequest took too long and I switched the project in the meantime, I received and assigned data from the old project in the new context! (Shortly thereafter the new data set arrived and everything went back to normal).

Nevertheless, that's the problem I had and I solved it by using the takeUntilBlock: method. My question is: How can I simplify / redesign this?

pkluz
  • 4,871
  • 4
  • 26
  • 40

1 Answers1

4

The crucial operator to most naturally take the tasks of the most recent project is -switchToLatest. This operator takes a signal of signals and returns a signal that sends only the values sent from the latest signal.

If that sounds too abstract, it will help to put it in terms of your domain. First, you have a signal of projects, specifically RACObserve(self, project). Then, you can -map: this project signal into a signal that contains the result of the call to -tasks, which happens to return a signal. Now you have a signal of signals. Applying -switchToLatest to the signal of task signals will give you a signal of tasks, but only sending tasks from the most recent project, never "old" tasks from a previously assigned project.

In code, this looks like:

[[RACObserve(self, project)
    map:^(MyProject *project) {
        return [project tasks];
    }]
    switchToLatest];

Another idiom you can apply to simplify your code is to use the RAC() macro, which assigns to a property while avoiding explicit subscription.

RAC(self, tasks) = [[RACObserve(self, project)
    map:^(MyProject *project) {
        return [project tasks];
    }]
    switchToLatest];

Update

To address the questions in the comments, here's an example of how you could initialize the tasks property to nil following an change of the project, and also a simplistic approach to handling errors in the -tasks signal.

RAC(self, tasks) = [[RACObserve(self, project)
    map:^(MyProject *project) {
        return [[[project
            tasks]
            startsWith:nil]
            catch:^(NSError *error) {
                [self handleError:error];
                return [RACSignal return:nil];
            }];
    }]
    switchToLatest];
Dave Lee
  • 6,299
  • 1
  • 36
  • 36
  • Fabulous! Just what i needed! One follow up question: Am I right in assuming that the best way to have self.tasks nil'ed on project change (until some dataset is received) is to sendNext:nil from within the tasks signal before doing the webrequest? – pkluz Dec 31 '13 at 12:04
  • Ran into another issue and I'm syntactically not familiar enough to know how to handle it. Assume tasks also uses sendError:, the above code crashes for me. I guess I need a way to handle the error event. Where do I do that? Exception: '[[RACObserve(self), project)] -map:] -switchToLatest in binding for key path "tasks" on self: Error Domain=...' – pkluz Dec 31 '13 at 13:29
  • Regarding my second follow up question, I solved it after finding https://github.com/ReactiveCocoa/ReactiveCocoa/issues/730 (second to last post) by injecting a `doError:` call. Nevertheless, still looking for verification that what I'm doing isn't utterly stupid :) – pkluz Dec 31 '13 at 15:00
  • 2
    `-doError:` is one way to respond to errors, but in some cases, such as when using the `RAC()` macro, you'll want to instead use [`-catch:`](https://github.com/ReactiveCocoa/ReactiveCocoa/blob/74ebd6de5138d68469f59ef5faccfe083919912f/ReactiveCocoaFramework/ReactiveCocoa/RACSignal%2BOperations.h#L311-312), which will allow you to squelch an error and return some default value using `+[RACSignal return:]`. It's important to not that you'll want to apply the `-catch:` operator to the signal returned from `-tasks`. – Dave Lee Dec 31 '13 at 18:16
  • 2
    As for a default value of `nil`, the functional way to do that is to apply `-startsWith:` to the signal returned from `-tasks`. This will cause `self.tasks` to sent immediately, followed by the actual tasks when they're available. – Dave Lee Dec 31 '13 at 18:17