I am trying to change the label on a button up on the selector being called.
It appears that the code is duplicated. Is there a way perhaps it's not obvious to me right now to have the signal switch after the map ? or no ?
[[[pressedStart map:^id(id value) {
UIButton* button = value;
BOOL transform = [button.titleLabel.text isEqualToString:#"Start"];
return [NSNumber numberWithBool:transform];
}] filter:^BOOL(id value) {
return [value boolValue];
}] subscribeNext:^(id x) {
self.start.titleLabel.text = #"Stop";
}];
[[[pressedStart map:^id(id value) {
UIButton* button = value;
BOOL transform = [button.titleLabel.text isEqualToString:#"Stop"];
return [NSNumber numberWithBool:transform];
}] filter:^BOOL(id value) {
return [value boolValue];
}] subscribeNext:^(id x) {
self.start.titleLabel.text = #"Start";
}];
First of all, in order to change the button's title, you have to call its setTitle:forState: method.
Also please note that using self inside the subscribeNext block is likely to create a retain cycle (and therefore a memory leak). You can read more about it in this answer. You can use #weakify / #strongify macros or, as mentioned in that answer, use rac_liftSelectors:withSignals: method (which IMHO seems to be cleaner).
Your code can be simplified as you actually don't need to split the signal at all. You can use a simple condition inside the map block and return the value which should be the button's title after it was pressed. This value will be sent as a next value of the resulting signal. You can also use startWith: operator to set the initial value (I guess it should be "Start").
RACSignal *buttonTextSignal = [[pressedStart map:^id(UIButton *buttonPressed) {
return [buttonPressed.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}]
startWith:#"Start"];
[self.start rac_liftSelector:#selector(setTitle:forState:) withSignals:buttonTextSignal, [RACSignal return:#(UIControlStateNormal)], nil];
What does rac_liftSelector:withSignals: do? Each time one of the signals sends its next value, it invokes the method identified by the selector (in this case setTitle:forState:). The method is invoked with next values of the signals as its parameters. So in our case it will initially call:
[self.startButton setTitle:#"Start" forState:UIControlStateNormal];
If you wanted to set a single property (let's say titleLabel.text), you could bind it with RAC macro:
RAC(self.startButton, titleLabel.text) = buttonTextSignal;
Unfortunately, it only works for setting properties, and in your case you have to call a method with two arguments, that's why you have to use rac_liftSelector:withSignals.
As I said, you could achieve the desired result using subscribeNext:
#weakify(self);
RACSignal *buttonTextSignal = [[[pressedStart map:^id(UIButton *buttonPressed) {
return [buttonPressed.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}]
startWith:#"Start"]
subscribeNext:^(NSString *title) {
#strongify(self);
[self.startButton setTitle:title forState:UIControlStateNormal];
}];
But as you can see, you should take extra care to avoid a retain cycle, using #weakify and #strongify macros.
Related
I'm creating simple contact application trying to learn ReactiveCocoa and MVVM.
I store array of cell's ViewModels in my tableView's ViewModel. When user enters into tableView's editing mode, some properties of some cell's ViewModel can be changed as user changes cell text. I want to observe these changes in order to enable/disable Done button and accordingly enable/disable signal for saving the data to the model.
How can I observe these changes in the tableViews view model?
Here is a snippet of code I tried to use:
-(RACSignal *)executeCheckChange {
return [RACObserve(self, cellViewModels)
map:^id(NSArray *viewModels) {
for (id viewModel in viewModels) {
if([viewModel isKindOfClass:[STContactDetailsPhoneCellViewModel class]])
{
STContactDetailsPhoneCellViewModel *phoneViewModel = (STContactDetailsPhoneCellViewModel *)viewModel;
if([phoneViewModel isChanged])
return #(YES);
}
}
return #(NO);
}];
}
But this RACObserve is only invoked if the array itself is changed, but not the element of array.
In my particular case I was able to solve the problem this way:
-(RACSignal *)executeChangeCheck {
#weakify(self);
return [[RACObserve(self, cellViewModels)
map:^(NSArray *viewModels) {
RACSequence *selectionSignals = [[viewModels.rac_sequence
filter:^BOOL(id value) {
return [value isKindOfClass:[STContactDetailsPhoneCellViewModel class]];
}]
map:^(STContactDetailsPhoneCellViewModel *viewModel) {
#strongify(self);
return [RACObserve(viewModel, editPhone)
map:^id(NSString *editPhone) {
return #(![editPhone isEqualToString:viewModel.phone]);
}];
}];
return [[RACSignal
combineLatest:selectionSignals]
or];
}]
switchToLatest];
}
All in all, every time my array changes, I create set of observations on each of ViewModels, filter them in such a way that I observe only these that I'm interested, compare values from observations to the original value and ensure that only newest signal takes effect.
To observe the changes to the properties of a class you need to add observer to that property using the key value observing functionality.
I'm facing the following problem and I already tried a lot. I have also read the others Questions in Stackoverflow like:
Objective-C: Calling selectors with multiple arguments
and the Cocoa Core Competencies about Selectors, but I'm searching for the best way to pass a variable of arguments to a selector.
-(void) runAllStatusDelegates : (SEL)selector
{
for (NSValue *val in self.statusDelegates)
{
id<StatusDelegate> delegate = val;
if ([delegate respondsToSelector:selector])
{
[delegate performSelector:selector];
}
}
}
This method is responsible to call the methods inside the delegates. The parameter is a Selector. My Problem is that the selector can have 0 - 3 arguments, as shown below.
-(void) handleBluetoothEnabled:(BOOL)aEnabled
{
if (aEnabled)
{
[self.statusDelegate bluetoothEnabled];
if (_storedPenSerialNumber != nil && ![_storedSerialNumber isEqual:kUnknownPenID])
{
[self runAllStatusDelegates: #selector(penConnected : _storedSerialNumber : _storedFirmware:)];
}
}
else
{
[self.statusDelegate bluetoothDisabled];
}
}
-(void) handleChooseDevice:(BluetoothDeviceList*)aDevices
{
NSLog(#"Handle Choose Device");
[self runAllStatusDelegates: #selector(chooseDevice:aDevices:)];
}
-(void) handleDiscoveryStarted
{
NSLog(#"Discovery Started");
[self runAllStatusDelegates: #selector(searchingForBluetoothDevice)];
[self.statusDelegate handleStatus:#"Searching for your digipen"];
}
This implementation isn't working because the performSelector is not recognizing the selector.
I also tried to implement it with #selector(penConnected::) withObject:_storedSerialNumber but then I have to implement another method with additional arguments as well and I don't want that.
I'm new to objective-c so I'm not so familiar with all possibilities.
My idea is to pass a String and an Array of arguments to runAllStatusDelegates and build up the selector inside that method, but is this the best way or are there more convenient ways?
I am personally not a fan of NSInvocation for complex signatures. Its really great for enqueueing a simple function call on a queue and running it when you need it but for your case, you know the selector so you don't really need to go the invocation route. I typically find invocations are more useful if you don't actually know the selector you want to call at compile time, maybe its determined by your API etc.
So what I would do is simply pass a block into your runAllStatusDelegates method that will execute against all your delegates:
- (void)performSelector:(SEL)selector againstAllDelegatesWithExecutionBlock:(void (^)(id<StatusDelegate>))blockToExecute
{
for (id<StatusDelegate> delegate in self.statusDelegates)
{
if ([delegate respondsToSelector:selector])
{
blockToExecute(delegate);
}
}
}
Then when you want to call your delegates with a function it looks like this:
[self performSelector:#selector(handleAnswerOfLifeFound)
againstAllDelegatesWithExecutionBlock:^(id<StatusDelegate> delegate){
[delegate handleAnswerOfLifeFound];
}];
I guess the only downside might be that you could change the selector and pass a different function into the block. How I would solve this is by actually making sure not all methods are optional, or if they are optional to make the actual check inside the block, this would clean up the signature:
- (void)callAllDelegatesWithBlock:(void (^)(id<StatusDelegate>))blockToExecute
{
for (id<StatusDelegate> delegate in self.statusDelegates)
{
blockToExecute(delegate);
}
}
and then your actual usage for an optional method:
[self callAllDelegatesWithBlock^(id<StatusDelegate> delegate){
if([delegate respondsToSelector:#selector(handleAnswerOfLifeFound)]){
[delegate handleAnswerOfLifeFound];
}
}];
Still error-prone but at least a bit tidier.
You can use NSInvocation for this case
SEL theSelector = #selector(yourSelector:);
NSMethodSignature *aSignature = [NSMethodSignature instanceMethodSignatureForSelector:theSelector];
NSInvocation *anInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
[anInvocation setSelector:theSelector];
[anInvocation setTarget:self];
[anInvocation setArgument:&arg1 atIndex:2];
[anInvocation setArgument:&arg2 atIndex:3];
[anInvocation setArgument:&arg3 atIndex:4];
[anInvocation setArgument:&arg4 atIndex:5];
//Add more
Note that the arguments at index 0 and 1 are reserved for target and selector.
For more info http://www.cocoawithlove.com/2008/03/construct-nsinvocation-for-any-message.html
you can binding the arguments to the selector
NSDictionary *argInfo=#{#"arg1":arg1,#"arg2":arg2,...};
objc_setAssociatedObject(self,#selector(chooseDevice:aDevices:),argInfo,OBJC_ASSOCIATION_COPY)
[self runAllStatusDelegates: #selector(chooseDevice:aDevices:)];
then in the
-(void) runAllStatusDelegates : (SEL)selector
{
for (NSValue *val in self.statusDelegates)
{
id<StatusDelegate> delegate = val;
if ([delegate respondsToSelector:selector])
{
NSDictionary *argInfo=objc_getAssociatedObject(self, selector);
//call the fun use arginfo
}
}
}
I'm trying to disable a UIBarButtonItem in my iOS app when a condition has been met.
So in my viewModel I created this signal:
-(RACSignal *)thresholdLimitReachedSignal
{
#weakify(self);
return [RACObserve(self, thresholdLimitReached) filter:^BOOL(id value) {
#strongify(self);
return self.thresholdLimitReached;
}];
}
Then in my viewController I have this:
self.requestNewPinButton.rac_command = [[RACCommand alloc]initWithEnabled:self.viewModel.thresholdLimitReachedSignal
signalBlock:^RACSignal *(id input) {
[self.viewModel.requestNewPinSignal subscribeNext:^(id x) {
//do some stuff here
}];
return [RACSignal empty];
}];
So the UIBarButtonItem is triggered and fires off a requestNewPinSignal which works just fine. Then I flag thresholdLimitReached which causes the thresholdLimitReachedSignal to fire - all good. However the button just does not get disabled and I am not sure why? No matter if I manually set the boolean to true or false inside the thresholdLimitReachedSignal method - button remains enabled!
If I manually subscribe to thresholdLimitReachedSignal
like so:
[self.viewModel.thresholdLimitReachedSignal subscribeNext:^(id x) {
self.requestNewPinButton.enabled = NO;
}];
Then button gets disabled no problem. I'd like to have this signal combined with the requestSignal some how - I thought initWithEnabled:signalBlock did this?
[RACObserve(self, thresholdLimitReached) filter:^BOOL(id value) {
#strongify(self);
return self.thresholdLimitReached;
}];
You're filtering thresholdLimitReachedSignal so that it only ever returns YES, so your button is always going to be enabled. For starters, you could rewrite that like this and avoid the #weakify/#strongify:
[RACObserve(self, thresholdLimitReached) filter:^BOOL(NSNumber *thresholdLimitReached) {
return thresholdLimitReached.boolValue;
}];
But don't do that: if you're using this as the enabled signal, it needs to be a signal of booleans that sends YES when it should be enabled and NO when it should be disabled.
Assuming that your want the button to be disabled when the threshold has been reached, you want something like this:
[[RACCommand alloc] initWithEnabled:[RACObserve(self.viewModel, thresholdLimitReached) not]
signalBlock:...];
In the snipped bellow, I want to change the enableness status of self.btnSave, that has a RACCommand defined.
The second block actually tries to change it based on another external condition (tableview selection).
It seams that the rac_command takes control of the enabled status of the button, and the only way to make it work, would be creating a signal to control the enable status (that I have no idea how to do it)
Do you have any idea?
I would like to keep using RAC for the button actions, but if it gets more complicated than I imagined, I will have to abandon it.
Thanks in advance.
#weakify(self);
self.btnSave.rac_command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
#strongify(self);
Album* lalbum = [NSEntityDescription
insertNewObjectForEntityForName:#"Album"
inManagedObjectContext:self.production.managedObjectContext];
lalbum.name = self.txtAlbumName.text;
[self.production addAlbunsObject: lalbum];
[self.production.managedObjectContext save: nil];
self.close();
return [RACSignal empty];
}];
[[self.txtAlbumName rac_textSignal] subscribeNext:^(NSString* text) {
#strongify(self);
if ([text isEqualToString:#""])
{
self.btnSave.enabled = NO;
} else
self.btnSave.enabled = YES;
}];
The point of RACCommand is that it lets you disable the button while you perform some long-running action. If you don't want that (and your question implies that you don't), just use normal target/action semantics. There are other libraries that give you nice block-based helpers for UIBarButtonItems, if that's what you're after.
Also, try this on for size:
RAC(self.btnSave, enabled) = [self.txtAlbumName.rac_textSignal map:^(NSString *text) {
return ![text isEqualToString:#""];
}];
In my application I have a signal which triggers some asynchronous network activity via flattenMap. I want to display a loading indicator while the network activity is in progress.
My current solution works just fine:
[[[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
// show the loading indicator as a side-effect
self.loadingIndicator.hidden = NO;
}]
flattenMap:^id(id x) {
return [self doSomethingAsync];
}]
subscribeNext:^(NSNumber *result) {
// hide the indicator again
self.loadingIndicator.hidden = YES;
// do something with the results
}];
This works, however I would like to change the above code so that the hidden property of the loading indicator can be set via a signal.
Is this possible?
Elsewhere in my app I have more complex requirements where the visibility of an element depends on a few different 'events', being able to compose these via signals would be much better.
RACCommand is tailor-built for exactly this use case, and will usually result in dramatically simpler code than the alternatives:
#weakify(self);
RACCommand *signInCommand = [[RACCommand alloc] initWithSignalBlock:^(id _) {
#strongify(self);
return [self doSomethingAsync];
}];
self.signInButton.rac_command = signInCommand;
// Show the loading indicator while signing in.
RAC(self.loadingIndicator, hidden) = [signInCommand.executing not];
It looks like your signal is: when signInButtonSignal or resultSignal send a value, invert the last value of hidden. That's easy enough.
[[[hiddenSig replayLast] not] sample:[RACSignal merge:#[signInButtonSignal, resultSignal]];
I'm using exactly that construct for a situation similar to yours. It might be nice to wrap it up into an operator:
- (RACSignal *)toggle:(RACSignal *)toggler
{
return [[[self replayLast] not] sample:toggler];
}
Then you have just
[hiddenSig toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]];
Another possibility might be a class method, tying the state to a mapping Block:
+ (RACSignal *)toggle:(RACSignal *)toggler initially:(BOOL)initial
{
__block BOOL currVal = initial;
return [[toggler map:^id (id _) {
currVal = !currVal;
return #(currVal);
}] startWith:#(initial)];
}
and then
[RACSignal toggle:[RACSignal merge:#[signInButtonSignal, resultSignal]]
initially:NO];
The answer from Josh helped quite a bit, but in the end I found a simpler solution. Simply breaking the pipeline into two signals, one for the button press, the other for the subsequent asynchronous activity. I then merged the two to give a signal which I used to bind to the loadingIndicator's hidden property:
// a signal that triggers sign-in
RACSignal *signInStartSignal = [self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside];
// a signal that emits the sign in result
RACSignal *signInResultSignal =
[signInStartSignal
flattenMap:^id(id x) {
return [self doSomethingAsync];
}];
[signInResultSignal
subscribeNext:^(NSNumber *result) {
// do something based on the result
}];
// merge the two signals
RACSignal *signInInProgress =
[[RACSignal merge:#[signInResultSignal, signInStartSignal]]
map:^id(id value) {
// if the signal value is a UIButton, the signal that
// just fired was the signInStartSignal
return #(![[value class] isSubclassOfClass:[UIButton class]]);
}];
RAC(self.signInFailureText,hidden) = signInInProgress;