BIT-101 [2003-2017]

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 <uikit/UIKit.h>
#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.

« Previous Post
Next Post »