NEW: Master Swift design patterns with my latest book! >>

< Previous: Loading images with UIImage   Next: Wrap up >

Final tweaks: hidesBarsOnTap, safe area margins

At this point you have a working project: you can press Cmd+R to run it, flick through the images in the table, then tap one to view it. But before this project is complete, there are several other small changes we're going to make that makes the end result a little more polished.

First, you might have noticed that all the images are being stretched to fill the screen. This isn't an accident – it's the default setting of UIImageView.

This takes just a few clicks to fix: choose Main.storyboard, select the image view in the detail view controller, then choose the attributes inspector. This is in the right-hand pane, near the top, and is the fourth of six inspectors, just to the left of the ruler icon.

If you don't fancy hunting around for it, just press Cmd+Alt+4 to bring it up. The stretching is caused by the view mode, which is a dropdown button that defaults to "Scale to Fill.” Change that to be "Aspect Fit," and this first problem is solved.

The Aspect Fit content mode for UIImageViews forces them to resize their images so they are fully visible.

If you were wondering, Aspect Fit sizes the image so that it's all visible. There's also Aspect Fill, which sizes the image so that there's no space left blank – this usually means cropping either the width or the height. If you use Aspect Fill, the image effectively hangs outside its view area, so you should make sure you enable Clip To Bounds to avoid the image overspilling.

The second change we're going to make is to allow users to view the images fullscreen, with no navigation bar getting in their way. There's a really easy way to make this happen, and it's a property on UINavigationController called hidesBarsOnTap. When this is set to true, the user can tap anywhere on the current view controller to hide the navigation bar, then tap again to show it.

Be warned: you need to set it carefully when working with iPhones. If we had it set on all the time then it would affect taps in the table view, which would cause havoc when the user tried to select things. So, we need to enable it when showing the detail view controller, then disable it when hiding.

You already met the method viewDidLoad(), which is called when the view controller's layout has been loaded. There are several others that get called when the view is about to be shown, when it has been shown, when it's about to go away, and when it has gone away. These are called, respectively, viewWillAppear(), viewDidAppear(), viewWillDisappear() and viewDidDisappear(). We're going to use viewWillAppear() and viewWillDisappear() to modify the hidesBarsOnTap property so that it's set to true only when the detail view controller is showing.

Open DetailViewController.swift, then add these two new methods directly below the end of the viewDidLoad() method:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    navigationController?.hidesBarsOnTap = true
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.hidesBarsOnTap = false
}

There are some important things to note in there:

  • We're using override for each of these methods, because they already have defaults defined in UIViewController and we're asking it to use ours instead. Don't worry if you aren't sure when to use override and when not, because if you don't use it and it's required Xcode will tell you.
  • Both methods have a single parameter: whether the action is animated or not. We don't really care in this instance, so we ignore it.
  • Both methods use the super prefix again: super.viewWillAppear() and super.viewWillDisappear(). This means "tell my parent data type that these methods were called." In this instance, it means that it passes the method on to UIViewController, which may do its own processing.
  • We’re using the navigationController property again, which will work fine because we were pushed onto the navigation controller stack from ViewController. We’re accessing the property using ?, so if somehow we weren’t inside a navigation controller the hidesBarsOnTap lines will do nothing.

If you run the app now, you'll see that you can tap to see a picture full size, and it will no longer be stretched. While you're viewing a picture you can tap to hide the navigation bar at the top, then tap to show it again.

The third change is a small but important one. If you look at other apps that use table views and navigation controllers to display screens (again, Settings is great for this), you might notice gray arrows at the right of the table view cells. This is called a disclosure indicator, and it’s a subtle user interface hint that tapping this row will show more information.

It only takes a few clicks in Interface Builder to get this disclosure arrow in our table view. Open Main.storyboard, then click on the table view cell – that’s the one that says “Title”, directly below “Prototype Cells”. The table view contains a cell, the cell contains a content view, and the content view contains a label called “Title” so it’s easy to select the wrong thing. As a result, you’re likely to find it easiest to use the document outline to select exactly the right thing – you want to select the thing marked “Picture”, which is the reuse identifier we attached to our table view cell.

When that’s selected, you should be able go to the attributes inspector and see “Style: Basic”, “Identifier: Picture”, and so on. You will also see “Accessory: None” – please change that to “Disclosure Indicator”, which will cause the gray arrow to show.

The fourth is small but important: we’re going to place some text in the gray bar at the top. You’ve already seen that view controllers have storyboard and navigationController properties that we get from UIViewController. Well, they also have a title property that automatically gets read by navigation controller: if you provide this title, it will be displayed in the gray navigation bar at the top.

In ViewController, add this code to viewDidLoad() after the call to super.viewDidLoad():

title = "Storm Viewer"

This title is also automatically used for the “Back” button, so that users know what they are going back to.

In DetailViewController we could add something like this to viewDidLoad():

title = "View Picture"

That would work fine, but instead we’re going to use some dynamic text: we’re going to display the name of the selected picture instead.

Add this to viewDidLoad() in DetailViewController:

title = selectedImage

We don’t need to unwrap selectedImage here because both selectedImage and title are optional strings – we’re assigning one optional string to another. title is optional because it’s nil by default: view controllers have no title, thus showing no text in the navigation bar.

Large titles in iOS 11

This is an entirely optional change, but I wanted to introduce it to you nice and early so you can try it for yourself and see what you think.

iOS 11 revamped Apple’s design guidelines in many places, but the most noticeable is the use of large titles – the text that appears in the gray bar at the top of apps. The default style is small text, which is what we’ve had so far, but with a couple of lines of code we can adopt the new design.

First, add this to viewDidLoad() in ViewController.swift:

navigationController?.navigationBar.prefersLargeTitles = true

That enables large titles across our app, and you’ll see an immediate difference: “Storm Viewer” becomes much bigger, and in the detail view controller all the image titles are also big. You’ll notice the title is no longer static either – if you pull down gently you’ll see it stretches ever so slightly, and if you try scrolling up in our table view you’ll see the titles shrinks away.

Apple recommends you use large titles only when it makes sense, and that usually means only on the first screen of your app. As you’ve seen, the default behavior when enabled is to have large titles everywhere, but that’s because each new view controller that pushed onto the navigation controller stack inherits the style of its predecessor.

In this app we want “Storm Viewer” to appear big, but the detail screen to look normal. To make that happen we need to add a line of code to viewDidLoad() in DetailViewController.swift:

navigationItem.largeTitleDisplayMode = .never

That’s all it takes – the large titles should behave properly now.

And what about iPhone X?

iPhone X introduces the first iPhone screen that isn’t rectangular, which creates some interesting problems. In particular, the rounded corners of the screen mean that content can easily be clipped if it isn’t positioned correctly, so the system tries to adapt by making safe areas – parts of the screen that are automatically avoided so that clipping doesn’t happen.

In this app, our table view controller will automatically run edge to edge because Apple has done all the hard work for us, but the image in our detail view controller won’t – it will have a white gap at the bottom to leave space for the home indicator.

That is sometimes what you’ll want, but here it looks pretty poor. Fortunately, it’s trivial to fix: open Main.storyboard, select the view inside Detail View Controller, then uncheck the Safe Area Layout Guide box in the size inspector. That will ask the view (and all its subviews) to run edge to edge, which looks better.

While we’re talking about the iPhone X, let’s extend our use of hidesBarsOnTap so that the home indicator gets hidden too. This is controlled by overriding a new method called prefersHomeIndicatorAutoHidden() – if that returns true, the home indicator will disappear after a couple of seconds, only to automatically reappear when the user touches the screen.

We’re going to write this method so that it returns the value of our navigation controller’s hidesBarsOnTap property, meaning that the bars and the home indicator should disappear or reappear together. We know we’ll always have a navigation controller, so we can force unwrap it and read its property – add this method to DetailViewController.swift now:

override func prefersHomeIndicatorAutoHidden() -> Bool {
    return navigationController!.hidesBarsOnTap
}

That method gets checked when the view controller is first shown, but will also automatically get called whenever the bars get toggled using hidesBarsOnTap. So, that’s our work done: the home indicator should automatically disappear a few seconds after the bars disappear.

That’s the last of the changes – we’re done! Go ahead and run the project now and admire your handiwork.

Download for free!

Want a free 75-minute video teaching functional programming, protocol-oriented programming, and more? This is your lucky day!

< Previous: Loading images with UIImage   Next: Wrap up >
Click here to visit the Hacking with Swift store >>