ReactiveCocoa, combine two signals on button enabled - ios

I'm using MVVM architecture and I have two signals:
RACSignal *internetEnabledSignal = RACObserve(self.regWizardVehicleViewModel, internetConnectionEnabled);
RACSignal *executingRegistrationSignal = RACObserve(self.regWizardVehicleViewModel, isExecuting);
I need to combine and bind signals on button enabled property. Button needs to be disabled if there is not any connection OR when some method is executing. I was searching and searching for solution but I don't know how to combine signals with OR. Is there any way to do that? Next question is: I'm using MBProgressHUD. I want to show that HUD while executing my async method in my Model. MBProgressHUD has show and hide methods, can't bind it to a property like button enabled?

There is a convenience method, -and which makes working with "boolean" value signals easier:
Returns a signal that applies AND to each NSNumber in the tuple.
To get a tuple with NSNumbers, use combineLatestWith: like this:
RAC(self.button, enabled) =[[[executingRegistrationSignal not]
combineLatestWith:internetEnabledSignal]
and];

If I'm not misunderstanding you, then the functionality should be easily achievable with combineLatest:reduce:, like so:
RACSignal * enabledSignal =
[RACSignal combineLatest:#[internetEnabledSignal, executingRegistrationSignal]
reduce:^id(NSNumber * internetEnabled, NSNumber * isExecuting) {
return #(internetEnabled.boolValue && !isExecuting.boolValue);
}].distinctUntilChanged;
RAC(self.button, enabled) = enabledSignal;
combineLatest:reduce: won't send any next events until all signals have fired at least once, so please keep that in mind.
Edit: Please see MichaƂ's answer for a much cooler solution via convenience methods.
As for your other question, I'm not very familiar with MBProgressHUD but this should do the trick:
[self.hud rac_liftSelector:#selector(show:) withSignalsFromArray:#[
[enabledSignal ignore:#NO]
]];
[self.hud rac_liftSelector:#selector(hide:) withSignalsFromArray:#[
[[enabledSignal ignore:#YES] mapReplace:#YES]
]];
This is a bit of a cheat, we're leveraging RAC's eager subscription to these signals to fire side effects, but if you're a pragmatist and not a pedantic idealist like me, you probably don't mind.
What's happening with this code is that we're asking RAC to perform the given selector after every one of the signals in the array we supplied fires, so it's a lot like combineLatest:. We're using mapReplace: because it'll use that signal as the (BOOL)animated argument (this is another reason why it's a cheat).

Related

Will code wait for return before continuing?

I using the below code to set elements in an array and return the array, the returned array is then passed to another method for more changes before again being returned.
NSMutableArray *returnArray = [[NSMutableArray alloc]init];
//call checkTP1
returnArray = [self checkTP1STD:addingTime :startToTP1 :TP1Result :nowDate :sevenHour :totalrest :returnArray];
//call check TP2
returnArray = [self checkTP2STD:addingTime :startToTP2 :TP2Result :nowDate :sevenHour :totalrest :returnArray :tp2Rest];
It is currently working as expected, my question is will it always wait for the checkTP1STD to return before executing checkTP2STD?
I have split the code into multiple methods to enable it to be more readable as i will be adding some other logic to pass different variable values to the methods,just wanted to make sure my basic idea will work.
In general: yes
Your question is curious, you seem to be concerned that checkTP1STD will return before checkTP2STD is called, but not that the calls to alloc and init will return before the call to checkTP1STD.
Are you actually intending to do asynchronous work in checkTP1STD (E.g. Using GCD or system framework methods which state they are async). If so the answer is still yes, but the call may return before all the work scheduled by checkTP1STD is complete - the very nature of asynchronous programming.
HTH
In short, yes. Code is executed in sequential order unless there's explicit calls to new threads, which you're not doing by the code you've given.

Unable to filter RACSignal events

My ViewController contains a UISearchBar and implements the UISearchBarDelegate protocol. I've created a signal for searchBartextDidChange: that fires correctly with subscribeNext:
RACSignal *searchTextChangeSignal = [self rac_signalForSelector:#selector(searchBar:textDidChange:) fromProtocol:#protocol(UISearchBarDelegate)];
[searchTextChangeSignal subscribeNext:^(id x){
// This works.
}];
At this point, I'd like to filter the results of this filter to 1) Only include text that is greater than 3 characters, and 2) throttled by 300 ms. My attempt:
[[searchTextChangeSignal filter:^(RACTuple *tuple) {
NSString *textEnteredIntoSearchBar = (NSString *)tuple.second;
return textEnteredIntoSearchBar.length > 3;
}] throttle:300];```
The code above doesn't work. The blocks are never executed. If I replace the filter method with subscribeNext, the subscribeNext block does execute. Furthermore, XCode autocompletes the filter method above, so the method is available. Is there something I'm missing here? What's the correct way to do this? Any help is much appreciated.
The missing understanding is that signals do not perform any work until they are subscribed to. Call one of the subscribe methods following the throttle, and you will see the data start to flow through.

How to make RACSignal to become hot?

ReactiveCocoa can convert the signal to "hot" signal by calling its -subscribeCompleted:. But I think this method is quite verbose if you do not care about the result (i.e. no subscribers).
RACDisposable *animationDisposable = [[self play:animation] subscribeCompleted:^{
// just to make the animation play
}];
And these 3 lines are not expressive enough to show my intention.
Is there any method for similar purpose? Thanks!
I want to do nothing except making it hot (=make it run once).
"You keep using that word. I do not think it means what you think it means."
A "hot signal" is a signal that sends values (and presumably does work) regardless of whether it has any subscribers. A "cold signal" is a signal that defers its work and the sending of any values until it has a subscriber. And a cold signal will perform its work and send values for each subscriber.
If you want to make a cold signal run only once but have multiple subscribers, you need to multicast the signal. Multicasting is a pretty simple concept, that works like this:
Create a RACSubject to proxy the values sent by the signal you want to execute once.
Subscribe to the subject as many times as needed.
Create a single subscription to the signal you want to execute only once, and for every value sent by the signal, send it to the subject with [subject sendNext:value].
However, you can and should use RACMulticastConnection to do all of the above with less code:
RACMulticastConnection *connection = [signal publish];
[connection.signal subscribe:subscriberA];
[connection.signal subscribe:subscriberB];
[connection.signal subscribe:subscriberC];
[connection connect]; // This will cause the original signal to execute once.
// But each of subscriberA, subscriberB, and subscriberC
// will be sent the values from `signal`.
If you do not care about the output of the signal (and for some reason you really want play to be a signal), you may want to make a command. A command causes a signal to be executed via some sort of event (such as a ui button press or other event). Simply create the Signal, add it to a command, then when you need to run it, execute it.
#weakify(self);
RACCommand * command = [[RACCommand alloc] initWithSignalBlock:^(id input) {
#strongify(self);
return [self play:animation];
}];
//This causes the signal to be ran
[command execute:nil];
//Or you could assign the command to a button so it is executed
// when the button is pressed
playButton.rac_command = command;

Using ReactiveCocoa to track UI updates with a remote object

I'm making an iOS app which lets you remotely control music in an app playing on your desktop.
One of the hardest problems is being able to update the position of the "tracker" (which shows the time position and duration of the currently playing song) correctly. There are several sources of input here:
At launch, the remote sends a network request to get the initial position and duration of the currently playing song.
When the user adjusts the position of the tracker using the remote, it sends a network request to the music app to change the position of the song.
If the user uses the app on the desktop to change the position of the tracker, the app sends a network request to the remote with the new position of the tracker.
If the song is currently playing, the position of the tracker is updated every 0.5 seconds or so.
At the moment, the tracker is a UISlider which is backed by a "Player" model. Whenever the user changes the position on the slider, it updates the model and sends a network request, like so:
In NowPlayingViewController.m
[[slider rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UISlider *x) {
[playerModel seekToPosition:x.value];
}];
[RACObserve(playerModel, position) subscribeNext:^(id x) {
slider.value = player.position;
}];
In PlayerModel.m:
#property (nonatomic) NSTimeInterval position;
- (void)seekToPosition:(NSTimeInterval)position
{
self.position = position;
[self.client newRequestWithMethod:#"seekTo" params:#[positionArg] callback:NULL];
}
- (void)receivedPlayerUpdate:(NSDictionary *)json
{
self.position = [json objectForKey:#"position"]
}
The problem is when a user "fiddles" with the slider, and queues up a number of network requests which all come back at different times. The user could be have moved the slider again when a response is received, moving the slider back to a previous value.
My question: How do I use ReactiveCocoa correctly in this example, ensuring that updates from the network are dealt with, but only if the user hasn't moved the slider since?
In your GitHub thread about this you say that you want to consider the remote's updates as canonical. That's good, because (as Josh Abernathy suggested there), RAC or not, you need to pick one of the two sources to take priority (or you need timestamps, but then you need a reference clock...).
Given your code and disregarding RAC, the solution is just setting a flag in seekToPosition: and unsetting it using a timer. Check the flag in recievedPlayerUpdate:, ignoring the update if it's set.
By the way, you should use the RAC() macro to bind your slider's value, rather than the subscribeNext: that you've got:
RAC(slider, value) = RACObserve(playerModel, position);
You can definitely construct a signal chain to do what you want, though. You've got four signals you need to combine.
For the last item, the periodic update, you can use interval:onScheduler::
[[RACSignal interval:kPositionFetchSeconds
onScheduler:[RACScheduler scheduler]] map:^(id _){
return /* Request position over network */;
}];
The map: just ignores the date that the interval:... signal produces, and fetches the position. Since your requests and messages from the desktop have equal priority, merge: those together:
[RACSignal merge:#[desktopPositionSignal, timedRequestSignal]];
You decided that you don't want either of those signals going through if the user has touched the slider, though. This can be accomplished in one of two ways. Using the flag I suggested, you could filter: that merged signal:
[mergedSignal filter:^BOOL (id _){ return userFiddlingWithSlider; }];
Better than that -- avoiding extra state -- would be to build an operation out of a combination of throttle: and sample: that passes a value from a signal at a certain interval after another signal has not sent anything:
[mergedSignal sample:
[sliderSignal throttle:kUserFiddlingWithSliderInterval]];
(And you might, of course, want to throttle/sample the interval:onScheduler: signal in the same way -- before the merge -- in order to avoid unncessary network requests.)
You can put this all together in PlayerModel, binding it to position. You'll just need to give the PlayerModel the slider's rac_signalForControlEvents:, and then merge in the slider value. Since you're using the same signal multiple places in one chain, I believe that you want to "multicast" it.
Finally, use startWith: to get your first item above, the inital position from the desktop app, into the stream.
RAC(self, position) =
[[RACSignal merge:#[sampledSignal,
[sliderSignal map:^id(UISlider * slider){
return [slider value];
}]]
] startWith:/* Request position over network */];
The decision to break each signal out into its own variable or string them all together Lisp-style I'll leave to you.
Incidentally, I've found it helpful to actually draw out the signal chains when working on problems like this. I made a quick diagram for your scenario. It helps with thinking of the signals as entities in their own right, as opposed to worrying about the values that they carry.

What is the equivalent for flash/actionscirpt based addEventListener, removeEventListener and dispatchEvent in IOS

AS some one porting code from actionscript to IOS, We have a lot of custom components that follow the event dispatching mechanism in Flash/Actionscript:
E.g. dispatcher:
dispatchEvent(new CustomEvent(CustomEvent.DRAG_DROP));
Consumer:
dispatcher.addEventListener(CustomEvent.DRAG_DROP, actionHandler);
private function actionHandler(event:CustomEvent):void {
trace("actionHandler: " + event);
}
I know of NSNotificationCenter, KVO pattern, action-target, but none seem to be an exact match?
Where would I define CustomEvent? CustomEvent.DRAG_DROP? and how would the consumer listen for the event? How would a consumer know of all the events that a dispatcher can dispatch? I do not wish to use a delegate because there could be multiple consumers.
The closes way I know is selectors ...
// store event handler
SEL targetHandler;
// firing an event
[targetHandler performSelector:targetHandler withObject:eventObj];
// event handler in the listening class
- (void) onStuffHappened: (Event*) event
{
}
that's obviously a quick thought, I would extend NSObject and store handlers in NSMutableArray then run performSelector on all the stored handlers ... something like that
or you can use delegates for a cleaner way.
Generally this is done with a list of delegates. If you want a multiple consumers, define a protocol (just like you would for a delegate), and then create an array of those objects. When you want to communicate with all of the listeners iterate through the list of listeners sending the event to each one.

Resources