Switching between Views with a UISegmentedControl

posted by crafterm, 26 May 2010

Please note an alternative approach to this article is available in further writing: Switching Views with a UISegmentedControl – Revisited

It’s boutique, shiny and several apps that ship with the iPhone do it, in this article I’ll step through using a UISegmentedControl to toggle between different subviews, each with their own layout, within a UINavigationController.

To see an example of this use case, take a look at the Apple Calendar app, along the bottom of the screen is a toolbar containing a UISegmentedControl, with the segments List, Day and Month. Tapping on any of these segments changes the view to a completely new layout.

The AppStore app also implements this paradigm to an extent – take a look at the Top 25 tab, in the navigation title view there’s a UISegmentedControl with the segment labels Top Free, Top Paid and Top Grossing.

The AppStore variant is a bit simpler and can actually be implemented using a single UITableView and multiple UITableViewDataSource/UITableViewDelegate objects, with the UISegmentedControl switching between each of them and reloading the table upon index changes.

In my case though, I needed to build the full Ferrari, and allow for switching between views with completely different layouts. In addition, it all had to work well within a navigation controller, with elements within these views pushing onto the navigation stack.

Solution

The solution was to create specialized view controllers for each style of view, and programatically add/remove these views as subviews to a managing view controller, in response to selected segment index changes on the UISegmentedControl.

To do all this, one has to create an additional UIViewController subclass that manages the UISegmentedControl changes, maintains a collection of sub view controllers and adds/removes their views on demand.

The advantage of this approach, is that it keeps the business logic between each subview separate, and makes it easy to add additional segments later as your application grows.

Interface

@interface SegmentManagingViewController : UIViewController <UINavigationControllerDelegate> {
    UISegmentedControl    * segmentedControl;
    UIViewController      * activeViewController;
    NSArray               * segmentedViewControllers;
}

@property (nonatomic, retain, readonly) IBOutlet UISegmentedControl * segmentedControl;
@property (nonatomic, retain, readonly) UIViewController            * activeViewController;
@property (nonatomic, retain, readonly) NSArray                     * segmentedViewControllers;

@end

Here we define a view controller subclass for managing the presentation of multiple subviews. segmentedControl is the UISegmentedControl, either created and assigned in code, or via a Interface Builder. activeViewController represents the view controller currently being presented, and segmentedViewControllers is an array of all view controllers presentable via the segmented control.

I’ll get to the UINavigationControllerDelegate protocol in just a minute..

Implementation

@interface SegmentManagingViewController ()

@property (nonatomic, retain, readwrite) IBOutlet UISegmentedControl * segmentedControl;
@property (nonatomic, retain, readwrite) UIViewController            * activeViewController;
@property (nonatomic, retain, readwrite) NSArray                     * segmentedViewControllers;

- (void)didChangeSegmentControl:(UISegmentedControl *)control;
- (NSArray *)segmentedViewControllerContent;

@end

In an anonymous category we redefine the interface properties as readwrite for local accessor/mutator methods, and define method signatures for a callback from the segmented control, and a helper method to create the view controllers representing each segment.

@implementation SegmentManagingViewController

@synthesize segmentedControl, activeViewController, segmentedViewControllers;

- (void)viewDidLoad {
    [super viewDidLoad];

    self.segmentedViewControllers = [self segmentedViewControllerContent];

    NSArray * segmentTitles = [self.segmentedViewControllers arrayByPerformingSelector:@selector(title)];

    self.segmentedControl = [[UISegmentedControl alloc] initWithItems:segmentTitles];
    self.segmentedControl.selectedSegmentIndex = 0;
    self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;

    [self.segmentedControl addTarget:self
                              action:@selector(didChangeSegmentControl:)
                    forControlEvents:UIControlEventValueChanged];

    self.navigationItem.titleView = self.segmentedControl;
    [self.segmentedControl release];

    [self didChangeSegmentControl:self.segmentedControl]; // kick everything off
}

- (NSArray *)segmentedViewControllerContent {
    UIViewController * controller1 = [[ItalyViewController alloc] initWithParentViewController:self];
    UIViewController * controller2 = [[AustraliaViewController alloc] initWithParentViewController:self];

    NSArray * controllers = [NSArray arrayWithObjects:controller1, controller2, nil];

    [controller1 release];
    [controller2 release];

    return controllers;
}

viewDidLoad creates our UISegmentedControl object within the navigation controller title, and installs a target/action pair to call back on didChangeSegmentControl: when the selected segment index changes. It also calls upon segmentedViewControllerContent to return an array containing the view controllers we’ll be toggling between. In this case, view controllers representing Italy (fantastic holiday a few years back) and Australia, where I come from.

We’ll also implement our memory management methods:

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

    for (UIViewController * viewController in self.segmentedViewControllers) {
        [viewController didReceiveMemoryWarning];
    }
}

- (void)viewDidUnload {
    self.segmentedControl         = nil;
    self.segmentedViewControllers = nil;
    self.activeViewController     = nil;

    [super viewDidUnload];
}

Now onto the beef, when a segment index change occurs, the UISegmentedControl will call back to our didChangeSegmentControl: method, where we can interrogate the segmented control for the new index.

When this changes we need to remove the current active subview from the view hierarchy, and replace it with a new one according to the users segmented control selection.

Since we’re also manipulating the view hierarchy directly, we also need to fire viewWillDisappear:/viewDidDisappear: and their counterparts viewWillAppear:/viewDidAppear: appropriately as well to ensure the outbound and inbound view controllers are notified of their view’s visual status change:

- (void)didChangeSegmentControl:(UISegmentedControl *)control {
    if (self.activeViewController) {
        [self.activeViewController viewWillDisappear:NO];
        [self.activeViewController.view removeFromSuperview];
        [self.activeViewController viewDidDisappear:NO];
    }

    self.activeViewController = [self.segmentedViewControllers objectAtIndex:control.selectedSegmentIndex];

    [self.activeViewController viewWillAppear:NO];
    [self.view addSubview:self.activeViewController.view];
    [self.activeViewController viewDidAppear:NO];

    NSString * segmentTitle = [control titleForSegmentAtIndex:control.selectedSegmentIndex];
    self.navigationItem.backBarButtonItem  = [[UIBarButtonItem alloc] initWithTitle:segmentTitle style:UIBarButtonItemStylePlain target:nil action:nil];
}

@end

The final part extracts the title of the selected segment, and creates a ‘back’ UIBarButtonItem with it’s name matching that title. This ensures that when we push onto the navigation stack from within one of these subviews, the navigation item back button matches the name of the selected segment.

We also pass on any view controller life cycle methods to the active subview:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.activeViewController viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self.activeViewController viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.activeViewController viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.activeViewController viewDidDisappear:animated];
}

Navigation Controllers

Interestingly, if we place this managing view controller within a UINavigationController, the managing view controller won’t actually receive the viewWillAppear:/viewDidAppear: events from the system. To be notified of when this occurs inside a navigation view hierarchy, we need to implement the UINavigationControllerDelegate methods to be informed when a view has been pushed on or off the navigation stack.

Without these methods bizarre side effects can occur, such as UITableView’s within a segments subview not knowing when to appropriately de-highlight the selected row.

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [viewController viewDidAppear:animated];
}

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [viewController viewWillAppear:animated];
}

Pushing onto the navigation stack

The final piece is to allow pushing onto the navigation stack from within one of the managed subviews.

Each subview’s UIViewController has an implicit navigationController property that you can use to send the pushViewController:animated: message to add an additional view controller to the navigation hierarchy.

In our case though, since each subview’s view controller has been instantiated outside of the navigation hierarchy, their navigationController reference will be nil.

The other observation is that we don’t actually want to push onto the navigation stack from within the subview – we want to push onto the navigation stack from the managing view controller.

The solution to this, is to pass the managing view controller to the subviews, to correctly allow pushing onto the navigation stack from within the subview. There’s several ways to do this, in the code above, I’ve defined a custom view controller initializer that accepts a managing view controller reference.

Conclusion

What I particularly like about this approach is that it separates the code and behaviour of each subview into separate view controllers, and assembles them together in a neat and compact manner.

Separate view controllers follow Apple’s single screen full of content per view controller paradigm, and pushing onto the navigation controller via the managing view controller yields a comfortable user experience.

An example XCode project of all this in action is also available if you’d like to step through the details, enjoy.

Updated

  • Example uploaded to github
  • Added pass of didReceiveMemoryWarning thanks to Jonah Williams

Scrolling with UIScrollView

posted by crafterm, 23 May 2010

Just about every iPhone/iPad application needs them, scrollable views to let your users pan and/or zoom over more content than can be shown on one screen at a time.

There’s several approaches out there for making your content scrollable with a UIScrollView, some of them quite complex with overlapping views in Interface Builder, others recommending heavy use of code for layout.

The method I’ve found the quite effective of late in terms of code and ease of design in Interface Builder, is to add a UIScrollView to your UIViewController subclass' UIView, and create a separate UIView in your XIB, where the underlying full content can be added. Upon viewDidLoad, you can add this UIView to the UIScrollView’s subview property, and set the contentSize and zoom properties appropriately.

Here’s the steps:

Add UIScrollView to your UIViewController subclass' UIView

Code

Define the scroll view in your class interface and synthesize the property in the implementation.

@interface MyViewController : UIViewController {
    UIScrollView * scrollView;
}

@property (nonatomic, retain) IBOutlet UIScrollView * scrollView;

@end

Interface Builder

Add the UIScrollView to your XIB, connecting it to your view controller via the outlet. The UIScrollView can be placed within any other decorations on the view such as navigation bars, tool/tab bars.

Create a separate UIView for your full content

Add an additional property for the view that will hold the content to be added to the scroll view.

Code

@interface MyViewController : UIViewController {
    UIScrollView * scrollView;
    UIView       * contentView;
}

@property (nonatomic, retain) IBOutlet UIScrollView * scrollView;
@property (nonatomic, retain) IBOutlet UIView       * contentView;

@end

Interface Builder

Create the view in interface builder, and connect it your contentView outlet. This view doesn’t have to be within standard iPhone view dimensions, it can be of any size, since the scroll view will allow us to pan over it. Add all the content you wish the user to be able to pan over to this view.

Configure the UIScrollView to pan over your content

Add the contentView as a sub view of the scroll view, and set the contentSize property on the scroll view to be the bounds of the content view, or if you have any dynamic text/etc, calculate the height appropriately.

Code

@synthesize scrollView, contentView;

- (void)viewDidLoad {
    [super viewDidLoad];

    // set the scrollview content and configure appropriately
    [self.scrollView addSubview:self.contentView];
    self.scrollView.contentSize = self.contentView.bounds.size;
}

Additionally, if you’d like pinch zooming, you can set the maximumZoomScale/minimumZoomScale properties, and viewForZoomingInScrollView: in your view controller as the UIScrollView delegate.

Also, let’s not forget to be good memory management citizens and release both the scrollview and contentview properties in viewDidUnload (iPhone 3.x), or didReceiveMemoryWarning (iPhone OS 2.x).

Code

- (void)viewDidUnload {
    self.scrollView  = nil;
    self.contentView = nil;

    [super viewDidUnload];
}

Summary

I find this pattern useful for several reasons, mainly that it keeps the design and dimensions of content view separate from view that contains the UIScrollView, and there’s less complexity within the XIB.

It doesn’t force any Interface Builder pain with overlapping views or magic code to add scrolling to an existing view, and works well when you’d like to retrofit a UIScrollView into an existing XIB content.

The XCode project used to build this article and screenshots, etc, is also available.

Updated:

  • Thanks to Nathan de Vries for mentioning memory management.
  • Example uploaded to github

Getting Started with MacRuby

posted by crafterm, 01 September 2009

For the those dual personality developers like myself who love Ruby, Rails and other awesome Ruby tools, but also get a big kick out of Mac and iPhone development, MacRuby is a really exciting project worth taking a look at. Originally a port of Ruby 1.9 to the Cocoa/Foundation frameworks under Mac OSX, MacRuby is now a fully fledged Ruby environment, with an LLVM based interpreter and DSL driven UI toolkit.

MacRuby is still under active development, and isn’t finished yet, but it’s Ruby and standard library compatibility is already quite impressive.

From the perspective of Ruby development, MacRuby can be used to build and run your Ruby and when supported Rails applications. From the perspective of a Mac applications developer, MacRuby can be used to build fully-fledged OSX desktop applications that interface with all the usual Mac development frameworks such as Core Data, Core Image and Cocoa, etc. User interfaces can be designed using Interface Builder, or using HotCocoa, a Ruby based DSL for describing Cocoa based UI’s.

Technically, MacRuby is implemented using the Objective-C common runtime and garbage collector, and the Core Foundation framework. This means that under MacRuby, Ruby objects are NSObjects (eg. a Ruby String is a NSString, likewise Ruby Hashes are NSDictionary’s, without any bindings or conversion required), Ruby classes are Objective-C classes, fully interoperable and interchangeable with each other, and instead of using the Ruby 1.9 garbage collector, the Objective-C 2.0 garbage collector is in full effect.

Most recently with MacRuby 0.5+ and the current development version of MacRuby, the YARV based interpreter has been removed and a new LLVM code generating interpreter has been implemented for performance and further optimisations down the track.

In this particular post I’ll work through the installation of MacRuby on your system, several follow up posts will discuss developing applications using MacRuby for Ruby and OSX desktop developers.

Installing

To get MacRuby running on your machine you currently need to build and install it from source. The build process is straightforward, but does take a bit of time. I’ll also assume you have the latest version of XCode installed. Here’s what you need to do.

Building LLVM

LLVM is required to build the latest MacRuby source, in particular a specific revision of LLVM for compatibility reasons, so even if you have it installed already as part of Snow Leopard or Ports, you might need to build it again. The particular revision required is 89156.

LLVM is hosted at http://www.llvm.org in Subversion, however personally I found it much easier to check out using a git mirror of the repository rather than attempt it from SVN (which for me took well over an hour just for the checkout that failed before completion resulting in a hosed source tree).

To checkout LLVM using git, and create a branch you can build, based off the right SVN revision number, perform the following commands:

$> git clone git://repo.or.cz/llvm.git
$> cd llvm
$> git checkout -b macruby-reliable 39a0f07ef82b6cc70ce87c038620921d87297ced
$> env UNIVERSAL=1 UNIVERSAL_ARCH="i386 x86_64" CC=/usr/bin/gcc CXX=/usr/bin/g++ ./configure --enable-bindings=none --enable-optimized --with-llvmgccdir=/tmp
$> env UNIVERSAL=1 UNIVERSAL_ARCH="i386 x86_64" CC=/usr/bin/gcc CXX=/usr/bin/g++ make
$> sudo env UNIVERSAL=1 UNIVERSAL_ARCH="i386 x86_64" CC=/usr/bin/gcc CXX=/usr/bin/g++ make install

Note that 39a0f07ef82b6cc70ce87c038620921d87297ced above corresponds to the git commit hash of svn commit 89156, you can tell this by looking at the output of git log and searching for 89156 in the git-svn-id section of the log message.

Alternatively, if you really need to check out llvm using Subversion, you can use the following command:

svn co -r 89156 https://llvm.org/svn/llvm*project/llvm/trunk llvm

Compiling and installing LLVM will take a while (45 minutes or more depending on your system), feel free to grab a cup of coffee – I had a nice Cappuccino:

2719.14 real      2429.54 user       183.13 sys

Building MacRuby

After installing LLVM you can now go ahead and build MacRuby itself. MacRuby now has an official git repository you can use to check out the source from. There is also an SVN repository however I’ll always favour git over Subversion:

$> git clone git://git.macruby.org/macruby/MacRuby.git
$> cd MacRuby
$> rake
....
/usr/bin/install -c -m 0755 libyaml.bundle /Library/Frameworks/MacRuby.framework/Versions/0.6/usr/lib/ruby/site_ruby/1.9.0/universal-darwin9.0
cd ext/fcntl
/usr/bin/make top_srcdir=../.. ruby="../../miniruby -I../.. -I../../lib" extout=../../.ext hdrdir=../../include arch_hdrdir=../../include install
/usr/bin/install -c -m 0755 fcntl.bundle /Library/Frameworks/MacRuby.framework/Versions/0.6/usr/lib/ruby/site_ruby/1.9.0/universal-darwin9.0
cd ext/zlib
/usr/bin/make top_srcdir=../.. ruby="../../miniruby -I../.. -I../../lib" extout=../../.ext hdrdir=../../include arch_hdrdir=../../include install
/usr/bin/install -c -m 0755 zlib.bundle /Library/Frameworks/MacRuby.framework/Versions/0.6/usr/lib/ruby/site_ruby/1.9.0/universal-darwin9.0
./miniruby instruby.rb --make="/usr/bin/make" --dest-dir="" --extout=".ext" --mflags="" --make-flags="" --data-mode=0644 --prog-mode=0755 --installed-list .installed.list --mantype="doc" --sym-dest-dir="/usr/local"
installing binary commands
installing command scripts
installing library scripts
installing headers
installing manpages
installing data files
installing extension objects
installing extension scripts
installing Xcode templates
installing Xcode 3.1 templates
installing samples
installing framework
installing IB support
$> sudo rake install

After installing you can test your MacRuby build by using the bundled spec suite:

$> rake spec:ci

Please note that this installs the latest version of MacRuby under development which is a moving target, however being on the bleeding edge means you can update to the latest source at any time with the latest features and fixes, and makes it much easier for contributing back to the project with patches, etc.

Later, if you’d like to update your installation, just return to the source directory, update your source from the git repository and reinstall.

$> git pull origin master
$> rake
$> sudo rake install

MacRuby Usage

Now that you have MacRuby installed, you can start using it. gems, ri, irb, etc, are all provided as part of your MacRuby installation with a mac* prefix so they don’t conflict with your existing MRI based installation (eg. macri, macirb, macgem, etc). In addition to this further interesting extensions such as XCode templates are included to get started building graphical Cocoa based apps using Interface Builder, etc.

$> macruby -v
MacRuby version 0.6 (ruby 1.9.0) [universal-darwin10.0, x86_64]

$> macirb
irb(main):001:0> puts "Hello World"
Hello World
=> nil
irb(main):002:0>

Summary

We’ve stepped through the installation of the latest version of MacRuby on your system, including it’s primary dependency LLVM, and described how to familiarise yourself with its environment. In future posts, I’ll write further about how to get started creating Cocoa applications using Mac OSX technologies such as XIBs, Bindings, Core Data, and Core Image with your apps, and also how to use HotCocoa, the nice and concise Ruby DSL for building OSX UI’s in Ruby.

If you can’t wait till then there’s a few further resources I’d recommend taking a look at:

  1. The MacRuby site
  2. The MacRuby Example Applications
  3. Rich Kilmer’s MacRuby and HotCocoa presentation from the Ruby on OSX conference in Amsterdam

There’s also @macruby on Twitter where regular updates are posted, an IRC channel, and two mailing lists. I also follow the Github mirrors to see each commit that’s made to the MacRuby source, together with all the other projects I’m actively following.

Looking forward to writing more MacRuby posts in the future, enjoy MacRuby!

  • Updated to include newer LLVM revisions and MacRuby 0.6 release (25th May 2010)