More on MVC on the iPhone

In my previous post, I showed how you could set up a model and using key/value observing, know when various properties of the model were changed. There is one potential problem with this method, as Sam Wan brought up on the comments – you only get one method to respond to all the model changes: observeValueForKeyPath:ofObject:change:context:. So if you are observing many properties of the model, you wind up with a big if/else or switch statement to figure out what change just occurred. I was thinking about, and mentioned, the idea of passing in selectors as contexts when making the addObserver call. This post is about how you would go about doing this.

The idea is that for each property you observe, in the addObserver call, you pass in a selector in the context param. In the observeValueForKeyPath… method, you perform that selector, i.e. execute that method. That way, each property is tied to a specific method, and that method will be called when that property changes.

We’ll need some scaffolding to set this up. First a really simple model:

[c]#import

@interface Model : NSObject {
BOOL propertyA;
BOOL propertyB;
}

@property BOOL propertyA;
@property BOOL propertyB;

@end
[/c]

and

[c]#import “Model.h”

@implementation Model
@synthesize propertyA;
@synthesize propertyB;

@end
[/c]

As you can see, it simply has two Boolean properties, propertyA and propertyB.

The UI consists of two buttons and a label.
mvc2model

The label will be hooked up to an IBOutlet called “label” and the buttons will be hooked up to two IBActions called “onButtonA” and “onButtonB”. These outlets and actions will be in the view controller. Here’s the header:

[c]#import
#import “Model.h”

@interface MVC2ViewController : UIViewController {
UILabel *label;
Model *model;
}

@property (nonatomic, retain) IBOutlet UILabel *label;

– (IBAction)onButtonA:(id)sender;
– (IBAction)onButtonB:(id)sender;
– (void)methodA;
– (void)methodB;

@end
[/c]

Here you can see we have the label and the model and the two button actions. We also have two other methods, methodA and methodB, which we’ll get to shortly.

Now the implementation. I’ll dump it all on you, then explain it.

[c]#import “MVC2ViewController.h”

@implementation MVC2ViewController
@synthesize label;

– (void)viewDidLoad {
[super viewDidLoad];
model = [[Model alloc] init];
[model addObserver:self forKeyPath:@”propertyA” options:NSKeyValueObservingOptionNew context:@selector(methodA)];
[model addObserver:self forKeyPath:@”propertyB” options:NSKeyValueObservingOptionNew context:@selector(methodB)];
}

– (IBAction)onButtonA:(id)sender
{
model.propertyA = YES;
}

– (IBAction)onButtonB:(id)sender
{
model.propertyB = YES;
}

– (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
SEL selector = (SEL)context;
[self performSelector:selector];
}

– (void)methodA
{
label.text = @”methodA was called”;
}

– (void)methodB
{
label.text = @”methodB was called”;
}

– (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning]; // Releases the view if it doesn’t have a superview
// Release anything that’s not essential, such as cached data
}

– (void)dealloc {
[label release];
[model release];
[super dealloc];
}

@end
[/c]

OK, first off, we create the model, then we listen for changes on two values – propertyA and propertyB. Only, unlike before, where we passed in nil to the context, now we are passing in two separate selectors, one for methodA, and one for methodB:

[c][model addObserver:self forKeyPath:@”propertyA” options:NSKeyValueObservingOptionNew context:@selector(methodA)];
[model addObserver:self forKeyPath:@”propertyB” options:NSKeyValueObservingOptionNew context:@selector(methodB)];[/c]

Now, when button A or button B is pressed, it will call onButtonA or onButtonB, which will set the model’s propertyA or propertyB to YES. Senseless example, but concentrate on the mechanics of what’s happening. If you understand it you can apply it to anything.

Now, when either property is changed, the observeValueForKeyPath… method will be called. This is passed the context we defined when we added the observer. It will either be a selector for methodA or for methodB. The context is cast to a pointer to void, so recast it to SEL, which is a selector. Then call performSelector, passing in that selector:

[c]- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
SEL selector = (SEL)context;
[self performSelector:selector];
}[/c]

The result of this is that if propertyA was changed, methodA will be performed. If propertyB was changed, methodB will be performed. No switch statements, no if/elses.

These methods just set the label to appropriate text. Realize that in a real app, they would likely need the updated property from the model itself. They could either just grab that from the model directly, or, in the observeValueForKeyPath… method, you could pass the new value in when you performed the selector, something like this:

[c]- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
SEL selector = (SEL)context;
[self performSelector:selector withObject:change];
}[/c]

Of course, now your methodA and methodB would need to be set up to accept a parameter, and you would probably want to cast “change” to the right type.

Also, look at the other variations of performSelector. There are some tasty possibilities there.

I think the most important thing if you are doing this is that for EVERY observer you add, you MUST pass in a valid selector in the context. And if you are passing arguments like the last example, and casting them to a particular type, then EVERY selector MUST be set up to receive the same type of argument. Of course, you could pass them as pointers to void and let the selector cast them too I guess. That gets a bit ugly though. That’s why I think it is generally better to have the method query the model directly for the data it needs, rather than passing changed values.

I have no idea if this is a standard or accepted way of doing stuff like this, but it seems pretty simple and clean and until I hear of something better, I think I’ll be using code like this. Hope it helps.

This entry was posted in iPhone, Objective C. Bookmark the permalink.

5 Responses to More on MVC on the iPhone

  1. Peter Balogh says:

    This may be totally obvious, but [self performSelector: mySel withObject: nil afterDelay: 1.0]; is the equivalent of the old setTimeout() function. Very handy…

  2. Erik says:

    That’s a nice way of dealing with the bloat that can accumulate in observeValueForKeyPath:blah:blah: method; nice job.

    One other kind of bloat that can occur in the observeValueForKeyPath:blah:blah method is testing to see which *instance* the method is being invoked for. That is, if your observing object A is listening for changes to “foo” on both objects B and C, and you want to take a different course of action if “foo” changed on B versus C, you still have to test the instance – and in most cases, you won’t be able to write the code to “know” what instances are being observed so that you can test them (at least not without jumping through a lot of hoops).

    AS3’s event registration system is much nicer than Cocoa’s key value observing system in this regard. Maybe when closures come to Objective-C we will get support for something like this?

  3. Rahmat Hidayat says:

    I’ve tried to download the iphone sdk recentely, and i keep getting a warning saying my session has expired. perhaps you know how to resolve this problem ?

  4. cain says:

    I guess you could also forget about the context, and get the observeValueForKeyPath method to create a method name from the keyPath. For instance, if the keyPath is ‘text’, then the method name textChanged is constructed (just add Changed to the keyPath), and then called with the value of text … [self textChanged:thetext]; … or something.

    But that’s probably making things too tightly coupled.

  5. Marco says:

    This is probably more efficient since 95% of the time you don’t need the change nsdictionary, you just need to know what the new value is:
    [self performSelector:selector withObject:[change new]];

Leave a Reply