What's the best way to communicate between view controllers?

A Hopeful Soul picture A Hopeful Soul · Feb 20, 2009 · Viewed 37.8k times · Source

Being new to objective-c, cocoa, and iPhone dev in general, I have a strong desire to get the most out of the language and the frameworks.

One of the resources I'm using is Stanford's CS193P class notes that they have left on the web. It includes lecture notes, assignments and sample code, and since the course was given by Apple dev's, I definitely consider it to be "from the horse's mouth".

Class Website:
http://www.stanford.edu/class/cs193p/cgi-bin/index.php

Lecture 08 is related to an assignment to build a UINavigationController based app that has multiple UIViewControllers pushed onto the UINavigationController stack. That's how the UINavigationController works. That's logical. However, there are some stern warnings in the slide about communicating between your UIViewControllers.

I'm going to quote from this serious of slides:
http://cs193p.stanford.edu/downloads/08-NavigationTabBarControllers.pdf

Page 16/51:

How Not To Share Data

  • Global Variables or singletons
    • This includes your application delegate
  • Direct dependencies make your code less reusable
    • And more difficult to debug & test

Ok. I'm down with that. Don't blindly toss all your methods that will be used for communicating between the viewcontroller into your app delegate and reference the viewcontroller instances in the app delegate methods. Fair 'nuff.

A bit further on, we get this slide telling us what we should do.

Page 18/51:

Best Practices for Data Flow

  • Figure out exactly what needs to be communicated
  • Define input parameters for your view controller
  • For communicating back up the hierarchy, use loose coupling
    • Define a generic interface for observers (like delegation)

This slide is then followed by what appears to be a place holder slide where the lecturer then apparently demonstrates the best practices using an example with the UIImagePickerController. I wish the videos were available! :(

Ok, so... I'm afraid my objc-fu is not so strong. I'm also a bit confused by the final line in the above quote. I've been doing my fair share of googling about this and I found what appears to be a decent article talking about the various methods of Observing/Notification techniques:
http://cocoawithlove.com/2008/06/five-approaches-to-listening-observing.html

Method #5 even indicates delegates as an method! Except.... objects can only set one delegate at a time. So when I have multiple viewcontroller communication, what am I to do?

Ok, that's the set up gang. I know I can easily do my communication methods in the app delegate by reference's the multiple viewcontroller instances in my appdelegate but I want to do this sort of thing the right way.

Please help me "do the right thing" by answering the following questions:

  1. When I am trying to push a new viewcontroller on the UINavigationController stack, who should be doing this push. Which class/file in my code is the correct place?
  2. When I want to affect some piece of data (value of an iVar) in one of my UIViewControllers when I am in a different UIViewController, what is the "right" way to do this?
  3. Give that we can only have one delegate set at a time in an object, what would the implementation look like for when the lecturer says "Define a generic interface for observers (like delegation)". A pseudocode example would be awfully helpful here if possible.

Answer

Clint Harris picture Clint Harris · Feb 22, 2009

These are good questions, and its great to see that you're doing this research and seem concerned with learning how to "do it right" instead of just hacking it together.

First, I agree with the previous answers which focus on the importance of putting data in model objects when appropriate (per the MVC design pattern). Usually you want to avoid putting state information inside a controller, unless it's strictly "presentation" data.

Second, see page 10 of the Stanford presentation for an example of how to programmatically push a controller onto the navigation controller. For an example of how to do this "visually" using Interface Builder, take a look at this tutorial.

Third, and perhaps most importantly, note that the "best practices" mentioned in the Stanford presentation are much easier to understand if you think about them in the context of the "dependency injection" design pattern. In a nutshell, this means that your controller shouldn't "look up" the objects it needs to do its job (e.g., reference a global variable). Instead, you should always "inject" those dependencies into the controller (i.e., pass in the objects it needs via methods).

If you follow the dependency injection pattern, your controller will be modular and reusable. And if you think about where the Stanford presenters are coming from (i.e., as Apple employees their job is to build classes that can easily be reused), reusability and modularity are high priorities. All of the best practices they mention for sharing data are part of dependency injection.

That's the gist of my response. I'll include an example of using the dependency injection pattern with a controller below in case it's helpful.

Example of Using Dependency Injection with a View Controller

Let's say you're building a screen in which several books are listed. The user can pick books he/she wants to buy, and then tap a "checkout" button to go to the checkout screen.

To build this, you might create a BookPickerViewController class that controlls and displays the GUI/view objects. Where will it get all the book data? Let's say it depends on a BookWarehouse object for that. So now your controller is basically brokering data between a model object (BookWarehouse) and the GUI/view objects. In other words, BookPickerViewController DEPENDS on the BookWarehouse object.

Don't do this:

@implementation BookPickerViewController

-(void) doSomething {
   // I need to do something with the BookWarehouse so I'm going to look it up
   // using the BookWarehouse class method (comparable to a global variable)
   BookWarehouse *warehouse = [BookWarehouse getSingleton];
   ...
}

Instead, the dependencies should be injected like this:

@implementation BookPickerViewController

-(void) initWithWarehouse: (BookWarehouse*)warehouse {
   // myBookWarehouse is an instance variable
   myBookWarehouse = warehouse;
   [myBookWarehouse retain];
}

-(void) doSomething {
   // I need to do something with the BookWarehouse object which was 
   // injected for me
   [myBookWarehouse listBooks];
   ...
}

When the Apple guys are talking about using the delegation pattern to "communicate back up the hierarchy," they're still talking about dependency injection. In this example, what should the BookPickerViewController do once the user has picked his/her books and is ready to check out? Well, that's not really its job. It should DELEGATE that work to some other object, which means that it DEPENDS on another object. So we might modify our BookPickerViewController init method as follows:

@implementation BookPickerViewController

-(void) initWithWarehouse:    (BookWarehouse*)warehouse 
        andCheckoutController:(CheckoutController*)checkoutController 
{
   myBookWarehouse = warehouse;
   myCheckoutController = checkoutController;
}

-(void) handleCheckout {
   // We've collected the user's book picks in a "bookPicks" variable
   [myCheckoutController handleCheckout: bookPicks];
   ...
}

The net result of all this is that you can give me your BookPickerViewController class (and related GUI/view objects) and I can easily use it in my own application, assuming BookWarehouse and CheckoutController are generic interfaces (i.e., protocols) that I can implement:

@interface MyBookWarehouse : NSObject <BookWarehouse> { ... } @end
@implementation MyBookWarehouse { ... } @end

@interface MyCheckoutController : NSObject <CheckoutController> { ... } @end
@implementation MyCheckoutController { ... } @end

...

-(void) applicationDidFinishLoading {
   MyBookWarehouse *myWarehouse = [[MyBookWarehouse alloc]init];
   MyCheckoutController *myCheckout = [[MyCheckoutController alloc]init];

   BookPickerViewController *bookPicker = [[BookPickerViewController alloc] 
                                         initWithWarehouse:myWarehouse 
                                         andCheckoutController:myCheckout];
   ...
   [window addSubview:[bookPicker view]];
   [window makeKeyAndVisible];
}

Finally, not only is your BookPickerController reusable but also easier to test.

-(void) testBookPickerController {
   MockBookWarehouse *myWarehouse = [[MockBookWarehouse alloc]init];
   MockCheckoutController *myCheckout = [[MockCheckoutController alloc]init];

   BookPickerViewController *bookPicker = [[BookPickerViewController alloc] initWithWarehouse:myWarehouse andCheckoutController:myCheckout];
   ...
   [bookPicker handleCheckout];

   // Do stuff to verify that BookPickerViewController correctly called
   // MockCheckoutController's handleCheckout: method and passed it a valid
   // list of books
   ...
}