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.
Related
In my app I have the Restaurant class that you can see below. I'd like to attach a KVOController to it. But I'm having no luck. When I attach it with the code below, it crashes.
FBKVOController *KVOController = [FBKVOController controllerWithObserver:self];
self.KVOController = KVOController;
[self.KVOController observe:self keyPath:#"[Restaurant current].name.asString" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
DDLogDebug(#"Restaurant changed");
}];
What's the best way to add KVO to a class like this?
#implementation Restaurant
static Restaurant *current = nil;
+ (Restaurant *)current {
#synchronized(self) {
if (current == nil) {
current = [[Restaurant alloc] initWithId:0];
}
}
return current;
}
- (id)initWithId:(NSInteger)number {
self = [super init];
if (self)
{
...
}
return self;
}
#end
The problem is not #synchronized. There are several issues with your code:
Do you want to observe when the current restaurant changes? Or when the current restaurant's name changes (without +[Restaurant current] pointing to a different restaurant instance). Or any kind of name change, whether triggered by a change of current or a change of name?
Depending on the answer, you'll either want to observe observe:[Restaurant class] or observe:[Restaurant instance], but definitely not observe:self (unless you're setting this up inside the Restaurant class implementation, in which case [self class] would be an alternative to [Restaurant class]).
For any change to be observable, you must ensure that the class is implemented in a KVO-compliant way. This goes both for changes to +[Restaurant current] as well as for changes to -[Restaurant name], depending on what you want to be able to observe.
[Restaurant current].name.asString is not a valid key path. Valid key paths may only contain property names (ASCII, begin with a lowercase letter, no whitespace) and dots to separate them (see Key-value coding for details). Once you're telling the KVOController to observe:[Restaurant class], all that remains for the key path is current.name.asString.
What is name if not a string? Do you really need to convert it to a string for observing it? If your intention is to watch for name changes, observing current.name is probably sufficient.
You'll likely end up with one of the following two options:
FBKVOController *kvoController = [FBKVOController controllerWithObserver:self];
[kvoController observe:[Restaurant class] keyPath:#"current.name" ...];`
// or
[kvoController observe:[Restaurant current] keyPath:#"name" ...];`
And again, for any changes to be observable, they need to be KVO-compliant.
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.
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;
I'm working on integrating RAC into my project with the goal of creating a ViewModel layer that will allow easy caching/prefetching from the network (plus all of the other benefits of MVVM). I'm not especially familiar with MVVM or FRP yet, and I'm trying to develop a nice, reusable pattern for iOS development. I have a couple of questions about this.
First, this is sort of how I've added a ViewModel to one of my views, just to try it out. (I want this here to reference later).
In ViewController viewDidLoad:
#weakify(self)
//Setup signals
RAC(self.navigationItem.title) = self.viewModel.nameSignal;
RAC(self.specialtyLabel.text) = self.viewModel.specialtySignal;
RAC(self.bioButton.hidden) = self.viewModel.hiddenBioSignal;
RAC(self.bioTextView.text) = self.viewModel.bioSignal;
RAC(self.profileImageView.hidden) = self.viewModel.hiddenProfileImageSignal;
[self.profileImageView rac_liftSelector:#selector(setImageWithContentsOfURL:placeholderImage:) withObjectsFromArray:#[self.viewModel.profileImageSignal, [RACTupleNil tupleNil]]];
[self.viewModel.hasOfficesSignal subscribeNext:^(NSArray *offices) {
self.callActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.directionsActionSheet = [[UIActionSheet alloc] initWithTitle:#"Choose Office" delegate:self cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
self.callActionSheet.delegate = self;
self.directionsActionSheet.delegate = self;
}];
[self.viewModel.officesSignal subscribeNext:^(NSArray *offices){
#strongify(self)
for (LMOffice *office in offices) {
[self.callActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
[self.directionsActionSheet addButtonWithTitle: office.name ? office.name : office.address1];
//add offices to maps
CLLocationCoordinate2D coordinate = {office.latitude.doubleValue, office.longitude.doubleValue};
MKPointAnnotation *point = [[MKPointAnnotation alloc] init];
point.coordinate = coordinate;
[self.mapView addAnnotation:point];
}
//zoom to include all offices
MKMapRect zoomRect = MKMapRectNull;
for (id <MKAnnotation> annotation in self.mapView.annotations)
{
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0.2, 0.2);
zoomRect = MKMapRectUnion(zoomRect, pointRect);
}
[self.mapView setVisibleMapRect:zoomRect animated:YES];
}];
[self.viewModel.openingsSignal subscribeNext:^(NSArray *openings) {
#strongify(self)
if (openings && openings.count > 0) {
[self.openingsTable reloadData];
}
}];
ViewModel.h
#property (nonatomic, strong) LMProvider *doctor;
#property (nonatomic, strong) RACSubject *fetchDoctorSubject;
- (RACSignal *)nameSignal;
- (RACSignal *)specialtySignal;
- (RACSignal *)bioSignal;
- (RACSignal *)profileImageSignal;
- (RACSignal *)openingsSignal;
- (RACSignal *)officesSignal;
- (RACSignal *)hiddenBioSignal;
- (RACSignal *)hiddenProfileImageSignal;
- (RACSignal *)hasOfficesSignal;
ViewModel.m
- (id)init {
self = [super init];
if (self) {
_fetchDoctorSubject = [RACSubject subject];
//fetch doctor details when signalled
#weakify(self)
[self.fetchDoctorSubject subscribeNext:^(id shouldFetch) {
#strongify(self)
if ([shouldFetch boolValue]) {
[self.doctor fetchWithCompletion:^(NSError *error){
if (error) {
//TODO: display error message
NSLog(#"Error fetching single doctor info: %#", error);
}
}];
}
}];
}
return self;
}
- (RACSignal *)nameSignal {
return [RACAbleWithStart(self.doctor.displayName) distinctUntilChanged];
}
- (RACSignal *)specialtySignal {
return [RACAbleWithStart(self.doctor.primarySpecialty.name) distinctUntilChanged];
}
- (RACSignal *)bioSignal {
return [RACAbleWithStart(self.doctor.bio) distinctUntilChanged];
}
- (RACSignal *)profileImageSignal {
return [[[RACAbleWithStart(self.doctor.profilePhotoURL) distinctUntilChanged]
map:^id(NSURL *url){
if (url && ![url.absoluteString hasPrefix:#"https:"]) {
url = [NSURL URLWithString:[NSString stringWithFormat:#"https:%#", url.absoluteString]];
}
return url;
}]
filter:^BOOL(NSURL *url){
return (url != nil && ![url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)openingsSignal {
return [RACAbleWithStart(self.doctor.openings) distinctUntilChanged];
}
- (RACSignal *)officesSignal {
return [RACAbleWithStart(self.doctor.offices) distinctUntilChanged];
}
- (RACSignal *)hiddenBioSignal {
return [[self bioSignal] map:^id(NSString *bioString) {
return #(bioString == nil || [bioString isEqualToString:#""]);
}];
}
- (RACSignal *)hiddenProfileImageSignal {
return [[self profileImageSignal] map:^id(NSURL *url) {
return #(url == nil || [url.absoluteString isEqualToString:#""]);
}];
}
- (RACSignal *)hasOfficesSignal {
return [[self officesSignal] map:^id(NSArray *array) {
return #(array.count > 0);
}];
}
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
My primary question comes with moving concerns that would have been handled by delegates into the ViewModel (hopefully). Delegates are so common in iOS world that I'd like to figure out the best, or even just a moderately workable, solution to this.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Any other comments would be appreciated as well. The goal of this work is to make a prefetching/caching ViewModel layer that can be signalled whenever needed to load data in the background, and thus reduce wait times on the device. If anything reusable comes out of this (other than a pattern) it will of course be open source.
Edit: And another question: It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
The view model should model the view. Which is to say, it shouldn't dictate any view appearance itself, but the logic behind whatever the view appearance is. It shouldn't know anything about the view directly. That's the general guiding principle.
On to some specifics.
It looks like according to the documentation, I should be using properties for all of the signals in my ViewModel instead of methods? I think I should configure them in init? Or should I leave it as-is so that getters return new signals?
Yes, we typically just use properties that mirror their model properties. We'd configure them in -init kinda like:
- (id)init {
self = [super init];
if (self == nil) return nil;
RAC(self.title) = RACAbleWithStart(self.model.title);
return self;
}
Remember that view models are just models for a specific use. Plain old objects with plain old properties.
Am I right in the way I'm using signals? Specifically, does it make sense to have bioSignal to update the data as well as a hiddenBioSignal to directly bind to the hidden property of a textView?
If the bio signal's hiddenness is driven by some specific model logic, it'd make sense to expose it as a property on the view model. But try not to think of it in view terms like hiddenness. Maybe it's more about validness, loading, etc. Something not tied to specifically how it's presented.
For a UITableView, for example, we need to provide both a delegate and a dataSource. Should I have a property on my controller NSUInteger numberOfRowsInTable and bind it to a signal on the ViewModel? And I'm really unclear on how to use RAC to provide my TableView with cells in tableView: cellForRowAtIndexPath:. Do I just need to do these the "traditional" way or is it possible to have some sort of signal provider for the cells? Or maybe it's best to leave it how it is, because a ViewModel shouldn't really be concerned with building the views, just modifying the source of the views?
That last line is exactly right. Your view model should give the view controller the data to display (an array, set, whatever), but your view controller is still the table view's delegate and data source. The view controller creates cells, but the cells are populated by data from the view model. You could even have a cell view model if your cells are relatively complex.
Further, is there a better approach than my use of a subject (fetchDoctorSubject)?
Consider using a RACCommand here instead. It'll give you a nicer way of handling concurrent requests, errors, and thread-safety. Commands are a pretty typical way of communicating from the view to the view model.
Should I have an active property as in the ViewModel example in ReactiveCocoa's github account?
It just depends on whether you need it. On iOS it's probably less commonly needed than OS X, where you could have multiple views and view models allocated but not "active" at once.
Hopefully this has been helpful. It looks like you're heading in the right direction generally!
For a UITableView, for example, we need to provide both a delegate and
a dataSource. Should I have a property on my controller NSUInteger
numberOfRowsInTable and bind it to a signal on the ViewModel?
The standard approach, as described by joshaber above is to manually implement the datasource and delegate within your view controller, with the view model simply exposing an array of items each of which represents a view model which backs a table view cell.
However, this results in a lot of boiler-plate in your otherwise elegant view controller.
I have created a simple binding helper that allows you to bind an NSArray of view models to a table view with just a few lines of code:
// create a cell template
UINib *nib = [UINib nibWithNibName:#"CETweetTableViewCell" bundle:nil];
// bind the ViewModels 'searchResults' property to a table view
[CETableViewBindingHelper bindingHelperForTableView:self.searchResultsTable
sourceSignal:RACObserve(self.viewModel, searchResults)
templateCell:nib];
It also handles selection, executing a command when a row is selected. The complete code is over on my blog. Hope this helps!
End Goal: I have an object-graph made up of leaves, connected to other leaves. I need to traverse the object-graph and return all those leaves that are not wilted and that either 1) don't have sub-leaves or 2) All sub-leaves are wilted.
Situation: I have an NSFetchedResultsController and table view where I'd like to display the results.
What I've Tried:
I started out trying to use an NSPredicate on the NSFetchRequest, but realized there was no way that I could see which could recursively run through a leaf's sub-leaves and all their sub-leaves, etc...
So I added an attribute to the Leaf object called "isFarthestNonWiltedLeaf" and created a custom get-accessor inside of a category on Leaf:
- (NSNumber*) isFarthestNonWiltedLeaf
{
[self willAccessValueForKey:#"isFarthestNonWiltedLeaf"];
NSNumber *returnValue = #([self.wilted boolValue] == NO && [[self allSubLeavesAreWilted] boolValue]);
[self didAccessValueForKey:#"isFarthestNonWiltedLeaf"];
return returnValue;
}
- (NSNumber*) allSubLeavesAreWilted
{
for(Leaf *aLeaf in self.subLeaves)
{
if([aLeaf.wilted boolValue] == NO || ![[aLeaf allSubLeavesAreWilted] boolValue])
return #NO;
}
return #YES;
}
Then, on the NSFetchedResultsController's fetch request, I set the following predicate:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"isFarthestNonWiltedLeaf == YES"]];
This works great the first time that I open the application and started adding leaves and sub-leaves in a different view. However, the next time that I opened the app, the custom accessors method were not accessed the first time that the table view appeared! I have to go to each leaf and check/uncheck its "wilted" status, which then has the NSFetchedResultsController refresh that single Leaf... at that point it does call the custom isFarthestNonWiltedLeaf accessor and the leaf correctly appears in the list. I'd have to do this for each leaf for the entire tableview to be properly updated.
So my question is... how do I get the NSFetchRequest / NSFetchedResultsController to use the custom get accessor of the Leaf object each time? Thank you.
Well, I'm going to have to answer my own question... I played around a bit more and I figured out the issue, though I'm still not sure about the "why" of it.
I realized that there is a lot of caching going on in CoreData, so even though I had the cacheName set to nil on the NSFetchedResultsController, I felt like there was some issue there. Also, it appeared that the primitive attribute was being accessed without calling the the get accessor.
So the primitive needs to be saved, in other words. So I added the following lines in the get accessor right after the value is calculated:
[self setPrimitiveValue:returnValue forKey:#"isFarthestNonWiltedLeaf"];
At first I also added in the KVO notification code (willChangeValueForKey and didChangeValueForKey) but that made the app completely unresponsive, for some reason... it appears to be some cyclical referencing issue.
So the final code looks like this:
- (NSNumber*) isFarthestNonWiltedLeaf
{
[self willAccessValueForKey:#"isFarthestNonWiltedLeaf"];
NSNumber *returnValue = #([self.wilted boolValue] == NO && [[self allSubLeavesAreWilted] boolValue]);
[self didAccessValueForKey:#"isFarthestNonWiltedLeaf"];
[self setPrimitiveValue:returnValue forKey:#"isFarthestNonWiltedLeaf"];
return returnValue;
}
- (NSNumber*) allSubLeavesAreWilted
{
for(Leaf *aLeaf in self.subLeaves)
{
if([aLeaf.wilted boolValue] == NO || ![[aLeaf allSubLeavesAreWilted] boolValue])
return #NO;
}
return #YES;
}
...with the NSPredicate as follows:
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"isFarthestNonWiltedLeaf == YES"]];