I'm currently re-writing a form controller for iOS. It's a custom object that is bound to a model, and handles editing form fields, jumping to the prev/next field, handling custom keyboards, validating data...
The first version was based on a plist for storing the form values, the form controller held all the data itself. Now I want to dissociate the storage (model) from the form controller, thus I've settled with using KVO.
For simplicity's sake, let's assume I've got a form designed to edit a time span for an absence. So it's got two fields: leaveDate
and returnDate
.
My model is as follows:
@interface Absence
@property (strong, nonatomic) NSDate *leaveDate;
@property (strong, nonatomic) NSDate *returnDate;
@property (readonly, nonatomic) BOOL isValid;
@end
My form controller has a property model
which points to this object.
When the user taps on the "leave date" text field in my XIB, the form controller hands in and presents a date picker based on the current value of my model’s leaveDate
. When the user picks some other date, the form controller updates its model by using setValue:forKey:
.
The isValid
property is declared as being impacted by leaveDate
and returnDate
(using +keyPathsForValuesAffectingIsValid
), and the form controller has registered for watching a change in this property, to enable/disable the submit button on the fly.
Up to this point, everything works like a charm. Now, for the twisted part:
I want my form controller to be able to handle changes in the model while it's open. Example: I've got a rule in the model that says "an absence must least at last 3 days". When the users changes the leave date, the return date is automatically adjusted if the total duration does not exceed 3 days.
So my form controller must also register for listening to changes in all properties. The problem is that it both changes the properties, and listens to changes.
That way, when the user changes leaveTime
, the form controller uses setValue:forKey:
to update the model, but instantly receives a KVO notification for this very change it has just made. This is unnecessary and potentially harmful (I just made the change myself, I don't need to be told I've just done it).
The only way around I found till now is un-registering just before setting the new value, then re-registering right after, like this:
[self.model removeObserver:self forKeyPath:self.currentField.key];
[self.model setValue:newValue forKey:self.currentField.key];
[self.model addObserver:self forKeyPath:self.currentField.key options:NSKeyValueObservingOptionNew context:nil];
It's working, but it's ugly, and performance-wise I doubt it's great.
Does somebody have an explanation as to how to do it better?
ControllerA
is a registered KVO observer of Model
.
ControllerB
updates Model
==> ControllerA
receives a KVO notification. That's fine.
ControllerA
updates Model
==> ControllerA
receives a KVO notification. I don't want this one.
You seem concerned about performance. I wouldn't be. Drawing is coalesced by the main run loop, so setting textField.text = @"foo";
should NOT be causing drawing, image processing, etc. to happen in-line. Typically, a setter like that will set its value and then call [self setNeedsDisplay]
which just sets a flag (very cheap), and then later, at the end of the run loop, the drawing system will trigger a single redraw. You could set textField.text
a thousand times, and there should still only be one draw operation.
As commenters have suggested, you should make it so your controllers are tolerant of multiple updates. If you're doing a bunch of work in-line with a setter, don't. Setters should be "dumb." They should set the value, and set a flag if necessary (like setNeedsDisplay
). In situations like this, you should avoid doing "real work" in a setter.
As another commenter suggested, you could also just not bother updating the UI in-line, and let KVO ripple out the change to all the observers, including the controller that caused the change.
Really, any of these approaches would work, but I suspect that your performance concerns are unfounded. If there is a performance problem, the problem isn't that there are multiple updates, but that you're doing real work during each update, when you should be setting a flag and doing the work later.