views:

129

answers:

3

Design question:

I have ViewController A which contains an NSMutableArray*. The ViewController A is responsible for displaying the user a Map, when the user interacts with this map, the view controller A fills the NSMutableArray* with Coordinate Objects.

The information contained in the NSMutableArray* should be later displayed in a UITable which is contained in another view asociated to a ViewController B.

What is the most correct way for ViewController B to access the NSMutableArray that was filled by A ? ( A holds reference to the NSMutableArray* ).

There should be several ways to do this, but to maintain myself pure to MVC, I would really like to know your opinion.

A: 

In your scenario, there should be someone responsible (MASTER) for the creation / update of the NSMutableArray filled with coordinates. In your case, this master seems to be Controller A.

Then, you want another object, controller B, to use this same information (model). This object is a kind of slave from Controller A : he needs to know when the model is changed to display it accordingly (as a list).

The way I would do it : Controller A can be set a delegate and this delegate should implement a protocol in which any update on the model (performed by A) is notified to the delegate (B). A rough definition of the protocol could be -(void)modelHasChanged:(NSArray*)theNewModel

controller B only has a readonly access to the model : even if controller A manipulates a NSMutableArray (to change content) , controller B only see it as a immutable NSArray : this ensure that only controller A is the real master of this model and that B cannot change its content.

You could also choose another approach : split the object that manage the model (NSMutableArray) and the 2 ways to represent it : as a map (controller A), or as a list (controllerB) You would then have 3 abstractions :

1 master that manipulates a NSMutableArray

1 controller A for the map on which you could set the model to be displayed as a NSArray (readonly access to the same instance manipulated by the previous master).

1 controller B for the list on which you could set the model to be displayed as a NSArray (readonly access to the same instance manipulated by the previous master).

yonel
@yonel Hello!, I didin't quite got your explanation about setting a delegate and implementing a protocol... could you go a bit more in depth, or maybe edit your post and make your explanation clearer ? Thanks again!
Mr.Gando
Couldn’t you simply use KVO instead of the protocol? At least if the performance considerations do not force you into partial updates.
zoul
@zoul : I'm not used to the KVO approach so I couldn'y say :) That's why I'm going to read your answer with strong interest !
yonel
+1  A: 

What is the most correct way for ViewController B to access the NSMutableArray that was filled by A?

I’d do something simple and only return to the decision if it causes problems. Something simple could be exposing the array in the public interface of controller A and sending notifications about the array updates so that B can watch:

@interface A
@property(readonly) NSArray *foos;
@implementation
- (void) updateFoos {
    NSString *const key = @"foos";
    [self willChangeValueForKey:key];
    [foos doSomething];
    [self didChangeValueForKey:key];
}

@interface B
@implementation
- (void) afterSettingA {
    [a addObserver:self forKeyPath:@"foos" options:0 context:NULL];
}
- (void) observeValueForKeyPath: (NSString*) keyPath ofObject: (id) object
    change: (NSDictionary*) change context: (void*) context 
{
    NSAssert([keyPath isEqualToString:@"foos"], @"Somethind fishy going on.");
    // update what depends on foos
}

Another simple solution would be turning the array into a full-fledged model class that you would connect both to A and B. (The connection would have to be done outside the controllers to avoid excessive coupling. You can use Interface Builder, a ‘factory’ class that would wire the objects together or anything else that fits.)

@interface Foo
@property(readonly) NSArray* items;
@implementation
- (void) updateItems {
    // send KVO notifications just as above
}

@interface A
@property(retain) Foo *fooModel;

@interface B
@property(retain) Foo *fooModel;

@interface Factory
@implementation
- (void) wireObjects {
    A *a = [[A alloc] init];
    B *b = [[B alloc] init];
    Foo *fooModel = [[Foo alloc] init];
    [a setFooModel:fooModel];
    [b setFooModel:fooModel];
    // Of course the A and B would be member variables of this
    // class or you would return a root of the constructed object
    // graph from this method, otherwise it would not make sense.
}

In the first solution the B controller has to have a pointer to A so that it can subscribe to the KVO notifications. This connection between the controllers is best maintaned somewhere else than in their code, ie. B should not create an instance of A. (This way you would introduce a tight coupling between A and B. Not very testable etc.) If you already instantiate the controllers in Interface Builder, this is the perfect place to give B the pointer to A. (Simply create an IBOutlet for A in B.)

The second solution with the separate model class is a “cleaner” MVC and does not require the controllers to know each other – they both depend on the model class. You can instantiate the model and link it to the controllers in Interface Builder, too.

By the way: If B wants to watch for changes in some property of A, it has to subscribe after the link to A has been set. A simple but slighly wrong way to do it is to subscribe in the viewDidLoad method of B. It’s convenient, but if the link to A gets changed after that, the notifications do not change accordingly. The harder but correct way to subscribe is in the setter for A – when somebody sets a new A, you cancel the notification subscriptions to the old A and subscribe to the new ones.

zoul
@zoul: Your solution is very interesting, I've always wanted to use KVO. Just a simple question: Your solution involves that @interface B contain a reference to the @interface A? ( a pointer that I set up at init time of my application that links B to A ). If it does, what would be the best way to assign this pointer ? I am instantiating A and B using Interface Builder.
Mr.Gando
I’ve updated the answer. If it’s not clear enough, do not hesitate to ask again.
zoul
@zoul: Your answers is one of the best i've gotten in this webpage. Thank you very much. Btw, there's a small typo that you could fix in this line : NSAssert([keyPath isEqualToString:@"foos"], @"Somethind fishy going on."); //that's the fixed line ( missing ']' ). Thanks again!
Mr.Gando
A: 

Just to throw a little variety into the answers:

You tagged this question with iPhone so I assume that to be the platform you're developing for.

As such this sounds like a perfect use-case for a UINavigationController with two UIViewControllers if you would like to use proper and standard iPhone MVC design.

Have your AppDelegate instantiate a UINavigationController with ViewControllerA set as the root.

Create a new init/dealloc for ViewControllerB where you have a local instance var to hold coordinates.

- (id)initWithCoords:(NSArray *)coords {
    if (self = [super initWithNibName:@"name of your nib" bundle:[NSBundle mainBundle]]) {
        coordinates = [coords retain];
    }
    return self;
}

- (void)dealloc {
    [coordinates release];
    [super dealloc];
}

Now, whenever you want to load up ViewControllerB from ViewControllerA with the selected coords you would simply call a method such as follows:

- (void)actionForCoords {
    ViewControllerB *bCon = [[ViewControllerB alloc] initWithCoords:[NSArray arrayWithArray:selectedCoords]];
    [self.navigationController pushViewController:bCon animated:YES];
    [bCon release];
}

This way the navigation controller handles most of the work for you on it's stack, providing clean/easy object management and animation. And if you pop back to ViewControllerA from ViewControllerB (with the navCon's default "back" implementation for instance) then ViewControllerA will still persist on the stack for you to use and modify while ViewControllerB is released to free resources (but ready to load up again with new coords via "actionForCoords").

Matthew McGoogan