Search
Rich's Mad Rants
Powered by Squarespace

Entries in Storyboards (3)

Monday
Sep242012

Advanced Storyboard Techniques

I'm not afraid to admit it. I love Storyboards. They were one of the new features in iOS 5 that greatly changed how I develop applications. I must say, it's nice to see how they continue to evolve and mature in iOS 6.

Not surprisingly, I heavily emphasized Storyboards in my book, Creating iOS 5 Apps: Develop and Design. However, it turns out they are a lot more flexible and powerful than I originally gave them credit for. So, without further ado, here's a shotgun blast of advanced Storyboard tips and techniques. Some of it applies to iOS 5; however, much of it is new for iOS 6. 

Segue Timing

Understanding the timing behind segues is important to using them successfully. When a new segue fires, the following steps occur:

  1. The destination view controller is instantiated (initWithCoder: is called)
  2. The source view controller's prepareForSegue:sender: method is called
  3. The destination view is loaded
    1. The destination's viewDidLoad is called
  4. The destination view appears
    1. The source's viewWillDisappear: is called
    2. The destination's viewWillAppear: is called
    3. The source's viewDidDisappear: is called
    4. The destination's viewDidAppear: is called.

When a view controller is disposed (dismissing a modal view or popping a navigation view), we go through the following sequence:

  1. The source's viewWillDisappear: is called
    1. The destination's viewWillDisappear: is called
    2. The source's viewWillAppear: is called
    3. The destination's viewDidDisappear: is called
    4. The source's viewDidAppear: is called.
  2. The destination view is deallocated

There are a few key point worth noting. Every time we fire off a segue, we will instantiate (and then later deallocate) a new copy of the destination's view controller. If you need to preload or cache your view controllers, you will want to manage the view controllers programmatically.

Also, when the prepareForSegue:sender: method is called, the destination controller's view has not yet loaded. You cannot access any of that controller's outlets. The best practice is to just pass whatever data you wish to the destination controller--storing the data in properties as necessary, and then let the destination use its properties in its viewDidLoad method to configure its own outlets.

Finally, our code can alter the order--sometimes in unexpected ways. For example, if we touch the destination's view property before its view has loaded, we will force the view to load. This can cause the destination controller's viewDidLoad method to fire earlier than we expected. Just another reason to leave the view (and everything inside the view) alone. 

Loading New Views Programmatically

Let's say we want to draw a segue to a view controller that builds its views programatically (by overriding its loadView method). Originally, I thought this was not possible; that we would have to manage the transition to this view controller entirely in code. Turns out, we can have the Storyboard automatically instantiate and deallocate our view controller--even though we're not designing its view in the Storyboard. There's just one small trick to make it all work:

  1. Make a UIViewController subclass.
  2. Implement its loadView method.
  3. In the Storyboard, drag out a view controller as normal.
  4. Set the view controller's class to your subclass.
  5. Set the segue to new view controller.
  6. Now, comes the secret sauce--delete the view from the view controller's scene.

ProgramaticViewController

When the segue fires, the view will be created using the slightly modified sequence:

  1. The destination view controller is instantiated (initWithCoder: is called)
  2. The source view controller's prepareForSegue:sender: method is called
  3. The destination view is loaded
    1. The destination's custom loadView method is called
    2. The destination's viewDidLoad method is called
  4. The destination view appears
    1. The source's viewWillDisappear: is called
    2. The destination's viewWillAppear: is called
    3. The source's viewDidDisappear: is called
    4. The destination's viewDidAppear: is called.

We can then instantiate and layout our views and subviews in the loadView method, as normal. We can even draw segues from this view controller to other view controllers in the Storyboard--though, since we don't have any controls to link the segue to, we will need to trigger these segues in code (draw the segue from the controller itself, then have the controller call performSegueWithIdentifier:sender: to trigger the desired segue).

Loading New Views Using Nibs

Being able to include programatically-designed view controllers is cool--but it would be even better if we could load the view controller from another nib file. This is basically the same as the previous example, except instead of implementing the loadView method, we provide a properly named nib file. 

Note: There's no way to specify the nib file for our view controller. However, whenever no name is provided, UIViewController searches for a nib whose name matches the view controller's class name. So, if we have a view controller called MyCoolViewController, it would look for MyCoolViewController.xib. As long as the names match, the nib will load as expected.

Loading New Views From Other Storyboards

For me, this is the holy grail for Storyboards. I'd love to be able to create a Storyboard that is a composite of other Storyboards. Ironically, there doesn't appear to be any way to do this from within Interface Builder. The "add the view controller, then delete the view" trick won't work. Don't get me wrong, we can still string together Storyboards, but we have to stitch everything together in code.

To segue to another Storyboard:

  1. Get the Storyboard by calling UIStoryboard's storyboardWithName:bundle: method.
  2. Get the view controller by calling instantiateInitialViewController (to get the initial view controller) or instantiateViewControllerWithIdentifier: (to get an arbitrary controller from within the Storyboard).
  3. Present the view controller (e.g. presenting it modally or pushing it onto a navigation controller).

It's a bit annoying that we have to do all this in code--but it's still a nice trick to master. There are often good reasons to split a Storyboard into separate, independent modules. For example, you might have different people working on different parts of the UI. Or, you might just want to prevent a single Storyboard from becoming overly complex.

On the other hand, many of the benefits of using Storyboards (e.g. drawing segues or unwinding segues between scenes) won't work between Storyboards. So, there are also good reasons to keep everything together. I guess the bottom line is, don't split things up just for the sake of splitting things up. But, don't be afraid to split them up either.

Unwinding Segues

In iOS 5, we could easily pass data forward along the segues--but we didn't have an automated way of passing data back. To get around this, we typically used a delegate pattern. The destination view would declare a delegate protocol. The sending view would then implement this protocol. In the prepareforSegue:sender: method, the sender would assign itself as the destination's delegate. The destination controller could then call any of the delegate methods--allowing us to send arbitrary messages back upstream.

While this is still a powerful design pattern, iOS 6 has partially automated the procedure--letting us more easily create a wider range transitions. Basically, we can define how the Storyboard will unwind any given segue.

In the controller you wish to return to, implement a method that returns an IBAction, and that takes a single UIStoryboardSegue argument:

- (IBAction)returnActionForSegue:(UIStoryboardSegue *)returnSegue {

// do useful actions here.

}

Now, in the scene we wish to unwind, control-drag from the UI element that will trigger the return to the scene's exit icon (the green icon in the dock). In the popup menu, select the previously defined return item.

DragToExitIcon

Note1: We cannot simply create a segue from the button back to the desired scene, since that will instantiate an entire new copy of the scene. Instead, we want to return to the existing copy of the scene.

Note2: We don't necessarily need to return to the previous scene. This is particularly handy if we are pushing a number of scenes into a navigation controller, and want to pop back to an arbitrary point in the middle--or if we're mixing modal and push segues, and want to unwind to any point in the chain.

The Details

The system will search for the return destination by calling canPerformUnwindSegueAction:fromViewController:withSender: on the current view controller. By default, this method simply checks to see if you have a method whose selector matches the return action. If it does not find a match, it will search the parent view controller, and will continue searching up the chain of parent view controllers until it finds a match.

We can override canPerformUnwindSegueAction:fromViewController:withSender: to add more control to the search--for example, if we define the same return action in multiple view controllers, and then override this method to make sure we return to the correct one.

Furthermore, container view controllers are responsible for searching their child view controllers. Apple's built-in container views all do the right thing. For example, a UINavigationViewController will search through the view controller stack from the top down, starting just below the exiting controller.

If we want our own custom container controller to search through its child view controllers, we need to override viewControllerForUnwindSegueAction:fromViewController:withSender:. In this method, we should iterate over our child view containers, calling canPerformUnwind… on them. If we find a match, we should return it. If not, we should call the superclass's implementation and return the result.

Once it finds a match, the system then calls segueForUnwindingToViewController:fromViewController:identifier: on the destination controller. Again, we need to override this method for our custom container controllers, returning a segue that performs the animation and other necessary steps to unwind the view controllers.

Then, the system calls prepareForSegue:sender: on the exiting view controller, followed by calling the exit action on the destination view controller. We can use either of these steps to pass data back to the return point.  Finally the system triggers the segue.

This is a bit complicated, so let's walk through a concrete example. Let's say I have a multi-page form that I want users to fill out. To use the form, I modally present a navigation controller. The navigation controller's root view controller is the first page of my form. As the user fills out each page, I push a new one on the navigation controller. On the final page I have a done and a clear buttons. 

The done button should take the user back to my original view (the navigation controller's presenting view controller). The clear button should just take them back to the beginning of the form.

To set this up, I need to implement two methods. formCompleted: should be implemented on the view controller that will present the form, while clearForm:  should be implemented on the first page's view controller. I then draw connections from the done button to the formCompleted:  exit action, and from the clear button to the clearForm:  exit action.

When the clear button is pressed, the system searches the final page's view controller for the clearForm:  method. It doesn't find a match, so it searches the parent view controller--our navigation controller. The navigation view controller manages the search of its children. Basically, it starts at the top of the stack, just below the last page controller, and works its way down.  Eventually it finds the method on its root view controller--so it returns that controller as the destination.

The navigation controller then creates a segue where it pops to the destination with animation, and passes this back to the final page controller. The final page's prepareForSegue:sender: is called. The root controller's clearForm: method is called. Then the segue is triggered.

If I press the done button, it will search the last page, then search the navigation controller. This time, the navigation controller doesn't find anything, so the system will search its presenting controller. Here a match is found. The presenting controller automatically creates a segue to dismiss the modal view controller with the correct animation. Again, the final page's prepareForSegue:sender: is called. The root controller's formComplete:  method is called, and the segue is triggered.

As you can see, the ability to unwind segues allows us to easily return to any existing view controller in our view controller hierarchy. in iOS 5, these transitions required a considerable amount of custom code--the more complex the transition, the more complicated the code. Now, these transitions can largely be automated, saving us time and effort, and hopefully avoiding bugs.

Container Views

The iOS SDK has always had two different types of view controllers. The most basic type are the content view controllers. These are the view controllers that we create to display our content. However, the system also had a number of pre-build container view controllers: UINavigationControllerUITabBarController and others. These controllers usually provided a minimal user interface of their own (the navigation or tab bar itself). Instead, they manage a number of content view controllers for us.

With iOS 5, Apple gave us the ability to create our own content view controllers. This is a surprisingly powerful technique, letting us easily break a complex user interface into several independent parts. However, all of the management of these sub view controllers and their views had to be done in code.

Now, with iOS 6, we can add a container view to our Storyboards (though, not our nibs). Simply drag a Container View object from the library and place it on one of your scenes. Interface Builder will automatically add a content view controller for the container view, with an embed segue--though you can change the segue to embed any view controller in the Storyboard.

AddingAContainerView

When the parent view controller is loaded, this segue will fire, and the child view will also load. Timing-wise, the child view will finish loading before the parent's viewDidLoad method is called, so you can safely configure the child from within the parent's viewDidLoad.

Unfortunately, this only gets us about half way to our goal. Don't get me wrong. It's a cool new feature, and I love the fact that I can graphically lay out my container views. However, I typically use container views because I'm going to be dynamically swapping the content within those views. And this may or may not be possible to set up in the Storyboard.

If the child views are responsible for swapping themselves around, then you can simply draw segues from one child to the next as usual. There are no problems, and everything just works (though, see the note on unwinding segues above).

However, there's no way to have the parent view controller drive these segues. To really let the parent freely drive arbitrary transitions between all the possible child controllers, you'll have to manage everything in code. Here's a common work flow:

  1. Instantiate the new child view controller.
  2. Set the frame of the new child controller's view to the existing container view.
  3. Animate the transition between the views.
  4. Remove the old child controller from the parent.
  5. Add the new child controller to the parent.

Assuming you only have a single container view--and thus only a single child controller--you could implement this as shown below. Obviously, if you want to preload or cache the view controllers, or if you have multiple container views, your implementation may become much more complex. 

// Assumes we have a MyChildViewController.xib, or that 
// MyChildViewController programmatically creates its views.
MyChildViewController *controller = [[MyChildViewController alloc] init];

// Assumes we only have 1 child view controller
UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerView.frame;

[UIView
 transitionFromView:self.containerView
 toView:controller.view
 duration:0.25f
 options:UIViewAnimationOptionTransitionCrossDissolve
 completion:^(BOOL finished) {
    [currentChild removeFromParentViewController];
    [self addChildViewController:controller];
}];

 

If you want to load another controller from within the Storyboard, you would modify the code as shown below:


UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];

// Must match the controller's Storyboard ID
SecondChildViewController *controller =
[storyboard instantiateViewControllerWithIdentifier:@"SecondChild"];


// Assumes we only have 1 child view controller
UIViewController *currentChild = [self.childViewControllers lastObject];
controller.view.frame = self.containerView.frame;

[UIView
 transitionFromView:self.containerView
 toView:controller.view
 duration:0.25f
 options:UIViewAnimationOptionTransitionCrossDissolve
 completion:^(BOOL finished) {
    [currentChild removeFromParentViewController];
    [self addChildViewController:controller];
}];

 

Be sure to set the matching Storyboard ID in the Identity Inspector for the child view controller--otherwise this won't work.

And that's it for now. I hope you found at least some useful information in here. Also, please let me know if you find any other cool Storyboard tricks.

Thursday
May172012

RubyMotion and Storyboards: two great tastes that taste great together

Ok, I'll admit it. I'm very excited by RubyMotion--the new framework that lets us write Ruby-based applications for our iOS devices. I've long been a huge fan of MacRuby, and I've been waiting impatiently for it to show up on iOS. I'm not sure how they managed it, but HipByte made the magic happen, and I purchased a developer's license as soon as I learned about it.

Unfortunately, my first impressions were somewhat tempered. Don't get me wrong, I loved the Ruby bits--I just wasn't that excited about building my interfaces by hand. Yes, I could use Xcode to create nib files and copy them into my project's resources directory--but that seemed like a lot of work…especially as the number of nibs grew.

However, RubyMotion is moving at near-light speed. In the few weeks since release, we've already seen four updates, and I must say, RubyMotion 1.4 made me giggle like a school girl. This update added support for Storyboards. Suddenly RubyMotion became a lot more useful to me.

To test out the new Storyboard integration, I've created a silly proof-of-concept project. It doesn't do anything useful, other than act as a test bed for trying various Storyboard fatues. Still, I'm quite pleased with the results. The process remains a bit more manual than I'd like--but it's a huge improvement over dealing with multiple nibs, or (shudder) building everything by hand.

There are a few steps that may not be obvious, so I'm going to quickly walk through everything at a high-level. For more details, be sure to check out the source code. It's small, and everything should be obvious. As always, if you have questions, just leave them in the comments.

Building the App

First things first, I could simply create a storyboard file with Xcode, and layout all my scenes. However, this would be largely useless. Yes, I can draw the segues and relationships. I can even change the view controller classes to the correct Ruby classes. However, I cannot connect my outlets or actions.

The problem is, Xcode doesn't know anything about my Ruby classes--so it cannot know which actions and outlets are available. Therefore, I have to make an entire Xcode project, and then create stub classes for my Ruby classes (at least, for any class with an outlet or action). I can then define the actions and outlets in these stub classes, and draw connections to my heart's content.

Note: The Xcode project doesn't need to compile or run. I just need header files with the proper IBOutlet and IBAction tags.

Once we have a completed storyboard, simply copy the storyboard file into our RubyMotion project's resources directory. You'll have to repeat this step any time you modify the storyboard. Usually, rake will detect the new storyboard file and update everything with the next build--however, if you're not seeing the expected changes, make sure you actually saved the storyboard, then copy it again. Delete the previously compiled version (it will also be in the resources directory, and will end in "c"), and then execute rake clean, just to be safe.

With the storyboard in place, I simply created Ruby classes whose names matched the class stubs in the Xcode project. Then I create their outlets and actions.

For outlets, I simply declared attributes with matching names:

attr_accessor :label, :textField, :delegate

Similarly, Actions are just methods. If an action method in the Objective-C header looks like this:

- (IBAction)textChanged:(id)sender;

The RubyMotion equivalent will be:

def textChanged(sender)

  # do something here

end

Next, I need to load the storyboard when the app launches. In an Xcode project, this would be defined in the info.plist. In RubyMotion, we can set those values in the Rakefile:

app.info_plist['UIMainStoryboardFile'] = 'MainStoryboard'

Finally, my app delegate needs to implement the window property. Oddly enough, I cannot use attr_accessor :window for this. Instead, I must explicitly create the required accessor methods:

def window

  @window

end

 

def setWindow(window)

  @window = window

end

And, that's it. The storyboard-based app is good to go.

If you have any interest in Ruby and iOS, then I'd strongly recommend giving RubyMotion a look. Yes, the developer license is a bit steep. If you need a free trial, you can always play around with MacRuby on the OS X side--however, honestly, I think RubyMotion feels more slick and solid at the moment. Especially since I can't seem to get MacRuby's Xcode integration to work with the new App-store version of Xcode.

Note: If you look at my Rakefile you will see that I've explicitly set the codesign_certificate. Typically, you won't need to do this. However, I have multiple code sign certificates on my laptop, and by default, RubyMotion selects the wrong one.

Thursday
Feb232012

Talking Mountain Lion and Storyboards on iDeveloper Live

I was lucky enough to be on Scotty and John's wonderful iDeveloper Live podcast this monday. Unfortunately, I was so sick this week that I both forgot to announce it, and I forgot to check back and see when the show was posted. Anyway, it's available now.  You can downlaod the audio here

I was orignally scheduled to talk about Storyboards, but we ended up spending the first half of the show discussing Mountain Lion. It was a great conversation, despite being heavily caffeinated to compensate for illnesses.