原文地址: http://www.raywenderlich.com/1040/ipad-for-iphone-developers-101-uisplitview-tutorial
This is the first part of a three part series to help get iPhone Developers up-to-speed with iPad development by focusing on three of the most interesting new classes (at least to me): UISplitView, UIPopoverController, and Custom Input Views.
We will make an iPad app from scratch that makes use of all of these new capabilities. The app will display a list of monsters from the Cocos2D game I recently released, and let you change the color of the label using a popover view. Finally, you’ll be able to change your favorite way of killing each monster by using a custom input view.
By the end, you’ll have a good handle of some of the most important new features of iPad development and will be able to hit the ground running!
In iPad development, it would rarely make sense to have a full-screen UITableView like we do so often in iPhone programming – there’s just too much space. To make better use of that space, the Split View comes to the rescue!
The Split View lets you carve up the iPad screen into two sections and display a view controller in each side. It’s typically used to have navigation on the left hand side, and a detail view on the right hand side.
So let’s start making this!
Although we could use the “Split View-based Application” template as a starting point, we’re going to start from scratch and pick the “Window-based Application” template. This is so we can get a better understanding of exactly how the UISplitView works. This knowledge will be helpful as we continue to use it in future projects (even if we choose the split view template in the future to save time!)
So create a new Project, and choose the “Window-based Application” template. Make sure to choose “iPad” in the Product dropdown – Universal Apps is a topic for another day.
Name your project “MathMonsters.” You can go ahead and compile and run if you want, but all you’ll see is a blank screen at this point.
Go ahead and open up MainWindow.xib. You’ll see a huge window the size of the iPad screen. Go ahead and add your Split View Controller by dragging it from the library into the MainWindow.xib.
Note that there are two elements underneath the Split View Controller. The first element is the view that appears on the left hand side – and in this case it’s a UINavigationController with a table view inside. The second element is the view that appears on the right hand side – and in this case it’s a plain UIViewController.
We’re going to want to have our own view controllers inside of here instead of the ones that are put in there by default. So let’s take a quick break to get some placeholders for those, and then we’ll come back here.
We’re going to make two view controller placeholders to put inside our split view – a table view controller, and a “plain” view controller.
Let’s start with the table view controller. Go to File/New, choose UIViewController subclass, make sure “Targeted for iPad” and “UITableViewController subclass” are checked, and click “Next”. Name the file “LeftViewController” and click “Finish”.
Scroll down to “numberOfSectionsInTableView” and replace “number of sections” with “1″. Similarly, scroll down to “numberOfRowsInSection” and replace “number of rows in section” with “10″.
This way, we’ll just have 10 empty rows to look at when we test this thing out later.
Next, let’s make the placeholder for the right hand side. Go to File/New, choose UIViewController subclass, make sure “Targeted for iPad” and “With XIB for user interface” are checked and “UITableViewController subclass” is NOT checked, and click “Next”. Name the file “RightViewController” and click “Finish”.
Then double click RightViewController.xib and drag a label into the middle, and make it say “Hello, World!” or something so we know it’s working when we test it out later.
Ok that should do it! Now back to MainWindow.xib.
Now that we have our placeholder view controllers in place, let’s hook them up.
Inside MainWindow.xib, expand the tree for the Split View Controller until you see the entry that reads “Table View Controller (Root View Controller)”. Click on that row, then go to the fourth tab in the inspector and change the Class to “LeftViewController”.
Next, click on the row right underneath in MainWindow.xib that reads “View Controller”. Similarly to above, in the fourth tab in the inspector change the Class to “RightViewController.” Also, in the first tab in the inspector change the NIB name to “RightViewController.”
Ok – we’ve made our split view controller, now we just need to add it to our window! To do this, we’ll need to make an outlet for the UISplitViewController. While we’re at it, we’ll make some outlets for our left/right View Controllers as well.
Open up MathMonstersAppDelegate.h and modify it to look like the following:
#import <UIKit/UIKit.h> @class LeftViewController; @class RightViewController; @interface MathMonstersAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window; UISplitViewController *_splitViewController; LeftViewController *_leftViewController; RightViewController *_rightViewController; } @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet UISplitViewController *splitViewController; @property (nonatomic, retain) IBOutlet LeftViewController *leftViewController; @property (nonatomic, retain) IBOutlet RightViewController *rightViewController; @end |
Note all we did here was declare a few outlets.
Then open up MathMonstersAppDelegate.m and modify it to look like the following:
#import "MathMonstersAppDelegate.h" @implementation MathMonstersAppDelegate @synthesize window; @synthesize splitViewController = _splitViewController; @synthesize leftViewController = _leftViewController; @synthesize rightViewController = _rightViewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions: (NSDictionary *)launchOptions { // Override point for customization after application launch [window addSubview:_splitViewController.view]; [window makeKeyAndVisible]; return YES; } - (void)dealloc { [window release]; self.splitViewController = nil; self.leftViewController = nil; self.rightViewController = nil; [super dealloc]; } @end |
Again, no big surprises here – we just synthesized our outlets, did some memory cleanup, and added the split view controller to the main window.
Ok – make sure those files are saved, and head back to MainWindow.xib. Control-drag from “Math Monsters App Delegate” to the “Split View Controller”, “Left View Controller”, and “Right View Controller”, connecting each to the appropriate outlet.
That’s it! Compile and run the app, and you’ll see your “Hello, World!” UISplitView.
The next thing we need to do is define a model for the data we want to display. I was tempted to use Core Data for this since we just had a tutorial series on the matter, but didn’t want to complicate things so we’re going with a simple model with no data persistence.
So anyway – let’s make a class representing the Monsters we want to display. Go to File/New, choose Objective-C class, make sure “Subclass of” is “NSObject”, and click “Next”. Name the file “Monster” and click “Finish”.
We’re just going to create a simple class with some member variables with attributes about each Monster we want to display. Replace Monster.h with the following contents:
#import <Foundation/Foundation.h> typedef enum { Blowgun = 0, NinjaStar, Fire, Sword, Smoke, } Weapon; @interface Monster : NSObject { NSString *_name; NSString *_descr; NSString *_iconName; Weapon _preferredWayToKill; } @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *descr; @property (nonatomic, copy) NSString *iconName; @property (nonatomic, assign) Weapon preferredWayToKill; - (Monster *)initWithName:(NSString *)name descr:(NSString *)descr iconName:(NSString *)iconName preferredWayToKill:(Weapon)preferredWayToKill; @end |
And Monster.m with the following:
#import "Monster.h" @implementation Monster @synthesize name = _name; @synthesize descr = _descr; @synthesize iconName = _iconName; @synthesize preferredWayToKill = _preferredWayToKill; - (Monster *)initWithName:(NSString *)name descr:(NSString *)descr iconName:(NSString *)iconName preferredWayToKill:(Weapon)preferredWayToKill { if ((self = [super init])) { self.name = name; self.descr = descr; self.iconName = iconName; self.preferredWayToKill = preferredWayToKill; } return self; } - (void) dealloc { self.name = nil; self.descr = nil; self.iconName = nil; [super dealloc]; } @end |
That’s it for defining the model – so let’s hook it up to our left side view!
Open up LeftViewController.h and add a new member variable/property for a monsters arary like the following:
#import <UIKit/UIKit.h> @interface LeftViewController : UITableViewController { NSMutableArray *_monsters; } @property (nonatomic, retain) NSMutableArray *monsters; @end |
Then add a few tweaks to LeftViewController.m to start using this new array:
// At top, under #import #import "Monster.h" // Under @implementation @synthesize monsters = _monsters; // In numberOfRowsInSection, replace return 10 with: return [_monsters count]; // In cellForRowAtIndexPath, after "Configure the cell..." Monster *monster = [_monsters objectAtIndex:indexPath.row]; cell.textLabel.text = monster.name; // In dealloc self.monsters = nil; |
That’s it for the table view. Now let’s just populate it with some default monsters!
First, download some art from my most recent app. Drag the images into your Resources folder, make sure “Copy items into destination group’s folder (if needed)” is checked, and click “Add.”
Then open up MathMonstersAppDelegate.m and add the following code:
// At top, under #import #import "Monster.h" #import "LeftViewController.h" // At top of application didFinishLaunchingWithOptions: NSMutableArray *monsters = [NSMutableArray array]; [monsters addObject:[[[Monster alloc] initWithName:@"Cat-Bot" descr:@"MEE-OW" iconName:@"meetcatbot.jpg" preferredWayToKill:Sword] autorelease]]; [monsters addObject:[[[Monster alloc] initWithName:@"Dog-Bot" descr:@"BOW-WOW" iconName:@"meetdogbot.jpg" preferredWayToKill:Blowgun] autorelease]]; [monsters addObject:[[[Monster alloc] initWithName:@"Explode-Bot" descr:@"Tick, tick, BOOM!" iconName:@"meetexplodebot.jpg" preferredWayToKill:Smoke] autorelease]]; [monsters addObject:[[[Monster alloc] initWithName:@"Fire-Bot" descr:@"Will Make You Steamed" iconName:@"meetfirebot.jpg" preferredWayToKill:NinjaStar] autorelease]]; [monsters addObject:[[[Monster alloc] initWithName:@"Ice-Bot" descr:@"Has A Chilling Effect" iconName:@"meeticebot.jpg" preferredWayToKill:Fire] autorelease]]; [monsters addObject:[[[Monster alloc] initWithName:@"Mini-Tomato-Bot" descr:@"Extremely Handsome" iconName:@"meetminitomatobot.jpg" preferredWayToKill:NinjaStar] autorelease]]; _leftViewController.monsters = monsters; |
Compile and run the app, and if all goes well you should now see the list of bots on the left hand side!
Next, let’s set up the right hand side so we can see the details for a particular bot.
Open up RightViewController.xib and delete the placeholder label you put down earlier.
To help make layout easier, you may wish to select “Layout/Show Layout Rectangles”. Then drag the following controls onto the screen.
The next step is to make sure we set up the auto-resizing behaviour properly. This is especially important for the iPad since apps are required to handle autorotation properly for any orientation.
In our case, there are two views that we want to dynamically resize based on the size of the view: the top two labels. So click on those labels, go to the third tab in the Inspector and set the auto-resizing options as follows:
That way they will grow/shrink on the right hand side based on how much space is available for the view.
Ok that’s it for screen layout for now – let’s hook these up to some class outlets. Add some outlets to RightViewController.h like the following:
#import <UIKit/UIKit.h> @class Monster; @interface RightViewController : UIViewController { Monster *_monster; UILabel *_nameLabel; UILabel *_descrLabel; UIImageView *_iconView; UIImageView *_weaponView; } @property (nonatomic, retain) Monster *monster; @property (nonatomic, retain) IBOutlet UILabel *nameLabel; @property (nonatomic, retain) IBOutlet UILabel *descrLabel; @property (nonatomic, retain) IBOutlet UIImageView *iconView; @property (nonatomic, retain) IBOutlet UIImageView *weaponView; @end |
Here we added member variables for the various UI elements we just added (that need to dynamically change). Note we also added a member variable for the monster this view controller should display.
While we’re at it, add the following code to RightViewController.m to display the information from the monster:
// At top, under #import #import "Monster.h" // Under @implementation @synthesize monster = _monster; @synthesize nameLabel = _nameLabel; @synthesize descrLabel = _descrLabel; @synthesize iconView = _iconView; @synthesize weaponView = _weaponView; // In didReceiveMemoryWarning self.nameLabel = nil; self.descrLabel = nil; self.iconView = nil; self.weaponView = nil; // In dealloc self.monster = nil; self.nameLabel = nil; self.descrLabel = nil; self.iconView = nil; self.weaponView = nil; // New method - (void)refresh { _nameLabel.text = _monster.name; _iconView.image = [UIImage imageNamed:_monster.iconName]; _descrLabel.text = _monster.descr; if (_monster.preferredWayToKill == Blowgun) { _weaponView.image = [UIImage imageNamed:@"blowgun.jpg"]; } else if (_monster.preferredWayToKill == Fire) { _weaponView.image = [UIImage imageNamed:@"fire.jpg"]; } else if (_monster.preferredWayToKill == NinjaStar) { _weaponView.image = [UIImage imageNamed:@"ninjastar.jpg"]; } else if (_monster.preferredWayToKill == Smoke) { _weaponView.image = [UIImage imageNamed:@"smoke.jpg"]; } else { _weaponView.image = [UIImage imageNamed:@"sword.jpg"]; } } // Another new method - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [self refresh]; } |
Now, go back to RightViewController.xib and control-drag from “File’s Owner” to each of the controls to hook them up to their corresponding outlets.
Ok, we’re close enough to test this out! Go back to MathMonstersAppDelegate.m and make the following tweaks:
// Up top, under #import #import "RightViewController.h" // Inside application didFinishLaunchingWithOptions, // right after setting monsters on leftViewController... _rightViewController.monster = [monsters objectAtIndex:0]; |
Compile and run the app, and if all goes well you should see some monster details on the right:
Note that selecting the monsters on the right does absolutely nothing, however. That is what we’ll do next!
Time to play matchmaker and hook these two sides together.
There are many different strategies for how to best accomplish this. In the Split View Application template they give the left view controller a pointer to the right view controller, and the left view controller sets a property on the right view controller when a row gets selected. The right view controller overrides the property to update the view when the property is updated.
That works fine, but we’re going to follow the approach suggested in the UISplitViewController class reference here – use delegates.
The basic idea is we’re going to define a protocol with a single method – “selectedBotChanged.” Our right hand side will implement this method, and our left hand side will accept a delegate of somebody who wants to know about this.
So let’s see this in code. Go to File/New, choose Objective-C class, make sure “Subclass of” is “NSObject”, and click “Next”. Name the file “MonsterSelectionDelegate” and click “Finish”.
Replace MonsterSelectionDelegate.h with the following:
#import <Foundation/Foundation.h> @class Monster; @protocol MonsterSelectionDelegate - (void)monsterSelectionChanged:(Monster *)curSelection; @end |
Then delete MonsterSelectionDelegate.m, since we don’t need it for a protocol.
Now, let’s modify the LeftViewController to take a MonsterSelectionDelegate. Make the following changes to LeftViewController.h:
// At top, under #import #import "MonsterSelectionDelegate.h" // Inside LeftViewController id<MonsterSelectionDelegate> _delegate; // Under @property @property (nonatomic, assign) id<MonsterSelectionDelegate> delegate; |
And the following changes to LeftViewController.m:
// Under @implementation @synthesize delegate = _delegate; // Inside didSelectRowAtIndexPath, remove commented stuff and put: if (_delegate != nil) { Monster *monster = (Monster *) [_monsters objectAtIndex:indexPath.row]; [_delegate monsterSelectionChanged:monster]; } // Inside dealloc self.delegate = nil; |
Then, we’ll modify RightViewController to implement the delegate. Make the following changes to RightViewController.h:
// At top, under #import #import "MonsterSelectionDelegate.h" // Class declaration line @interface RightViewController : UIViewController <MonsterSelectionDelegate> { |
Then add the following to RightViewController.m:
- (void)monsterSelectionChanged:(Monster *)curSelection { self.monster = curSelection; [self refresh]; } |
And for the final step, make the following modification to MathMonstersAppDelegate.m inside didFinishLaunchingWithOptions right after the last line we added:
_leftViewController.delegate = _rightViewController; |
That’s it! Compile and run the app, and you now should be able to select between the monsters like the following:
In case you’re wondering what the advantages are for using delegates like we just did, note that there are no imports between the left side view and the right side view. This means that it would be a lot easier to re-use these views in other projects or swap out the views with other views, since we’ve been quite clear about what each one expects.
So far so good with split views! Except there’s one problem left – if we rotate the phone to vertical orientation, we can’t see the men of monsters anymore, so have no way to switch between them. Luckily, Apple has made an easy way to remedy this – which we’ll do next!
Apple’s standard solution to this situation is for you to put a toolbar (or button of some kind) on the right view, and tap a button to bring up a popover view showing the content from the left view.
Apple has a built in API to make this pretty easy. The basic idea is you register the right hand side as a delegate of the UISplitViewController. This way the UISplitViewController can send the right side a notification when it hides the left hand side – as well as a handy button that you can add to the toolbar to use to display the left hand side in a popover.
So let’s give this a try. Open up RightViewController.xib and drag a UIToolbar to the top of the view. You might have to move everything else down a bit. Also, delete the default Bar Button Item it adds in.
Also and this is important – by default the auto-resizing behavior for the UIToolbar assumes it’s on the bottom of the view, so fix the settings to be proper for its location in the top of the view as follows:
Then make the following changes to RightViewController.h:
// Add UISplitViewControllerDelegate to the list of protocols @interface RightViewController : UIViewController <MonsterSelectionDelegate, UISplitViewControllerDelegate> { // Inside the class definition UIPopoverController *_popover; UIToolbar *_toolbar; // In the property section @property (nonatomic, retain) UIPopoverController *popover; @property (nonatomic, retain) IBOutlet UIToolbar *toolbar; |
And the following to RightViewController.m:
// After @implementation @synthesize popover = _popover; @synthesize toolbar = _toolbar; // In deallloc self.popover = nil; self.toolbar = nil; // In monsterSelectionChanged if (_popover != nil) { [_popover dismissPopoverAnimated:YES]; } // New functions - (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { barButtonItem.title = @"Monsters"; NSMutableArray *items = [[_toolbar items] mutableCopy]; [items insertObject:barButtonItem atIndex:0]; [_toolbar setItems:items animated:YES]; [items release]; self.popover = pc; } - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { NSMutableArray *items = [[_toolbar items] mutableCopy]; [items removeObjectAtIndex:0]; [_toolbar setItems:items animated:YES]; [items release]; self.popover = nil; } |
Two last things. First go back to RightViewConntroller.xib and control-drag from File’s Owner to the Toolbar to hook it up to its outlet. Secondly open up MainWindow.xib and control-drag from “Split View Controller” to “Right View Controller” to set the right view controller as the delegate of the split view controller.
That’s it! Compile and run the project and now when you rotate, a button will appear on your toolbar that you can tap for a popover as follows:
Pretty cool, except that’s a little long eh? There’s a trivial way to fix it – add the following to viewDidLoad in the LeftViewController:
- (void)viewDidLoad { [super viewDidLoad]; self.clearsSelectionOnViewWillAppear = NO; self.contentSizeForViewInPopover = CGSizeMake(320.0, 300.0); } |
In this project, we’re using a custom view with a toolbar on the right hand side. However in your projects, you might want to use a UINavigationController on the other side instead. How can you set it up so that the bar button item goes into the UINavigationController’s toolbar?
To do this, all you need to do is store a reference to the navigation controller, and in the willHideViewController method simply set the left bar button item for the root view controller’s UINavigationItem as follows:
UINavigationItem *navItem = [[_rightNavController.viewControllers objectAtIndex:0] navigationItem]; [navItem setLeftBarButtonItem:barButtonItem animated:YES]; |