I started out with a simple project of mine, the demo I made for DTNotepadViewController. This is a navigation stack which has a UITableView. From there you can select an entry to get a detail view looking like note paper.
The goal is to convert this iPhone-only app into a hybrid app that uses a split view on iPad.
Upgrade for iPad
We’ll dive in, head first. Probably for the first time, we’re upgrading our target for iPad. There’s an option to do that in the context-menu for your target.
On the following screen we choose “One Universal Application” instead of “Two device-specific applications” because we plan to make this a hybrid. At first you don’t see any change to the project, but when you Build&Go for iPad Sim so see that the table scales well, but the notepad looks really ugly.
Of course all our images are too small and the lines limit I seem to have hard-coded. The whole thing appears to be hard than anticipated because for reasons of laziness I have packed most of the notepad code into a single view controller. The other problem is that I cannot use the scaleToFill contentMode because then the two lines on the edge image no longer line up with the drawn vertical lines.
So, first order of business is to move the view construction code out of the view controller and into it’s own view. This means moving the instance variables, properties, essentially loadView becoming a new method to setup DTNotePadView’s subviews. Actually that’s the second time where I found that I did not save myself work in the long run. Note to self for the future: for components the smallest bit should always be a self-contained view, not a view controller.
HighDef’ing the Components
Any component that we want to use for iPhone and iPad variants needs to be able to handle the different resolutions.
We don’t want the view to be autorotating on iPad because notepaper looks really weird in landscape. So we change the shouldAutorotateToInterfaceOrientation such that autorotation only occurs on iPhones.
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { // only rotate on iPhone return [UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad; } |
The big paper body image can be stretched without problems because it does not have any details that would start pixellating. For the top and bottom edges I needed to use my favorite image editor’s cloning tool to get it to 768 pixels width, leaving the vertical lines untouched. That width is sufficient because as stated above we don’t support landscape lined paper.
So that fixes the graphics, but still the user experience is not quite what you’d expect from an iPad app.
When we updated our project for iPad the wizard also created a duplicate of the MainWindow.xib file with the same kind of structure. In our second step we’ll replace this with a UISplitView.
Switching to a UISplitView
Ok, that’s another first. No idea, what’s necessary to transplant a split view into our existing project. But luckily there is a project template that consists of just that. So let’s inspect how Apple is doing it. File – New Project …
The project this creates contains everything to have a UISplitView and the mechanics to deal with interface orientation. In Portrait you have a button showing a pop over with the table view contents. In landscape mode you have the table view on the left side and the detail view on the right.
Having a look at the MainWindow.xib reveals nothing special. UISplitView just has two view controllers as children. The first (left) has a navigation controller containing a UITableViewController sub-class named RootViewController. The second (right) has a custom view controller, called the DetailViewController.
RootViewController has a pointer to DetailViewController and a property to specify the size of the view in the Popover. Apart from this it’s a regular table view datasource/delegate.
DetailViewController is more confusing. Instead of having a navigation controller taking care of creating a bar at the top Apple opted to create a UIToolBar instead. That’s a problem if you rely on a navigationItem in your detail view controller to set the title and buttons. The toolbar makes sense, because typically you would have more than just a left and right button, like on a navigation bar.
So I either had to adapt my DTNotePadViewController to also work with a toolbar or implement a UINavigationController for the detail view as well.
[ad#WCPOOLS]
Subclassing View Controllers for iPad
After long pondering I decided to subclass my DTNotePadViewController as DTNotePadSplitViewController and override all these methods that should behave differently. A method setupButtons would create UIBarButtonItems for the navigationBar on iPhone and for the new toolbar on iPad. Compare how they look on iPhone versus iPad.
iPhone
// adjust button availability - (void)updateButtons { if (mode == DTNotePadModeNew) { [self.navigationItem.rightBarButtonItem setEnabled:[self.notePadView.textView.text length]>0]; } } - (void)setupButtons { if (mode == DTNotePadModeNew) { // new UIBarButtonItem *saveButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)] autorelease]; UIBarButtonItem *cancelButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)] autorelease]; self.navigationItem.leftBarButtonItem = cancelButton; self.navigationItem.rightBarButtonItem = saveButton; } else { // edit UIBarButtonItem *deleteButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(delete:)] autorelease]; self.navigationItem.rightBarButtonItem = deleteButton; } [self updateButtons]; } |
iPad
- (void)setupButtons { NSMutableArray *items = [NSMutableArray array]; UIBarButtonItem *rightButton = nil; if (popoverButton) { popoverButton.title = @"Notes"; [items addObject:popoverButton]; } UIBarButtonItem *flex = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil]autorelease]; [items addObject:flex]; if (mode == DTNotePadModeNew) { // new UIBarButtonItem *saveButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSave target:self action:@selector(save:)] autorelease]; UIBarButtonItem *cancelButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:@selector(cancel:)] autorelease]; [items addObject:cancelButton]; [items addObject:saveButton]; rightButton = saveButton; } else { // edit self.notePadView.textView.text = _originalText; UIBarButtonItem *deleteButton = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(delete:)] autorelease]; [items addObject:deleteButton]; rightButton = deleteButton; } [toolbar setItems:items animated:YES]; [self updateButtons]; } |
For the iPad there is another speciality necessary: in portrait mode you need to show the button to display the popover with the list and to separate it from the other buttons adding a flexible space item.
Whenever you rotate the iPad with a split view in place there are two methods that get called to prompt you to show or remove the list button on the very left. With my setupButtons taking care of this those became rather simple.
- (void)splitViewController: (UISplitViewController*)svc willHideViewController:(UIViewController *)aViewController withBarButtonItem:(UIBarButtonItem*)barButtonItem forPopoverController: (UIPopoverController*)pc { popoverButton = barButtonItem; self.popoverController = pc; [self setupButtons]; } // Called when the view is shown again in the split view, invalidating the button and popover controller. - (void)splitViewController: (UISplitViewController*)svc willShowViewController:(UIViewController *)aViewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem { self.popoverController = nil; [self setupButtons]; } |
I’m using the pointer to the popoverController as indicator whether or not to show the popover button.
Physicality
Apple encourages developers to give the iPad versions of their apps a “physical” appearance. That means for me two things for this demo. I want to preserve the note’s aspect ratio because you would not see a almost-square piece of notes paper. Secondly I wanted the sheet of note paper appear to be lying on a wooded desk
I wanted to have is to preserve the note’s aspect ration of 1:1.25 because in landscape mode it would look weird to stretch it to always fill the detail view. I achieved that by simple experimentation to get the frames setup right.
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:duration]; if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation)) { _notePadView.frame = CGRectMake(10, 54, 747, 940); } else if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) { _notePadView.frame = CGRectMake(87, 54, 528, 680); } [UIView commitAnimations]; } |
I just could not manage to calculate these positions from the underlying view. I literally spent hours on this but the view ended always up shifted to the left or right. Possible having to do with the toolbar, but I’m uncertain. If you find a better method please let me know.
One problem you start getting with UISplitViewControllers is that you are always showing the detail view. Upon app launch you’d probably want to display the first item of the list right off the bat. And if you are deleting the note you’re currently editing you have to visualize the state transition. To note make it overly complicated I added an animation to “rip off” the note and set the DTNotePadView to half transparent. With the disabled userInteraction this should make it clear what happened. Now the user needs to either add a new note modally or select an existing one for editing which would return the note to “solid” state.
- (void)animateNoteDeletion { // nothing fancy, we just disable the current note [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:1]; [UIView setAnimationTransition:UIViewAnimationTransitionCurlUp forView:_notePadView cache:YES]; _notePadView.alpha = 0.5; _notePadView.textView.text = nil; _notePadView.userInteractionEnabled = NO; [UIView commitAnimations]; [self updateButtons]; } |
Finally I wanted to have a nice wooden finish framing the note paper, a simple Mahagoni PNG did the trick.
Love, Modally
I saved the modal dialog coming from pushing the + on top of the notes list for last, thinking it would be the most difficult. Boy was I wrong, I only had to add a single line:
- (void)addNote { // pushing with nil = add mode, we need to present modally DTNotePadViewController *notes = [[DTNotePadViewController alloc] initWithText:nil]; notes.delegate = self; notes.title = @"New Note"; UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:notes]; // That's all for iPad, get's ignored on iPhone navController.modalPresentationStyle = UIModalPresentationPageSheet; [self presentModalViewController:navController animated:YES]; [notes release]; } |
So in general if you present a modal view all you have to do is choose an iPad-friendly presentation style and you’re done because modal dialogs work exactly the same. Provided of course that your view controller’s view can deal with the iPad-specific resolution.
Conclusion
For Testing both variants you only need to switch between the simulator version. The app will always launch on the appropriate simulator, so you can and should check if the iPhone version is still working as before.
Upgrading an existing project can be daunting because you have to turn into a somewhat shizophrenic person. If the navigation flows are exactly the same between iPhone and iPad you’re off the hook. But if they are not and you want to use the famous UISplitViewController then be prepared that some of the UI paradigms will be different.
And that’s not just because you always have the detail view visible and the requested physicality makes more design necessary. But on the other hand you will get amply rewarded if you wrote very well modularized coded using things like delegate methods and such.
Converting an existing iPhone app might only be worth it if you expect a great deal of new customers for an otherwise forgotten app. For your future projects you better plan for an iPad UI right from the start.
If you want to get DTNotePadView including the iPad-version I’ve discussed in this article you find them on the Dr. Touch’s Parts Store.
Categories: Recipes
Thanks for sharing. The wood grain is a nice touch too!
Thank you for a great article!
This is not a big deal, but I want to point out that there is a formatting error that makes some of the example code hard to read. In the section called “Subclassing View Controllers for iPad”, both iPad code examples put the last part of the code outside of the “wp_codebox” div, so they appear in the style of the article text.
Again, thanks for your informative articles.
Thanks, I corrected the mistake.