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?
TL;DR
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.
Related
I'm trying to make a NSTouchBar in an SDL application and I need to attach a responder to the NSWindow object (that's the only access SDL gives into the Cocoa windowing system).
https://developer.apple.com/reference/appkit/nstouchbar
If you explicitly adopt the NSTouchBarProvider protocol in an object,
you must also explicitly send the associated key-value observing
notifications within NSTouchBar methods; this lets the system respond
appropriately to changes in the bar.
What does that mean and how do I do it? I see lots of documentation about how to subscribe to the notifications, but not how to send them?
Right now I have:
#interface MyTouchBarResponder : NSResponder <NSTouchBarDelegate>
- (id)init;
- (NSTouchBar *)makeTouchBar;
- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier;
#property(strong, readonly) NSTouchBar *touchBar;
#end
and I'm attaching it to the window with the code from a previous question I asked here: How to create an NSTouchBar from an NSWindow object?
touchBarResponder.nextResponder = window.nextResponder;
window.nextResponder = touchBarResponder;
but my callbacks aren't ever being called (I put exit(0) in them to make it very obvious). When I hack the code directly into the SDL library, things work as expected, but that's not a viable permanent solution.
Thank you.
First, your custom responder should conform to NSTouchBarProvider (in the above, you declare the touchBar property, but not the explicit conformance)
Second, you want to make sure that your custom responder is in the responder chain of the window (whether the first responder or just later in the chain). After adjusting the responder chain with your above code, you want to call -makeFirstResponder: and pass in some view in the window (if you need that view to be first responder) or with the custom responder object. You should then verify that the window's firstResponder is that object.
With these in place, you should get at least one call to touchBar after the window is shown and made key.
To answer the question on key-value observing notifications, that is needed for when you want to change the actual NSTouchBar object being returned from touchBar. In the general case this isn't necessary, since it's unnecessary in the static touch bar case, and even in the dynamic case, you can rely on just setting the defaultItemIdentifiers on the previously created touch bar and it will update. However, should you need to change the touch bar object, you need to ensure that -willChangeValueForKey: and -didChangeValueForKey: are sent for touchBar when you change the return value. This developer documentation on KVO goes into much more detail.
I have an NSDocument app and I would like to have a NSTextField to commit the current changes to the model every time the user save (through cmd+s for example).
I don't use binding and at the moment the changes are pushed to the model in the -controlTextDidEndEditing: method. Calling the [window makeFirstResponder:nil] does push the changes to the model but also causes the control to lose focus which is not really a reasonable behaviour.
Googling around I have seen that several people suggested to use the -commitEditing method but it only applies to bindings, am I wrong?
You can just call your already defined controlTextDidEndEditing: from your save action:
-(IBAction)save:(id)sender
{
[self controlTextDidEndEditing: ...]
}
to trigger the same code you already wrote!
Since UIBarButtonItem doesn't subclass UIView, it's impossible to get at the normal characteristics like its frame.
One way to do this is [barButtonItem valueForKey:#"view"]
This works perfectly, and allows you to add a GestureRecognizer (for instance) to the underlying UIView.
However, is this a private UIKit API violation?
This is not private in terms of immediate rejection upon validation, but it's private enough to be considered fragile (that is, new iOS version can break your existing app in the app store that's using the code).
I can say, that a similar code (fetching backgroundView ivar of UIToolbar via KVC) has passed app store validation and is being used in production.
In case of possible bad things, you must wrap the method in #try { ... } #catch, so that you intercept KVC possibly failing in newer iOS release.
Five Pieces of Evidence for "It's Not Private"
It's a property that you can get to in other ways. Try this and one of those views is, in fact, the _view ivar of the UIBarButtonItem in question. This indicates that access to this UIView is not prohibited itself, though the KVO way in might be questionable (but I doubt it).
NSArray *array = self.toolBar.subviews;
for (UIView *view in array) {
view.backgroundColor = UIColor.greenColor;
}
They actually trigger the KVO for this property. ivars do not have to trigger the KVO API, right?
#Farcaller mentions a similar case which is for sale in the App Store. Since he/she answered within the first 20 minutes of the question being up there, it's reasonable (but not safe!) to assume that there might be thousands of apps in the App Store that do this.
This UIView gets subbed out each time the button is pressed, so you cannot just, for example, set a gesture recognizer on it and be done. You can, however, keep setting the same gesture recognizer every time the view gets replaced. To me, this is actually more evidence that it's not a private API thing, but rather you have to be very careful when using it (and use KVO to make sure you have the latest one).
My app is for sale in the App Store and does this.
I'm developing simple MVC app in Cocoa/Objective-C. I have a strange issue (or misunderstanding) with notifications and KVO.
I have AppController object in MainMenu.xib, hence I implement awakeFromNib method where I register for NSImageView changing its image property. I add self as an observer in the following way:
// options:3 equals to new/old passed values in changeDictionary
[backgroundImageView addObserver:self
forKeyPath:#"image"
options:3
context:NULL];
The backgroundImageView is an IBOutlet in AppController connected to NSImageView.
In standard observeValueForKeyPath:ofObject:change:context method I just log the received notification.
Problem is - when i change the image value of NSImageView I get 3 notifications instead of one. Can you help me with this? Maybe I'm overlooking something in options or in generally registering observer?
UPDATE: backgroundImageView is the instance of BackgroundImageView class which is sublcass of NSImageView. I subclassed the latter one for handling drag and drop operations as drag destination. When performDragOperation: is called (the last 'state' of the dragging) it changes the value for image property with setImage between willChangeValueForKey and didChangeValueForKey.
… it changes the value for image property with setImage between willChangeValueForKey and didChangeValueForKey.
When you send an accessor message, you get KVO notifications for free with it. You should remove the {will,did}ChangeValueForKey: messages, because they're the cause of at least one of the extraneous change notifications.
Is your AppController the File's Owner of two other nibs? If so, it'll receive an awakeFromNib message for each of those, too. MainMenu plus two makes three awakeFromNib messages, which means you'll add yourself as an observer three times.
There does not seem to be any obvious problem with setting of the observer.
Have a look at how you update the image that you observe, maybe it's being modified 3 times?
I have a MainViewController in my Cocoa Touch app which shows a status view containing a UIProgressBar view.
From the MainViewController, FlickrImage model objects are created and iterated over. the FlickrImage objects themselves interact with the Flickr API, which takes time which is why I want the user see the progress bar. The challenge (at least to me it is ;) now is to interact with the UIProgressBar (send progress messages) from the FlickrImage objects. Do I do this using a NSNotificationCenter or can I just declare the MainViewController as a forward class in FlickrImage and interact with the UIProgressBar directly?
The best way is to not have the model call anything at all, but use KVO to observe the model, and then react to the message you get when the model updates. This is because the point of having a separate model layer is that the model shouldn't have to know anything about how data is presented to the user.
So create a class that keeps track of the loading of all the images (most likely you already have such a class) and do something like:
[imageManager addObserver:self forKeyPath:#"progress" options:nil context:nil];
Make sure that the data manager class has a declared property called progress (ideally with values ranging from 0.0 to 1.0) and when you update the value in the manager, you must use the dot notation: self.progress = newVal;
This is much cleaner than sending notifications from the model class. An alternative would be to register the view as a delegate on the manager. There is no clear-cut rule of thumb for when you should use delegates and when KVO is better, although there might be slightly less overhead in using a delegate protocol. Apple uses both approaches but there is a tendency to rely more on KVO, which in my opinion is a good thing.
I prefer NSNotificationCenter, MainViewController register as observe and update the UIProgressBar.
keeping the object MainViewController in FlickrImage and updating UIProgressBar from FlickrImage which make handling UI from model(you are violating MVC)