Blogroll

A question that I have seen pop up a few times is how to build a Wizard in Cocoa. Having thought about that question a bit I realized that a better answer to it is — how to build a wizard in Cocoa using Core Animation.

The basic concept behind this project is to present a window to the user that will walk them through a list of options. To accomplish this task, I created a number of NSViews that will be presented to the user in order so that they can make the decisions needed. To make this a little easier, I have extended NSView to create MSZLinkedView. The added functionality in this subclass is that the view has a reference to the previous and next views in the wizard. These references are set in interface builder directly so that I do not have to worry about them in code.

There is not much to this class. The primary purpose is to keep a reference to its place in the linked list. But since I have the subclass I have added the core animation setWantsLayer: call into the awakeFromNib and it also checks to see if it has a previous and/or next view to disable the appropriate buttons. This NSView subclass will be used to build the different “pages” of the wizard.

Building the XIB/NIB

The next step in building the wizard is to lay out the views in Interface Builder. In the sample project, I have created three views: first, second and third. Each of these views has a next button, previous button and a custom box with a different color. If you build and run the project you can flip through these three and the appropriate button will be disabled.

Each view links to the next view, the previous view, and its own next and previous button. All of these links will occur before the awakeFromNib is called on each view so that the buttons will be disabled as appropriate. In an actual application I would have put in text fields, radio buttons, pictures, etc on each “slide” but in this case I only put in a simple box with a custom design. This will allow the user to see the slides clearly.

The application delegate in this example holds a reference to the main window and only the first selected view. Because the views are in a linked list, the AppDelegate does not care about the other views at all. That is responsibility of the list. There is one little catch though. When the nib is being loaded, the current view will be referenced before the awakeFromNib is called. To get the view added as a subview I need to add it in the awakeFromNib. This will set the stage for the wizard.

AppDelegate.h

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

@interface AppDelegate : NSObject{

__weakIBOutletNSWindow*window;

__weakIBOutletMSZLinkedView*currentView;

__weakCATransition*transition;

}

@property(retain)NSWindow*window;

@property(retain)MSZLinkedView*currentView;

-(IBAction)nextView:(id)sender;

-(IBAction)previousView:(id)sender;

@end

Method breakdown of the AppDelegate

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

-(void)awakeFromNib

{

NSView*contentView=[[selfwindow] contentView];

[contentView setWantsLayer:YES];

[contentView addSubview:[selfcurrentView]];

transition=[CATransitionanimation];

[transition setType:kCATransitionPush];

[transition setSubtype:kCATransitionFromLeft];

NSDictionary*ani=[NSDictionary dictionaryWithObject:transition

forKey:@"subviews"];

[contentView setAnimations:ani];

}

awakeFromNib

The awakeFromNib first turns on Core Animation for the content view of the window. It then adds the currently referenced “currentView” as a subview of the contentView. Since we have already set up the “currentView” to have a frameOrigin of 0,0 I do not need to worry about setting the subview’s location.

Next I build a CATransition animation. Note that I have kept a reference to this animation as an ivar in the AppDelegate. The reason for that will become apparent later. Once the animation is built, I add it as the transition animation for the key “subviews” in the content view. This transition animation will trigger whenever a subview is added, removed or replaced.

Objective-C

1

2

3

4

5

6

7

8

9

10

-(void)setCurrentView:(MSZLinkedView*)newView

{

if(!currentView){

currentView=newView;

return;

}

NSView*contentView=[[selfwindow] contentView];

[[contentView animator] replaceSubview:currentView with:newView];

currentView=newView;

}

setCurrentView:(MSZLinkedView*)view

The reason that I overrode this setter method is a matter of convenience. Since I only ever wanted one view showing at any time, I can comfortably swap out the views whenever the setter for “currentView” is called. I could do this in other places to the same effect but since I want to keep track of the current view it is ideal to handle both at the same time. Therefore, whenever I want to switch the subviews I can pass the new view into the setCurrentView method and be done with it.

Objective-C

1

2

3

4

5

6

7

8

9

10

11

12

13

-(IBAction)nextView:(id)sender;

{

if(![[selfcurrentView] nextView])return;

[transition setSubtype:kCATransitionFromRight];

[self setCurrentView:[[selfcurrentView] nextView]];

}

-(IBAction)previousView:(id)sender;

{

if(![[selfcurrentView] previousView])return;

[transition setSubtype:kCATransitionFromLeft];

[self setCurrentView:[[selfcurrentView] previousView]];

}

nextView:(id)sender previousView:(id)sender

Both of these methods perform the same function. In each method I grab a reference to the current view and check that its previous/next is not nil. This is only protective since each view should have already disabled the buttons that don’t have a link.

Once that check is complete, I simply call setCurrentView: passing it the appropriate view — either the next or previous based on the method called. The setCurrentView: discussed above takes care of the rest. Just before I change the view, however, I change the subtype for the transition. This is the reason I keep a reference to the transition around. I do this so that visually the transitions make sense. The previous view comes in from the left and the next view comes in from the right. While this step is not necessary, it does give a nicer spacial feel to the animation.

Conclusion

So where is the magic? What makes the Core Animation transition happen? All of that is in the AppDelegate’s awakeFromNib. Simply setting the CATransition on the content view of the window takes care of all the animation. The rest of the code is just to handle all of the switching of the subview. You can play with the transitions very easily by change the type and subtype being set in the awakeFromNib of the AppDelegate.

Comments

[…] Cocoa Is My Girlfriend: “A question that I have seen pop up a few times is how to build a Wizard in Cocoa. Having thought about that question a bit I realized that a better answer to it is—how to build a wizard in Cocoa using Core Animation.” […]

[…] Marcus can really crank these articles out. His latest demonstrates how to create a Wizard interface with next and previous buttons, however, this is not your average every day Wizard interface. This one employs Core Animation. When you click the next or previous buttons, you’ll see that the next view slides in from the right or left depending on which you clicked. Take a look at his latest tutorial post: Core Animation Tutorial: Wizard Dialog with Transitions. […]

[…] Cocoa Is My Girlfriend: “A question that I have seen pop up a few times is how to build a Wizard in Cocoa. Having thought about that question a bit I realized that a better answer to it is—how to build a wizard in Cocoa using Core Animation.” […]

Only problem: If you add a label to the MSZLinkedView you can easily see that the size of the view is somehow not correct. Adding an editable NSTextField makes matters even worse. Is there any cure for that?

Hey,
I am wondering how I would extend this example to allow a choice/path. For example, right now it starts out with the First, which is wired to the Second. What is on the First view there was a Radio button, and depending on the users choice you would get either the Second or Third? How might that work?

Ah, okay I get it. I was able to add a radio button array to make the choice, and I added another MSZLinkedView called alternateView. This was hooked up in interface builder to the Third NSView, and then in the code, I just put in an if statement for the nextView action. Seems to work just fine.

Now I just need to figure out how to make this all work in Tiger and I will be on my way. Thanks for a great article, and a great site!

Adding some NSTextField or NStableView in place of the colored boxes ruins the animation. Some kind of ghost white rects stick into place while the actual contents of the view animates in or out. At the animation’s normal duration, it flickers, if you slow down the animation, you can easily see them. Can you confirm you’re having them too?

Like wasserturm I’d like to know if there’s something to do with that… Even 2 years after this post the problem remains.

I usually use a big view inserted in a smaller one which masks the unwanted parts and I animate the big view right or left with the animator as needed. It’s of course VERY painful, especially when you’ve got to calculate the various frame positions to pass to the [bigView.animator setFrame:…] method and when you happen to have to change something to the big view and have to unembed all subviews and start from scratch… :/

Same here – embedding controls in a view and animating that view as a subview distorts all controls in that view.
Almost as if the subview is slightly scaled up/down, i.e. a 500×500 view is displayed/scaled to a 496/496 or 502/502..
Any ideas..?