Search code examples
cocoamodel-view-controllercocoa-bindingskey-value-observing

Need some tips regarding the Cocoa MVC/KVO patterns


This is a very wide-ranging/vague question, but here goes. Apologies in advance.

The app (desktop app) I'm building takes different kinds of input to generate a QR code (I'm just building it to learn some Obj-C/Cocoa). The user can switch between different views that allow input of plain text (single text field), VCard/MeCard data (multiple text fields), and other stuff. No matter the input, the result is a QR code.

To keep things contained, I'd like to use the views as view-controllers, so they handle they're own inputs, and can simply "send" a generic "data to encode" object containing all the data to a central encoder. I.e. the plain text view would make a data object with its textfield's text, while the VCard/MeCard view would use all of its fields to make structured VCard/MeCard data.

I can bind all of this stuff together manually in code, but I'd really like to learn how bindings/KVO could help me out. Alas, after reading Apple's developer docs, and the simpler tutorials/examples I could find, I'm still not sure how to apply it to my app.

For instance: The user edits the textfields in the VCard-view. The VCard view-controller is notified of each update and "recalculates" the data object. The central encoder controller is then notified of the updated data object, and encodes the data.

The point of all this, is that the input views can be created completely independently, and can contain all kinds of input fields. They then handle their own inputs, and "return" a generic data object, which the encoder can use. Internally, the views observe their inputs to update the data object, and externally the encoder needs only observe the data object.

Trouble is I have no idea how to make this all happen and keep it decoupled. Should there be an object controller between the input-view and its fields? Should there be another one between the view and the encoder? What do I need where? If anyone has a link to a good tutorial, please share.

Again, I can roll my own system of notifications and glue code, but I think the point is to avoid that.


Solution

  • Definitely a vague question, but one beginner to another, I feel your pain :)

    I downloaded and unpacked every single example and grep through them frequently. I've found that to be the most valuable thing to get me over the hump. I definitely recommend not giving up on the examples. I hacked up this script to download and unpack them all.

    In terms of good KVO patterns, I found the technique described here to be very useful. It doesn't work as-is in Objective-C 2.0 however. Also he doesn't give much detail on how it's actually used. Here's what I've got working:

    The KVODispatcher.h like this:

    #import <Foundation/Foundation.h>
    
    @interface KVODispatcher : NSObject {
    
        id owner;
    }
    
    @property (nonatomic, retain) id owner;
    
    - (id) initWithOwner:(id)owner;
    
    - (void)startObserving:(id)object keyPath:(NSString*)keyPath 
                   options:(NSKeyValueObservingOptions)options 
                  selector:(SEL)sel;
    
    - (void)observeValueForKeyPath:(NSString *)keyPath 
                          ofObject:(id)object 
                            change:(NSDictionary *)change 
                           context:(void *)context;
    @end
    

    And the KVODispatcher.m is as so:

    #import "KVODispatcher.h"
    #import <objc/runtime.h>
    
    @implementation KVODispatcher
    
    @synthesize owner;
    
    - (id)initWithOwner:(id)theOwner 
    {
        self = [super init];
        if (self != nil) {
            self.owner = theOwner;
        }
        return self;
    }
    
    - (void)startObserving:(id)object 
                   keyPath:(NSString*)keyPath 
                   options:(NSKeyValueObservingOptions)options 
                  selector:(SEL)sel
    {
        // here is the actual KVO registration
        [object addObserver:self forKeyPath:keyPath options:options context:sel];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath 
                          ofObject:(id)object 
                            change:(NSDictionary *)change 
                           context:(void *)context
    {
        // The event is delegated back to the owner
        // It is assumed the method identified by the selector takes
        // three parameters 'keyPath:object:change:'
        objc_msgSend(owner, (SEL)context, keyPath, object, change);
    
        // As noted, a variation of this technique could be 
        // to expand the data passed in to 'initWithOwner' and 
        // have that data passed to the selected method here.
    }
    @end
    

    Then you can register to observe events like so:

    KVODispatcher* dispatcher = [[KVODispatcher alloc] initWithOwner:self];
    [dispatcher startObserving:theObject 
                       keyPath:@"thePath" 
                       options:NSKeyValueChangeNewKey 
                       selector:@selector(doSomething:object:change:)];
    

    And in the same object that executed the above, you can have a method like so:

    - (void) doSomething:(NSString *)keyPath 
                 object:(id)object 
                 change:(NSDictionary *)change {
    
        // do your thing
    }
    

    You can have as many of these "doSomething" type methods as you like. Just as long as they use the same parameters (keyPath:object:change:) it will work out. With one dispatcher per object that wishes to receive any number of notifications about changes in any number of objects.

    What I like about it:

    1. You can only have one observeValueForKeyPath per class, but you may want to observe several things. Natural next thought is "hey maybe I can pass a selector"
    2. Oh, but it isn't possible to pass multiple arguments via performSelector unless wrapper objects like NSNotification are used. Who wants to clean up wrapper objects.
    3. Overriding observeValueForKeyPath when a superclass also uses KVO makes any generic approaches hard -- you have to know which notifications to pass to the super class and which to keep.
    4. Who wants to re-implement the same generic selector-based observeValueForKeyPath in every object anyway? Better to just do it once and reuse it.

    A nice variation might be to add another field like id additionalContext to KVODispatcher and have that additionalContext object passed in the objc_msgSend call. Could be useful to use it to stash a UI object that needs to get updated when the observed data changes. Even perhaps an NSArray.