Question

I have one RACSignal that carries NSArray of objects of class Bookmark

RACSignal *bookmarksSignal = ... // Carries @[[[Bookmark alloc] init], ...]

And two RACCommands to add and delete bookmark

RACCommand *addBookmarkCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){
        return [RACSignal return:input];
    }];
...
[addBookmarkCommand execute: bookmark1];
RACCommand *deleteBookmarkCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input){
        return [RACSignal return:input];
    }];
...
[deleteBookmarkCommand execute: bookmark2]

Commands are executed on user interaction. How can I combine bookmarksSignal and executions signals of both commands to create signal that carries NSArray of bookmarks that contains original bookmarks with those added by addBookmarkCommand and without those removed by deleteBookmarkCommand?

(bookmarks are compared by url property)

I'm afraid that I'm missing something obvious, but I can't figure out how to do this in pure way.

Was it helpful?

Solution

For lack of time, I'll go with a more laconic answer compared to @IanHenry's :)

For anything here that doesn't make sense, please comment and I'll be happy explain in detail.

// Map additions into blocks that add the bookmark to the given mutable array.
RACSignal *addBookmarkSignal = [[addBookmarkCommand concat] map:^(Bookmark *bookmark) {
    return ^(NSMutableArray *bookmarks) {
        return [bookmarks addObject:bookmark];
    };
}];

// Map deletions into blocks that remove the bookmark from the given mutable array.
RACSignal * addBookmarkSignal = [[deleteBookmarkCommand concat] map:^(Bookmark *bookmark) {
    return ^(NSMutableArray *bookmarks) {
        [bookmarks removeObject:bookmark];
    };
}];

// Combine the add and delete functions into a single signal.
RACSignal *updatesSignal = [RACSignal merge:@[addBookmarkSignal, addBookmarkSignal]];

RACSignal *updatedBookmarksSignal = [[bookmarksSignal
    // Each time bookmarksSignal sends an array, this -map: builds a
    // signal that updates the latest list of bookmarks, and sends it. 
    map:^(NSArray *bookmarks) {
        NSMutableArray *mutableBookmarks = [bookmarks mutableCopy];

        // Using the update blocks from the add/delete commands,
        // produce the modified list of bookmarks.
        return [[updatesSignal
            map:^(void (^update)(NSMutableArray *)) {
                update(mutableBookmarks);
                return [mutableBookmarks copy];
            }]
            startWith:bookmarks];
    }]
    // When bookmarksSignal sends anew, switch to the newest signal of updates.
    switchToLatest];

OTHER TIPS

Let's forget about RACCommand for a second, and pretend that we just have a signal of bookmarks to add and a signal of bookmarks to remove and we want to create a signal of sets of bookmarks based on that. It's a good starting point, and we can figure out how to implement those after the fact.

RACSignal *addedSignal = ...;
RACSignal *removedSignal = ...;

Then we want a signal that's the combination of those two signals into a single set of all the added things without anything removed afterwards (we can map it into an NSArray after the fact if we want to).

RACSignal *bookmarkSetSignal = ...;

Now we have to fill it in. We could optimize here by making a mutable set that we modify and just send references to that same set every time a change happens. But that's sort of contrary to the nature of signals. Let's put that optimization on hold for a minute and do it the pure, functional way.

We're going to use the scanWithStart:reduce: method, because it fits this problem perfectly. It's like a fold that returns every intermediate value, which is exactly what we want.

But first we have to make addedSignal and removedSignal useful. Here's my idea: merge them into a single signal, but attach another value to them that says whether it's an add or a remove.

// turn a signal of bookmarks into a signal of tuples of the form (bookmark, isAdded)
RACSignal *changes = [RACSignal merge:@[[addedSignal map:^(id x) { return RACTuplePack(x, @YES); }],
                                        [removedSignal map:^(id x) { return RACTuplePack(x, @NO); }]]];

Now that it's just one signal, it'll be a little easier to wrangle it. Then we can fold those changes into a single value (the composition of all the changes), except that we're reporting every step of the way. So it's a scan, not a fold. But scan makes a bad verb. Anyway:

RACSignal *bookmarkSetSignal = [changes scanWithStart:[NSSet set] reduce:^(NSSet *running, RACTuple *next) {
    RACTupleUnpack(Bookmark *bookmark, NSNumber *isAddingNumber) = next;
    if (isAddingNumber.boolValue) {
        [running setByAddingObject:bookmark];
    } else {
        // you can do this with a much nicer helper, but this is the shortest way for this answer...
        return [running filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"url != %@", bookmark.url]];
    }
}];

Great! We started with an empty set, and every time a change occurred we created a new set by adding or removing that element. running is always whatever we computed last time (starting from the empty set), and next is the description of the change that should happen (a bookmark + whether or not it was being added or removed). We now have a signal of sets of bookmarks, just like we wanted!

Except now we need to fill out addedSignal and removedSignal.

The exact way we do this is a little...well, it depends on the user interaction. We could make each one a subject, and then user interaction would manually send new values. That might be the right way to do it. It's cleaner than manually triggering an RACCommand. Anyway, that's a separate question. Assuming the exact RACCommand format that you have now, I think we can implement it like this:

RACSignal *addedSignal = [self.addBookmarkCommand.executionSignals switchToLatest];
RACSignal *removedSignal = [self.deleteBookmarkCommand.executionSignals switchToLatest];

executionSignals is a signal of every signal returned in the signal block. Which are merely [RACSignal return:bookmark]. By switchingToLatest, we're basically "unwrapping" that value. But if we used a subject we wouldn't need to wrap/unwrap in the first place. But anyway, separate discussion.

This code will almost certainly require a little modification to do what you want (haven't tested it), but hopefully this'll point you in the right direction idea-wise.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top