Changing Models with Key-Value Observing
Making our model code flexible is the low-hanging fruit, and keeping a view up-to-date with a model is usually a hard problem. A model object has to be aware of all the views accessing its data, which just leads to all sorts of problems and code spaghetti. Imagine a world where it all just works: you change the name on a User, and the appropriate label instantly reflects those changes...no code spaghetti or zero-ing references.
I’m talking this up so much because it is possible to implement. iOS has a concept of key-value observing (KVO). Built into the frameworks is a system by which one object can passively observe changes in properties of another object. Those properties are referred to by keys, which typically correspond to the variable name of the attribute.
In our User case, our controller would observe the "name" key of the User. In the callback for that observation, we would reset the label’s text to reflect the new name value.
I don’t know about you, but I’m anxious to write some code. But not so fast! The original implementation of KVO isn’t very Ruby-like. Thankfully, there is a popular RubyGem called BubbleWrap (http://bubblewrap.io) that “wraps” many Objective-C APIs into idiomatic Ruby structures. To install it, run gem install bubble-wrap in your shell and add require "bubble-wrap" to your Rakefile.
Note here that the order of these statements does matter: BubbleWrap’s require should come before the block involving Bundler.
BubbleWrap is an extensive library with many different features, so definitely browse its documentation sometime. For now, we’re just going to use the wrappers it creates for key-value observing on our model. We’ll be working with UserController, so let’s open that and include BubbleWrap’s KVO module.
|||class UserController < UIViewController|
This is a nice and easy first step. This gives us access to the observe(object, key) method, which we’ll use right now. In the loop over User::PROPERTIES, we’re going to observe each property and update the value label accordingly. After we initialize the label, add the observe method.
|||value = UILabel.alloc.initWithFrame(CGRectZero)|
|||value.text = self.user.send(prop)|
|»||observe(self.user, prop) do |old_value, new_value||
|»||value.text = new_value|
We pass it the object we want to observe (self.user) and the key we want updates about (prop). The callback block returns both the old and new values for the property, which could be useful if we were making more discriminatory UI updates.
Since the controller’s title is originally set to the user’s name, it’s a nice idea to update that as the name changes.
|||self.title = self.user.name|
|»||observe(self.user, "name") do |old_value, new_value||
|»||self.title = new_value|
Finally, we need to do a quick cleanup by overriding viewDidUnload, one of the controller life-cycle methods. Since we’re creating these observations in viewDidLoad, we need to mirror their un-observing in the counterpart method.
Whew, all done. Let’s play with our app and actually see the fruits of our labor. Go ahead and rake and get ready to use the debugger!
In the interactive debugger, grab the @user instance variable of our AppDelegate. BubbleWrap includes a nifty shortcut for grabbing the app’s delegate, which we can now use.
|||(main)> user = App.delegate.instance_variable_get("@user")|
|||=> #<NSKVONotifying_User @id="123", @email="firstname.lastname@example.org", @phone="555-555-5555">|
Kind of a weird class name, isn’t it? I won’t get too technical, but under the hood KVO does a lot of tricks involving dynamically subclassing your observed objects. But it is proof that our user is being observed! So, let’s make some changes:
|||(main)> user.email = "email@example.com"|
|||(main)> user.name = "Charlie"|
In Chapter 1, Creating a New App, we learned to make changes to the UI using the debugger, but now we’re not even playing with the view objects! Everything just works. This is an incredibly powerful asset in your toolbox when it comes to making more complex, reactive apps.
So, now that we can make all these changes and synchronize the UI, how can we save them? If we quit the app right now and restart, our old “Clay” user will still be hanging around. And I personally have no problem with that, but most apps will want to save the user’s changes.