Monday 10 September 2012

MVC Architecture on the iPhone


MVC on the iPhone: The Model



CocoaTouch is built from the ground up with an MVC paradigm in mind. in most templates, views and view controllers are built for you. UIView and UIViewController are core classes. In many cases you use UIView as is, adding UI elements to a generic UIView in Interface Builder. You subclass UIViewController to add your own functionality. IBOutlets let you hook up UIElements in your view to access them, and IBActions allow you to trigger actions based on user gestures on the UIView elements. All this will be explained to you in any basic tutorial or book on the subject.

But what about the Model? Very little is said about that, at least from what I could find. Most of the basic tutorials I’ve seen avoid the model all together and code any data right into the controllers.

For a couple of the apps I’ve been writing, I finally settled on what I think is a decent implementation, which I’ll share here, not as an authoritative description, but as an offering and even somewhat a request for validation. If this works for you, great. If you can see some improvement, please offer it up.

Basically, I create a Singleton class that extends NSObject for my model. Then use key/value observing to get notified of updates. This is very much like the ModelLocator in Cairngorm, if you’ve worked with that in Flex.

To start with, let’s create a project that has a couple of views. One view will allow the user to change a value. This value will be set on the model, which will trigger a change in the other view. The Utility Application template works well for this. So create a project using that template, naming it “MVC”. (OK, name it whatever you want, but you’ll be able to follow along with my code better if you name it the same thing.)

This will give you something like this:

mvc_01


As you see, you have a main view and a flipside view, with view controllers and nibs for both. And a RootViewController that controls both views. Run the project and take a look through the code to familiarize yourself with what is going on. I’m going to concentrate mainly on the model part, so I assume you understand the rest of this so far.
Now let’s add a label and a text field to these views. The label will go in the main view, and the text field in the flipside view. First let’s make the outlets.
Make your MainViewController.h look like this:

#import
          @interface MainViewController : UIViewController {
          UILabel *label;
          }
          @property (nonatomic, retain) IBOutlet UILabel *label;
          @end


and MainViewController.m like so:

#import "MainViewController.h"
          #import "MainView.h"
          @implementation MainViewController
          @synthesize label;

          - (id)initWithNibName:(NSString *)nibNameOrNil bundle:
          (NSBundle *)nibBundleOrNil {
          if (self = [super initWithNibName:nibNameOrNil bundle:
          nibBundleOrNil])           {
          // Custom initialization
          }
          return self;
          }

          /*
          // Implement viewDidLoad to do additional setup after
          loading the view, typically from a nib.
          -(void)viewDidLoad {
          [super viewDidLoad];
          }
          */

          /*
          // Override to allow orientations other than the 
          defaultportrait orientation.
          -(BOOL)shouldAutorotateToInterfaceOrientation:
          (UIInterfaceOrientation) interfaceOrientation {
          // Return YES for supported orientations
          return (interfaceOrientation == UIInterfaceOrientationPortrait);
          }
          */

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

          - (void)dealloc {
          [label release];
          [super dealloc];
          }
          @end

Similarly, in FlipsideViewController.h, we’ll add a textField outlet. We’ll also add an IBAction which will let us know when the text in the text field has changed.

#import
          @interface FlipsideViewController : UIViewController {
          UITextField *textField;
          }
          @property (nonatomic, retain) IBOutlet UITextField 
          *textField;
          -(IBAction)textChanged:(id)sender;
          @end

and FlipsideViewController.m:
          #import "FlipsideViewController.h"
          @implementation FlipsideViewController
          @synthesize textField;
          - (void)viewDidLoad {
          [super viewDidLoad];
          self.view.backgroundColor = [UIColor 
          viewFlipsideBackgroundColor];
          }

          /*
          // Override to allow orientations other than the 
          default portrait orientation.
          - (BOOL)shouldAutorotateToInterfaceOrientation:
          (UIInterfaceOrientation)interfaceOrientation
          {
          // Return YES for supported orientations
          return (interfaceOrientation == UIInterfaceOrientationPortrait);
          }
          */

          - (void)didReceiveMemoryWarning {
          [super didReceiveMemoryWarning];
          // Releases the view if it doesn't have a superview
          // Release anything that's not essential, such as
          cached data
          }
          - (IBAction)textChanged:(id)sender
          {

          }
          - (void)dealloc {
          [textField release];
          [super dealloc];
          }
          @end


Now you can open up MainView.xib and add a UILabel to the view. Drag a connection from the File’s Owner (MainViewController class) to the label and connect it to the label outlet.

                                     mvc_02



                                     mvc_03

Do the same thing in FlipsideView.xib, adding a UITextField and hooking it up to the textField outlet in the File’s Owner (FlipsideViewController) and hooking up the textChanged IBAction to the editingChanged event of the text field.

                                     mvc_04


                                     mvc_05

By now, you should understand that what we want to do is have the user type some text into the text field, and have that appear in the label. But these things are in two separate views, and controlled by two sepearate view controllers. So how do we hook them together? The model of course. Make sure both these nibs are saved, close Interface Builder and go back to XCode.
The Model
Now we create the model. Add a new file to your project, sublcass NSObject. Name the class Model. Now, I’m not a big fan of Singletons. I know the downsides, and believe they are way overused, but I’m also pragmatic, and I think in a case like this, a Singleton is fine. If you want to start a debate on Singletons, do it elsewhere. If it’s really abhorrent to you, you can create your Model in the RootViewController and pass an instance of it to each of your other view controllers. That will work fine. But again, I’m going to go with Singleton. To do that, I’m using this nifty SynthesizeSingleton file from Matt Gallagher at CocoaWithLove.com. You can get it here. That link is also a good place to debate Singletons, if you feel so obliged. To use it, add the SynthesizeSingleton.h file to your project and call the SYNTHESIZE_SINGLETON_FOR_CLASS macro in the implementation of the class you want to be a Singleton. One thing I noticed is that using this macro, you will get a warning unless you also declare the static sharedModel method in the interface file.
Our model will also need a single property, text, with some public accessors. Here’s Model.h:
            #import <foundation/Foundation.h>
          @interface Model : NSObject {
          NSString *text;
          }
          @property (nonatomic, retain) NSString *text;
          + (Model *)sharedModel;
          @end

And here is Model.m:



                         #import "Model.h"

          #import "SynthesizeSingleton.h"
          @implementation Model

          SYNTHESIZE_SINGLETON_FOR_CLASS(Model);

          @synthesize text;

          - (id) init
          {
          self = [super init];
          if (self != nil){
          text = @"";
          }
          return self;
          }
          - (void) dealloc
          {
          [text release];
          [super dealloc];
          }
          @end






Setting the data on the model.
When the text in the text field changes, the textChanged method in FlipsideViewController will be called. Here we can set the Model’s text property to the new value.
                 -(IBAction)textChanged:(id)sender
          {
          Model *model = [Model sharedModel];
          model.text = textField.text;
          }
Make sure you import Model.h in FlipsideViewController.m.

Listening for changes to the model.
Now, the main view just needs to know when the model is changed. We can do this through key/value observing. This means you set up a watcher on a particular property of an object, which will call a method any time that property is changed. Here we want to know when the text property of the model singleton class is changed. We can do this in the initWithNibName method of the MainViewController class. Here’s what it looks like:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)
  nibBundleOrNil {
  if (self = [super initWithNibName:nibNameOrNil bundle:
  nibBundleOrNil]) {
  // Custom initialization
  Model *model = [Model sharedModel];
  [model addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
  }
  return self;
  }


First we get a reference to the Model singleton. Then we call the addObserver:forKeyPath:options:context method.
The observer is self, the MainViewController class. The keyPath is the property we want to observe, text, as a string. The options allow us to specify what data we want about the change. Here we want the new value of the property that changed. You can also get initial, old, or prior values, or any combination thereof. The context we can leave nil.
Whatever object is observing the change needs to implement a special method that will be called when the change occurs. Here is the signature of that method:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
The keyPath is the property name as a string, the object is the object that owns that property, in this case, the Model, the change is a dictionary containing the old, new, prior, initial values, as specified, and context is whatever context you might have used, in this case, nil.
If we were observing more than one property on the model, we would probably need to use a switch statement with the keyPath to see which property changed. For now, we are only observing text, so we know that’s what changed. We can get the new value of this property by querying the change dictionary parameter. Of course we could also just grab the model’s text property directly, but let’s use the data that’s passed in here.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id) object change:(NSDictionary *)change context:(void *)context
    {
    label.text = [change valueForKey:@"new"];
    }

Here we are asking the change object for its “new” value (which is all that it will contain, since that’s all we specified). We assign that to label.text and we’re done.
And that’s that. We have a model that stores data and broadcasts events when it changes. Note that in the Cocoa MVC paradigm, the views don’t generally listen directly to the model. The view controllers do, and they tell the view what to do. That’s not written in stone, but is generally how you go about it.
Another concept I want to investigate is using NSNotification instead of key/value observing. Not sure if that’s really a viable alternative and what the benefits/drawbacks might be.
Now of course, this is a rather useless example, for illustration only. There are lots of things you’d do differently in a real app, like probably not listening to every edit change event, just getting it when the flipside view is closed. But that’s not the real purpose of the exercise. The purpose is to see how to implement a model. Or at least ONE way to implement one.
Oh, and in case you are too damn lazy to type in a few lines of code, or can’t get it to work right with the above, here’s a link to the project itself.


No comments:

Post a Comment