In ReactiveCocoa, is there a mechanism similar to merge: that completes when any of the signals that are being merged complete?
I found a workaround that involves concatenating the input signal with a [RACSignal return:foo] and then adding a take:1 after the merge, but that seems rather long-winded. Is there a simpler way?
Not built-in to ReactiveCoca. This is probably something you should define in a helper category on RACSignal, so that any long-windedness is hidden behind a nice method abstraction.
Here's an (untested) example using materialize, which will give you a signal of signal events so you don't need to append anything onto your input signals:
+ (RACSignal *)sheepishMerge:(NSArray *)signals {
RACSequence *completions = [signals.rac_sequence map:^(RACSignal *signal) {
return [[signal materialize] filter:^(RACEvent *event) {
return event.eventType == RACEventTypeCompleted;
}];
}];
RACSignal *firstCompletion = [[RACSignal merge:completions] take:1];
return [[RACSignal merge:signals] takeUntil:firstCompletion];
}
Related
I created this state machine to handle events. I'm unsure about my implementation firstly because I don't use a transition table or anything like that, instead, I simply input the next event/state that needs to happen. I think this is cleaner than calling method to handle event b inside the method to handle event a, right?
Secondarily, since some of the methods have parameters - and this is not swift where enum members can have associated values, I added a param for that via withObj argument. I don't see a problem with this but want to know if this is confusing/violating something from your perspective.
I also found this useful for async task, so after the task completes I set resulting value to a local variable then update state to packData using that stored value amongst many other values already existing. This value is packed in a byte array which cannot be used in blocks so this was a useful workaround - although I haven't tested yet.
States are defined via enum like so:
typedef NS_ENUM(NSUInteger, ExampleState) {
ExampleStateIdle,
ExampleStateWaitingForAsyncTask,
ExampleStateReadyToPack,
ExampleStateRespond,
ExampleStateSomethingInProgress,
ExampleStateSomething2InProgress,
ExampleStateComplete
};
Here I define a method to handle each event using helper methods. I also have an extra argument for any state that might have an associated value.
- (void)updateState:(ExampleState)state withObj:(id)obj {
self.currentState = state;
switch (state) {
case ExampleStateWaitingForAsyncTask:
[self getAsyncInfo];
break;
case ExampleStateReadyToPack:
[self packData];
break;
case ExampleStateRespond:
[self respond:obj];
break;
case ExampleStateComplete:
[self showPopup];
break;
default:
break;
}
}
Example usage:
-(void)packData {
NSData *data = [NSData dataWithBytes:&result length:resultIndex];
// next step is to respond to `client`/`central` with data
[self updateState:ExampleStateRespond withObj:data];
}
I combine three signals together to get the latest values, but I only want the subscription is only triggered when the value of the specific signal (toppestStretchPercentageSignal) is changed.
How to deal with it?
The subscription of the following code will be triggered when any value of those three signals is changed.
[[RACSignal
combineLatest:#[contentOffsetDidChangeSignal,
toppestVisibleItemSignal,
toppestStretchPercentageSignal]]
subscribeNext:^(RACTuple *tuple) {
NSLog(#"need to be only triggered when toppestStretchPercentageSignal sends the next value");
}];
Update:
Thank jhosteny's answer. It looks like I just need to add sample: method after combineLatest:, doesn't it?
[[[RACSignal
combineLatest:#[contentOffsetDidChangeSignal,
toppestVisibleItemSignal,
toppestStretchPercentageSignal]]
sample:toppestStretchPercentageSignal]
subscribeNext:^(id x) {
NSLog(#"update toppest item frame");
}];
I think you want something like this:
[[RACSignal
zip:#[toppestStretchPercentageSignal,
[[RACSignal
combineLatest:#[contentOffsetDidChangeSignal,
toppestVisibleItemSignal]]
sample:toppestStretchPercentageSignal]]] subscribeNext:^(RACTuple *tuple) {
RACTupleUnpack(NSNumber *toppestStretch, RACTuple *tuple2) = tuple;
RACTupleUnpack(id contentOffset, id visibleItem) = tuple2;
}];
See this comment in the ReactiveCocoa issues section on GitHub.
Ok, I have a signal that sends an event when a certain method of a protocol gets called in response to some data getting retrieved from the server:
self.dataReceivedSignal = [[self rac_signalForSelector:#selector(didReceiveData:) fromProtocol:#protocol(DataServiceDelegate)] mapReplace:#YES];
This signal is then used to fire another signal that formats and returns the data:
- (RACSignal *)dataSignal
{
return [RACSignal combineLatest:#[self.dataReceivedSignal] reduce:^id(NSNumber * received){
...
return my_data;
}];
}
This view controller just listens to this second signal to get the data.
This works fine.
The problem is, the second time I enter this view controller, I don't want to load the data again, so I save it locally and do this:
if (!self.alreadyHasData) {
self.dataService = [[DataService alloc] init];
self.dataService.delegate = self;
[self.dataService getData];
} else {
self.dataReceivedSignal = [RACSignal return:#YES];
}
In case I already have the data, I'm replacing the dataReceivedSignal with a new one that just sends #YES and completes.
This works too, but that if/else doesn't seem too functional to me. Is this the correct approach?
Thanks.
First of all you can exchange combineLatest to map.
If you want not reload data if it's already loaded, you can write something like this:
- (RACSignal *)dataSignal
{
if (!_dataSignal) {
RACMulticastConnection *dataConnection = [[self.dataReceivedSignal map:^id(NSNumber * received){
/// ...
return my_data;
}] multicast:[RACReplaySubject replaySubjectWithCapacity:1]];
// Only do all of the above after one subscriber has attached.
_dataSignal = [RACSignal defer:^{
[dataConnection connect];
return dataConnection.signal;
}];
}
return _dataSignal;
}
And don't matter how much subscribers signal will have, retrieve data block will be called only one time.
Simpler code = better code. I think you can solve task with simpler solution without RAC.
The client I'm building is using Reactive Cocoa with Octokit and so far it has been going very well. However now I'm at a point where I want to fetch a collection of repositories and am having trouble wrapping my head around doing this the "RAC way"
// fire this when an authenticated client is set
[[RACAbleWithStart([GHDataStore sharedStore], client)
filter:^BOOL (OCTClient *client) {
return client != nil && client.authenticated;
}]
subscribeNext:^(OCTClient *client) {
[[[client fetchUserRepositories] deliverOn:RACScheduler.mainThreadScheduler]
subscribeNext:^(OCTRepository *fetchedRepo) {
NSLog(#" Received new repo: %#",fetchedRepo.name);
}
error:^(NSError *error) {
NSLog(#"Error fetching repos: %#",error.localizedDescription);
}];
} completed:^{
NSLog(#"Completed fetching repos");
}];
I originally assumed that -subscribeNext: would pass an NSArray, but now understand that it sends the message every "next" object returned, which in this case is an OCTRepository.
Now I could do something like this:
NSMutableArray *repos = [NSMutableArray array];
// most of that code above
subscribeNext:^(OCTRepository *fetchedRepo) {
[repos addObject:fetchedRepo];
}
// the rest of the code above
Sure, this works, but it doesn't seem to follow the functional principles that RAC enables. I'm really trying to stick to conventions here. Any light on capabilities of RAC/Octokit are greatly appreciated!
It largely depends on what you want to do with the repositories afterward. It seems like you want to do something once you have all the repositories, so I'll set up an example that does that.
// Watch for the client to change
RAC(self.repositories) = [[[[[RACAbleWithStart([GHDataStore sharedStore], client)
// Ignore clients that aren't authenticated
filter:^ BOOL (OCTClient *client) {
return client != nil && client.authenticated;
}]
// For each client, execute the block. Returns a signal that sends a signal
// to fetch the user repositories whenever a new client comes in. A signal of
// of signals is often used to do some work in response to some other work.
// Often times, you'd want to use `-flattenMap:`, but we're using `-map:` with
// `-switchToLatest` so the resultant signal will only send repositories for
// the most recent client.
map:^(OCTClient *client) {
// -collect will send a single value--an NSArray with all of the values
// that were send on the original signal.
return [[client fetchUserRepositories] collect];
}]
// Switch to the latest signal that was returned from the map block.
switchToLatest]
// Execute a block when an error occurs, but don't alter the values sent on
// the original signal.
doError:^(NSError *error) {
NSLog(#"Error fetching repos: %#",error.localizedDescription);
}]
deliverOn:RACScheduler.mainThreadScheduler];
Now self.repositories will change (and fire a KVO notification) whenever the repositories are updated from the client.
A couple things to note about this:
It's best to avoid subscribeNext: whenever possible. Using it steps outside of the functional paradigm (as do doNext: and doError:, but they're also helpful tools at times). In general, you want to think about how you can transform the signal into something that does what you want.
If you want to chain one or more pieces of work together, you often want to use flattenMap:. More generally, you want to start thinking about signals of signals--signals that send other signals that represent the other work.
You often want to wait as long as possible to move work back to the main thread.
When thinking through a problem, it's sometimes valuable to start by writing out each individual signal to think about a) what you have, b) what you want, and c) how to get from one to the other.
EDIT: Updated to address #JustinSpahrSummers' comment below.
There is a -collect operator that should do exactly what you're looking for.
// Collect all receiver's `next`s into a NSArray. nil values will be converted
// to NSNull.
//
// This corresponds to the `ToArray` method in Rx.
//
// Returns a signal which sends a single NSArray when the receiver completes
// successfully.
- (RACSignal *)collect;
I have a method on ParentViewModel which returns an RACSequence of ViewModel objects like so:
- (RACSequence *) viewModels
{
return [self.models.rac_sequence map:^id(Model *model) {
return [[ViewModel alloc] initWithModel: model];
}];
}
Each of the ViewModels has a state property on which is an enum and has 3 states: NotStarted, InProgress and Completed. When all the ViewModels in my sequence have the state Completed I know ParentViewModel is valid. I have a validSignal on the ParentViewModel which I want to derive the fact that is valid from the viewModels sequence. At the moment I have this code:
BOOL valid = [[self viewModels] all:^BOOL(ViewModel *vm) {
return vm.state == Completed;
}];
Which gives me an indicator if all ViewModels in the sequence are valid. How can I then turn this into a RACSignal which will update every time the state property on one of the ViewModels changes?
You need first to turn state into a RACSignal, and then everything is easy from that point.
The final code will be something like the following:
RACSignal *valid = [[RACSignal combineLatest:
[[self viewModels] map:^id(ViewModel *viewModel) {
return RACAbleWithStart(viewModel, state);
}]
]
map:^(RACTuple *states) {
return #([states.rac_sequence all:^BOOL(NSNumber *state) {
return state.unsignedIntegerValue == Completed;
}]);
}
];
The first block maps each of your view models into a signal that observes the state property (with the starting value as first value of the signal).
combineLatest: will take a collection of RACSignals and will create a new signal that fires everytime one of the underlaying signals changes, and sends a RACTuple with the value of each signal.
That RACTuple is then converted into a RACSequence, and we can generate a value of #YES or #NO depending if all the values are Completed or not.
I think the result is the signal you were looking for.
(Disclaimer: I’m new to ReactiveCocoa, so there may be an easier way).