What's the best way to communicate between view controllers?
Solution 1:
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
...
}
Solution 2:
This sort of thing is always a matter of taste.
Having said that, I always prefer to do my coordination (#2) via model objects. The top-level view controller loads or creates the models it needs, and each view controller sets properties in its child controllers to tell them which model objects they need to work with. Most changes are communicated back up the hierarchy by using NSNotificationCenter; firing the notifications is usually built in to the model itself.
For example, suppose I have an app with Accounts and Transactions. I also have an AccountListController, an AccountController (which displays an account summary with a "show all transactions" button), a TransactionListController, and a TransactionController. AccountListController loads a list of all accounts and displays them. When you tap on a list item, it sets the .account property of its AccountController and pushes the AccountController onto the stack. When you tap the "show all transactions" button, AccountController loads the transaction list, puts it in its TransactionListController's .transactions property, and pushes the TransactionListController onto the stack, and so on.
If, say, TransactionController edits the transaction, it makes the change in its transaction object and then calls its 'save' method. 'save' sends a TransactionChangedNotification. Any other controller that needs to refresh itself when the transaction changes would observe the notification and update itself. TransactionListController presumably would; AccountController and AccountListController might, depending on what they were trying to do.
For #1, in my early apps I had some sort of displayModel:withNavigationController: method in the child controller that would set things up and push the controller onto the stack. But as I've become more comfortable with the SDK, I've drifted away from that, and now I usually have the parent push the child.
For #3, consider this example. Here we are using two controllers, AmountEditor and TextEditor, to edit two properties of a Transaction. The Editors should not actually save the transaction being edited, since the user could decide to abandon the transaction. So instead they both take their parent controller as a delegate and call a method on it saying if they've changed anything.
@class Editor;
@protocol EditorDelegate
// called when you're finished. updated = YES for 'save' button, NO for 'cancel'
- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated;
@end
// this is an abstract class
@interface Editor : UIViewController {
id model;
id <EditorDelegate> delegate;
}
@property (retain) Model * model;
@property (assign) id <EditorDelegate> delegate;
...define methods here...
@end
@interface AmountEditor : Editor
...define interface here...
@end
@interface TextEditor : Editor
...define interface here...
@end
// TransactionController shows the transaction's details in a table view
@interface TransactionController : UITableViewController <EditorDelegate> {
AmountEditor * amountEditor;
TextEditor * textEditor;
Transaction * transaction;
}
...properties and methods here...
@end
And now a few methods from TransactionController:
- (void)viewDidLoad {
amountEditor.delegate = self;
textEditor.delegate = self;
}
- (void)editAmount {
amountEditor.model = self.transaction;
[self.navigationController pushViewController:amountEditor animated:YES];
}
- (void)editNote {
textEditor.model = self.transaction;
[self.navigationController pushViewController:textEditor animated:YES];
}
- (void)editor:(Editor*)editor finishedEditingModel:(id)model updated:(BOOL)updated {
if(updated) {
[self.tableView reloadData];
}
[self.navigationController popViewControllerAnimated:YES];
}
The thing to notice is that we've defined a generic protocol which Editors may use to communicate with their owning controller. By doing so, we can reuse the Editors in another part of the application. (Perhaps Accounts can have notes, too.) Of course, the EditorDelegate protocol could contain more than one method; in this case that's the only one necessary.