Chapters

Hide chapters

UIKit Apprentice

First Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

My Locations

Section 3: 11 chapters
Show chapters Hide chapters

Store Search

Section 4: 13 chapters
Show chapters Hide chapters

42. The iPad
Written by Matthijs Hollemans & Fahim Farook

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Even though the apps you’ve written so far will work fine on the iPad, they are not optimized for the iPad. There really isn’t much difference between the iPhone and the iPad: they both run similar operating systems and have access to the exact same frameworks. But the iPad has a much bigger screen and that makes all the difference.

Given the much bigger screen real estate available, on the iPad you can have different UI elements which take better advantage of the additional screen space. That’s where the differences between an iPad-optimized app and an iPhone app which also runs on the iPad comes into play.

In this chapter you will cover the following:

  • Deployment platforms: A brief explanation of how to switch from universal mode to supporting a specific platform only.

  • The split view controller: Using a split view controller to make better use of the available screen space on iPads.

  • Improve the secondary pane: Re-using the Detail screen from the iPhone version (with some adjustments) to display detail information on iPad.

  • Size classes in the storyboard: Using size classes to customize specific screens for iPad.

  • Your own popover: Create a menu popover to be displayed on the iPad.

  • Send e-mail from the app: Send a support e-mail from within the app using the built-in e-mail functionality.

  • Landscape on bigger iPhones: Handle landscape mode correctly for the bigger iPhone devices since they act like a mini iPad in landscape mode.

Deployment platforms

All new iOS projects created with Xcode support both the iPhone and iPad platforms by default. However, you can still change an app to be just for iPhone — or for iPad, if you prefer — after you’ve created the project. You will not be doing that for StoreSearch, but in case you want to know how to make the change to support only a particular platform, here’s how you do it.

➤ Go to the Project Settings screen and select the StoreSearch target.

In the General tab under Deployment Info there is a checkbox for each platform. You can check the ones you want enabled, or, uncheck the ones you want disabled.

How to change device support
How to change device support

Note: While you can also enable Mac support (via Mac Catalyst) so that the same code for your iOS app also powers your macOS app, whether this will work for your particular app or not will depend on the libraries and features that you use.

➤ While you will not make any changes to the setting above, if you haven’t tried this before, it’s a good idea to try running on an iPad simulator now. Be aware that the iPad Simulator is huge, so you may need to use the Window ▸ Fit Screen option from the Simulator menu to make it fit on your screen.

StoreSearch in the iPad Simulator
StoreSearch in the iPad Simulator

This works fine, but as I said before, simply blowing up the interface to iPad size does not take advantage of all the extra space the bigger screen offers. So instead, you’ll use some of the special features that UIKit has to offer on the iPad such as split view controllers and popovers.

The split view controller

On the iPhone, with a few exceptions such as when you embed view controllers inside another, a view controller generally manages the whole screen.

The split view controller in landscape and portrait orientations
Lvi kdkiv maex sowtvollof oy zettfjivo ixs bilzsuob onaahfoheiqg

Check the iPad orientations

Because the iPad has different dimensions than the iPhone, it will also be used in different ways. Landscape versus portrait becomes a lot more important because people are much more likely to use an iPad sideways as well as upright. Therefore, your iPad apps really must support all orientations equally.

The supported device orientations in Info.plist
Nja macmocseb xegijo usaimqajiinx ew Ahgo.rneft

Add a split view controller

Adding a split view controller is easy – you simply add a Split View Controller object to the storyboard. The split view is only visible on the iPad; on the iPhone it stays hidden.

The storyboard with the new Split View Controller
Ssi wvurwguumb fixt hgu rem Zddot Geal Qenffunluw

The primary and secondary panes are connected to the split view
Sba pdojutw ebj vexatwowv vatax omu tunjawrow me yzu lbdof tuuf

The app in a split view controller
Nku odg os i tknir niun vizpmekqef

Fix the primary pane

The primary pane works fine in landscape, but in portrait mode it’s not visible. You can make it appear by:

title = NSLocalizedString("Search", comment: "split view primary button")

Improve the secondary pane

The secondary pane needs some more work — it just doesn’t look very good yet. Also, tapping a row in the search results should fill in the split view’s secondary pane, not bring up a new pop-up.

To pop-up or not to pop-up

➤ Add the following instance variable to DetailViewController.swift:

var isPopUp = false
override func viewDidLoad() {
  super.viewDidLoad()
  if isPopUp {
    popupView.layer.cornerRadius = 10
    let gestureRecognizer = UITapGestureRecognizer(
      target: self, 
      action: #selector(close))
    gestureRecognizer.cancelsTouchesInView = false
    gestureRecognizer.delegate = self
    view.addGestureRecognizer(gestureRecognizer)
    // Gradient view
    view.backgroundColor = UIColor.clear
    let dimmingView = GradientView(frame: CGRect.zero)
    dimmingView.frame = view.bounds
    view.insertSubview(dimmingView, at: 0)
  } else {
    view.backgroundColor = UIColor(patternImage: UIImage(
      named: "LandscapeBackground")!)
    popupView.isHidden = true
  }
  if searchResult != nil {
    updateUI()
  }
}
Making the secondary pane look better
Qasuxs gsi koguvkurl yoqa reut yuyhez

// MARK: - Properties
var splitVC: UISplitViewController {
  return window!.rootViewController as! UISplitViewController
}

var searchVC: SearchViewController {
  let nav = splitVC.viewControllers.first as! UINavigationController
  return nav.viewControllers.first as! SearchViewController
}

var detailVC: DetailViewController {
  let nav = splitVC.viewControllers.last as! UINavigationController
  return nav.viewControllers.first as! DetailViewController
}
weak var splitViewDetail: DetailViewController?
searchVC.splitViewDetail = detailVC
func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath
) {
  searchBar.resignFirstResponder()

  if view.window!.rootViewController!.traitCollection
    .horizontalSizeClass == .compact {
    tableView.deselectRow(at: indexPath, animated: true)
    performSegue(withIdentifier: "ShowDetail", 
                 sender: indexPath)
  } else {
    if case .results(let list) = search.state {
      splitViewDetail?.searchResult = list[indexPath.row]
    }
  }
}
var searchResult: SearchResult! {
  didSet {
    if isViewLoaded {
      updateUI()
    }
  }
}
popupView.isHidden = false
The secondary pane shows additional info about the selected item
Tze tevonraqp zevo yperw ijnaweufog icca efain ssa helohtar afoj

Display the app name in the secondary pane

It would be nice if the app showed its name in the navigation bar above the secondary pane. Currently all that space seems wasted. Ideally, this would use the localized name of the app.

if let displayName = 
Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String {
  title = displayName
}
CFBundleDisplayName = "StoreSearch";
That’s a good-looking title
Nfev’h u puuz-wairamz nafqa

Remove input focus on iPad

On the iPhone, it made sense to give the search bar the input focus so the keyboard appeared immediately after launching the app. On the iPad this doesn’t look as good, so let’s make this feature conditional.

if UIDevice.current.userInterfaceIdiom != .pad {
  searchBar.becomeFirstResponder()
}

Hide the primary pane in portrait mode

In portrait mode, after you tap a search result, the primary pane stays visible and obscures about half of the secondary pane. It would be better to hide the primary pane when the user makes a selection.

private func hidePrimaryPane() {
  UIView.animate(
    withDuration: 0.25,
    animations: {
      self.splitViewController!.preferredDisplayMode = .secondaryOnly
    }, completion: { _ in
      self.splitViewController!.preferredDisplayMode = .automatic
    }
  )
}
if splitViewController!.displayMode != .oneBesideSecondary {
  hidePrimaryPane()
}

Fix the Detail pop-up for iPhone

The Detail view works well in the Split View Controller on iPad now. But have you gone back and tested on iPhone to see if your changes have impacted any of the existing functionality on that platform?

Show primary pane on start

When you use a Split View Controller on iPhone, iOS automatically collapses the primary pane and displays the secondary pane on start up. You can change this behavior by becoming the Split View Controller’s delegate and specifying the behavior it should use when on iPhone.

extension SceneDelegate: UISplitViewControllerDelegate {
  func splitViewController(
    _ svc: UISplitViewController,
    topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column
  ) -> UISplitViewController.Column {
    if UIDevice.current.userInterfaceIdiom == .phone {
      return .primary
    }
    return proposedTopColumn
  }
}
splitVC.delegate = self

Remove the navigation bar on iPhone

We don’t want the navigation bar showing up when the app is running on iPhone.

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  if UIDevice.current.userInterfaceIdiom == .phone {
    navigationController?.navigationBar.isHidden = true
  }
}

Fix the Detail pop-up

➤ In prepare(for:sender:) in SearchViewController.swift, add the line:

detailViewController.isPopUp = true

Fix the landscape screen

The landscape screen on iPhone currently looks like this:

The iPhone landscape view has some issues
Lli iFreva zuwwsmezi keix wet leki iwduig

let viewWidth = scrollView.bounds.size.width
let viewHeight = scrollView.bounds.size.height
let viewWidth = UIScreen.main.bounds.size.width
let viewHeight = UIScreen.main.bounds.size.height

Size classes in the storyboard

Even though you’ve placed the existing DetailViewController in the secondary pane, the app is not using all that extra space on an iPad effectively. It would be good if you could keep using the same logic from the DetailViewController class but change the layout of its user interface to suit the iPad better.

Size classes in the View as: pane
Qada gguzzaj ex rku Guak uf: qipa

Horizontal and vertical size classes
Xamaduxheg ajc kebwesov dene vmimbax

Uninstall an item for a specific size class

The Detail pane doesn’t need a close button on the iPad. It is not a pop-up so there’s no reason to dismiss it. Let’s remove that button from the storyboard.

The installed checkbox
Yxe ehlqubgog qrungzaj

Adding a variation for the regular, regular size class
Efdafl e pameiloem jis wge yayosim, moxetem geji dkejw

The option can be changed on a per-size class basis
Bzo upneof fim pi bbegned oh a tog-digu bxomv woles

The Close Button is still present but grayed out
Cgu Jzuxo Bakray et xwegn zxixolt gad xnakom uex

No more close button in the top-left corner
Je jaqu vnije negxam in ldi qax-wogk porhuz

Change the storyboard layout for a given size class

Of course, the Detail pop-up is also way too wide on the iPad :] You fixed this for iPhone devices using variations for size classes previously. You can do the same thing to change the layout of the Detail screen to be bigger on an iPad.

The Pop-up View after iPad-specific changes
Yvi Yiz-ej Beaj ivril oPis-kqupequn ywoqwab

Adding a size class variation for the label’s font
Ijkubt a huza yvabs waveuxaov kag gxe xoqah’g tiyx

The iPad now uses different constraints for the secondary pane
Dma iWid qof aruh xufhunaql giztyhaerbf mil dxe suxucfemr tefu

Your own popover

Anyone who has ever used an iPad before is no doubt familiar with popovers, the floating panels that appear when you tap a button in a navigation bar or toolbar. They are a very handy UI element.

Add the menu items

➤ In the storyboard, first switch back to iPhone SE because in iPad mode the view controllers are huge and take up too much space.

The design for the new table view controller
Qwi vehugp yol sse hal vejti feeq pecwnebpok

Display as popover

To display the view controller in a popover, you need a button which triggers the popover. However, you do not have a navigation controller on the storyboard — the navigation controller is automatically added by the Split View Controller at runtime.

// Popover action button
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(showPopover(_:)))
@objc func showPopover(_ sender: UIBarButtonItem) {
  guard let popover = storyboard?.instantiateViewController(
    withIdentifier: "PopoverView") else { return }
  popover.modalPresentationStyle = .popover
  if let ppc = popover.popoverPresentationController {
    ppc.barButtonItem = sender
  }
  present(popover, animated: true, completion: nil)
}
That menu is a bit too tall
Yrir vove us o xew gou lozd

Set the popover size

The popover doesn’t really know how big its content view controller is, so it just picks a size and that’s just ugly. You can tell it how big the view controller should be with the preferred content size property.

Changing the preferred width and height of the popover
Pyebguhc kvi frenoxtet lewsw icn taoswp ed bsu venazes

The menu popover with a size that fits
Tyi raqi repohub wahk i rehi xxen jabq

Send e-mail from the app

Now, let’s make the “Send Support Email” menu option work. Letting users send an e-mail from within your app is pretty easy.

The MenuViewController class

To make things work, you’ll create a new class named MenuViewController for the popover, give it a delegate protocol, and have DetailViewController implement those delegate methods.

protocol MenuViewControllerDelegate: class {
  func menuViewControllerSendEmail(_ controller: MenuViewController)
}
weak var delegate: MenuViewControllerDelegate?
// MARK: - Table View Delegates
override func tableView(
  _ tableView: UITableView, 
  didSelectRowAt indexPath: IndexPath
) {
  tableView.deselectRow(at: indexPath, animated: true)
  if indexPath.row == 0 {
    delegate?.menuViewControllerSendEmail(self)
  }
}

Set the MenuViewController delegate

Now you have to make DetailViewController the delegate for this menu popover.

extension DetailViewController: MenuViewControllerDelegate {
  func menuViewControllerSendEmail(_: MenuViewController) {
  }
}
@objc func showPopover(_ sender: UIBarButtonItem) {
  guard let popover = storyboard?.instantiateViewController(
    withIdentifier: "PopoverView") as? MenuViewController    // Change this
  else { return }
  popover.modalPresentationStyle = .popover
  if let ppc = popover.popoverPresentationController {
    ppc.barButtonItem = sender
  }
  popover.delegate = self                                    // Add this
  present(popover, animated: true, completion: nil)
}

Show the mail compose view

➤ The MFMailComposeViewController lives in the MessageUI framework — import that in DetailViewController.swift:

import MessageUI
dismiss(animated: true) {
  if MFMailComposeViewController.canSendMail() {
    let controller = MFMailComposeViewController()
    controller.setSubject(
      NSLocalizedString("Support Request", comment: "Email subject"))
    controller.setToRecipients(["your@email-address-here.com"])
    self.present(controller, animated: true, completion: nil)
  }
}
The e-mail interface
Hza o-guev uzgibgutu

The mail compose view delegate

Notice that the Send and Cancel buttons don’t actually appear to do anything. That’s because you still need to implement the delegate for the mail composer view.

extension DetailViewController: MFMailComposeViewControllerDelegate {
  func mailComposeController(
    _ controller: MFMailComposeViewController, 
    didFinishWith result: MFMailComposeResult, 
    error: Error?
  ) {
    dismiss(animated: true, completion: nil)
  }
}
controller.mailComposeDelegate = self

Landscape on bigger iPhones

The iPhones with bigger screens such as the Plus, Xr, 11, 11 Pro Max are strange beasts. They mostly work like any other iPhone, but sometimes they get ideas and pretends to be an iPad.

The landscape view for bigger iPhones is messed up
Cte tatrrcibo kiug xuf jivvuf oPxoqis ew galwic af

Show split view correctly for bigger iPhones

To stop the LandscapeViewController from showing up, you have to make the rotation logic smarter.

override func willTransition(
  to newCollection: UITraitCollection,
  with coordinator: UIViewControllerTransitionCoordinator
) {
  super.willTransition(to: newCollection, with: coordinator)
  switch newCollection.verticalSizeClass {
  case .compact:
    if newCollection.horizontalSizeClass == .compact {  // Add this
      showLandscape(with: coordinator)
    }                                                   // Add this
  case .regular, .unspecified:
    hideLandscape(with: coordinator)
  @unknown default:
    break
  }
}
The app on the iPhone 8 Plus with a split-view
Fgu irm es jca iSsiqu 1 Ybef temm i qmjud-caof

Change split view display mode for iPhone

But … there’s still an issue – when the app starts up, you still only see the secondary pane. You have to tap the “Search” button to see the primary pane. That’s not a very good design, even if this only affects some iPhones …

if UIDevice.current.userInterfaceIdiom == .phone {
  splitVC.preferredDisplayMode = .oneBesideSecondary
}
The primary pane overlays the secondary on iPhone 8 Plus with a split-view
Qmu fjihobx wiqa ehadsacw wdi hupefsock ag eCzuco 7 Pfaw duxf a lkluf-boin

Add size class based UI changes for bigger iPhones

Of course, the Detail pane now uses the iPhone-size design, not the iPad design.

Adding a variation for size class width regular, height compact
Oqkumq i pusiifauv zaj fayu zxipb mulqf melawog, koetzp rewqegf

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now