Split Views on the iPhone

So it’s great that we have our app making better use of the space on the iPad, but that begs the question of what’s happening on the iPhone. Does it have a side-by-side split view too? How will that fit on a dinky iPhone 4s? We’d better check what’s going on, so use the scheme selector to switch to one of the smaller iPhone models (the 4s, 5, or 5s) and run the app again.

What happens is pretty unexpected:

images/bigscreens/unpopulated-detail-view-iphone.png

Somehow, our changes have caused us to start on a full-screen detail view instead of the master view with the list of tweets. Also, there’s a navigation bar over the detail view, even though there wasn’t one on top of the right pane of the split view on the iPad.

Why? The split view controller realizes there’s not enough space on the screen for both view controllers, so it has switched into a navigation-like metaphor for showing the two parts of the split on separate pages. This is actually a handy feature, since it lets us use a split view controller for both iPad and iPhone. Prior to iOS 8, we had to have completely separate storyboards for iPhone and iPad. So this is a big win for building Universal apps—those that run on iPhone and iPad—if only it did the right thing out of the box.

Notice that the back button at the top left says Tweets (the title of the master view, which it gets from a navigation item), and we can tap it to go back to the list of tweets. However, if we tap one of the tweets, it doesn’t populate the detail view and take us to it. So we have two things to fix: we want to start on the master view controller (the list of tweets) instead of the detail, and we want tapping a table row to fill in the detail like it did in the old navigation app, and in the iPad version of the split view.

Handling Collapsing Split Views

The first step to dealing with the user starting on the wrong scene is knowing that our code is even in this scenario and that we need to do something different. Actually, we can gain the ability to address the problem by becoming the split view controller’s delegate. The delegate gets told about changes like rotation, which cause it to rework how it presents its contents. It gets these callbacks at startup too, including one that says it’s running in the compact space of an iPhone.

Start in RootViewController.swift by appending UISplitViewControllerDelegate to the comma-separated list of protocols in the class declaration. This will allow our RootViewController to become the split view controller’s delegate.

We want to become the delegate as soon as possible, so viewDidLoad is a good place to do so. At the bottom of that method, add the following code:

 if​ ​let​ splitViewController = splitViewController {
  splitViewController.delegate = ​self
 }

All this does is check if there’s a splitViewController parent, just like we checked when we handled the table row tap. If there is, we become its delegate.

Actually, removing the second view controller, the detail scene, is exactly what we want. If we just return true, the split view controller will give up on the detail view controller, leaving us with just the master view controller, which is the list of tweets. So implement the method like this:

 func​ splitViewController(splitViewController: ​UISplitViewController​,
  collapseSecondaryViewController secondaryViewController: ​UIViewController​,
  ontoPrimaryViewController primaryViewController: ​UIViewController​)
  -> ​Bool​ {
 return​ ​true
 }

Run the app on one of the iPhone models now, and we come up on the list of tweets.

Restoring Discarded View Controllers

Well, that’s great, except that tapping on a row still doesn’t do anything. And the reason for that is in our tap-handling logic in tableView(didSelectRowAtIndexPath:). When we implemented that before, we made sure the split view controller had two child view controllers, so we could take the second one (the TweetDetailViewController) and populate it.

But we can’t do that now, because there is no second view controller. We just told the split view controller that it was OK to discard the second view controller. So that’s just great.

Maybe we’ll just have to remake that view controller ourselves! Fortunately, it’s pretty easy to do so with storyboards. There’s a UIStoryboard class that offers just three methods, two of which are for creating scenes from within the storyboard. The one we need is instantiateViewControllerWithIdentifier, which takes a string and gives us back a UIViewController, with its view and all its subviews laid out exactly like we created them in the storyboard.

For this to work, we need to give the Tweet Detail scene a unique ID string. In the storyboard, select the Tweet Detail View Controller, and bring up its Identity Inspector (3). In the Storyboard ID field, enter TweetDetailVC, as shown in the following figure.

images/bigscreens/tweet-detail-storyboard-id.png

Perform a clean build (Product > Clean, or K), since changes to storyboards aren’t always picked up by Xcode’s build process, and we want to make sure this scene is findable by that string.

Now we can re-create this view controller when we need it. The place we’re going to do so is in RootViewController’s tableView(didSelectRowAtIndexPath:). We want to handle the case where the split view controller has only one child view controller, so find the closing brace that matches if let splitViewController = splitViewController where splitViewController.viewControllers.count > 1 {, and replace its closing brace with the following else block.

1: } ​else​ {
2: if​ ​let​ storyboard = storyboard,
3:  detailVC =
4:  storyboard.instantiateViewControllerWithIdentifier(​"TweetDetailVC"​)
5: as?​ ​TweetDetailViewController​ {
6:  detailVC.tweetIdString = parsedTweet.tweetIdString
7:  splitViewController?.showDetailViewController(detailVC,
8:  sender: ​self​)
9:  }
10: }

On lines 2--5, we check to see that we’re in a storyboard, that it has a scene called TweetDetailVC, and that we can cast it to a TweetDetailViewController. If all that works, then we’ve got our detail view controller, and its whole view hierarchy, just as laid out in the storyboard. In turn, that means we can get it to load its contents like we always have, by setting its tweetIdString (on line 6). Then we just have to navigate to it. UISplitViewController gives us that ability with the showDetailViewController method, on lines 7--8.

And that’s it! Run the app on a simulated iPhone, and it works just like the navigation version did from the previous chapter, perfectly well suited to the small space of the iPhone. Back on the iPad, we get a side-by-side split that makes better use of all the screen real estate. Best of both worlds, and with the split view controller we get it all with one storyboard and this little bit of tricky code.

Split Views and iPad Portrait Orientation

Actually, we’re not quite done. Run the app on an iPad and rotate to portrait orientation. Adding the UISplitViewDelegate has killed the gesture that showed the master view controller (that is, the list of tweets). Maybe it’s just as well, since the gesture wasn’t very discoverable, but we need a way to hide and show the list.

Two things will help us here: the UISplitViewController has a UIBarButton that shows its master view controller. We could add this on the fly, if we knew when to do so. Fortunately, the UISplitViewControllerDelegate gives us that, too.

We’ll need to add the bar button in two places: when the UI first comes up, and when the split view controller changes its display mode. So let’s stub out an empty method in RootViewController for performing the fix:

 func​ addShowSplitPrimaryButton(splitViewController: ​UISplitViewController​) {
 }

The first place we’ll call our helper method is in viewDidLoad, right after we set ourselves as the UISplitViewController’s delegate:

 if​ ​let​ splitViewController = splitViewController {
  splitViewController.delegate = ​self
  addShowSplitPrimaryButton(splitViewController)
 }

The second time we need to call our helper is when we get rotated: we need to show it in portrait orientation, which shows only the detail view controller, but not in landscape, which shows both master and detail. A delegate method called splitViewController(willChangeToDisplayMode:) helps us here. It passes in a UISplitViewControllerDisplayMode, which is an enum whose values are .Automatic, .PrimaryHidden, .AllVisible, and .PrimaryOverlay. When we rotate to portrait, this will get called with the .PrimaryHidden value, and in landscape, it will be called again with .AllVisible. So we need to call our helper when the primary view controller, our list of tweets, becomes hidden:

 func​ splitViewController(svc: ​UISplitViewController​,
  willChangeToDisplayMode displayMode: ​UISplitViewControllerDisplayMode​) {
 if​ displayMode == .​PrimaryHidden​ {
  addShowSplitPrimaryButton(svc)
  }
 }

Now, for the main attraction: how do we actually get this button to show the primary view controller? It’s available from UISplitViewController, via the method displayModeButtonItem, so getting it is easy. What to actually do with it is a little trickier. Here’s the recipe:

 func​ addShowSplitPrimaryButton(splitViewController: ​UISplitViewController​) {
 let​ barButtonItem = splitViewController.displayModeButtonItem()
 if​ ​let​ detailNav = splitViewController.viewControllers.last
 as?​ ​UINavigationController​ {
  detailNav.topViewController?.navigationItem.leftBarButtonItem =
  barButtonItem
  }
 }

After we get the barButtonItem, the trick is what to do with it. Our detail view controller has a navigation controller in front of it, which gives it a navigation bar (and also the “Tweet” title in that bar). So what we’re doing here is asking if the split view controller’s last view controller (the last member of its viewControllers array) is a UINavigationController. If it is, then we can ask for that navigation controller’s first view controller, get its UINavigationItem, and set its leftBarButtonItem.

Run the app on an iPad simulator and rotate a few times. Whenever the screen is in portrait orientation, there will now be a blue “back” chevron, as seen in the following figure. Tap it to slide the list of tweets over the detail view. Now it works just like the side-by-side presentation in landscape: select a tweet from the list, and its contents pop up in the detail view. Also, tapping outside the list of tweets (that is to say, in the detail view) dismisses the list of tweets, but we can always get back to it with our handy left bar button.

images/bigscreens/fix-ipad-portrait.png
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.181.45