跳转至

17 Auto Layout for External Displays

img

Nowadays, using an external display goes well beyond merely connecting a monitor to a stationary computer. In recent years, more and more people are looking to connect their mobile devices to some type of external display — and as an app developer, it’s your responsibility to know how to handle this demand.

For iOS, there are currently three popular external display solutions:

  • AirPlay
  • Physical connections (cables)
  • Chromecast

Generally speaking, an iPhone user will often look for this type of functionality for:

  • Video playback: The iOS device becomes the video playback controller, and the external display becomes the core content provider.
  • Powerpoint presentations: The iOS device shows the slide notes to the presenter while the external display shows the slide content to the audience.
  • Gaming: The iOS device becomes the game controller, and the external display becomes the core content provider.

While this type of symbiotic relationship is common — where the device is the controller and the display shows the content — it’s not the only reason users rely on external displays. Some like to use external displays simply because they offer more screen real estate than their mobile counterparts. Whatever the reason may be, by adding support for external displays, you can provide a better overall experience for your users.

In this chapter, you’ll learn how to:

  • Build a layout for an external display.
  • Configure an external display window.
  • Handle new external display connections.
  • Handle existing external display connections.
  • Handle external display disconnections.
  • Accommodate different external display resolutions.

You’ll accomplish all of this by building a music playback app that supports external displays. By the end of this chapter, you’ll know how to support external displays in your iOS project, and in the process, you’ll gain greater clarity of using windows within your apps.

Getting started

Open ExternalDisplay.xcodeproj in the starter folder. Build and run the app on the simulator.

img

From within the Simulator app, select Hardware ▸ External Displays ▸ 1920×1080 (1080p). For now, you won’t see anything but a blank screen. Close the simulator, and return to Xcode.

The good news is that you can use the Auto Layout fundamentals you already know to build layouts for external displays — provided you understand the key differences.

Building a layout for an external display

Open Main.storyboard and look at MusicPlayerViewController. This will be the view controller for the external display.

img

Notice the existing Auto Layout constraints; these will save you the trouble of having to implement them yourself. For Stack View, you have the following constraints:

  • Center aligned to the superview’s horizontal axis.
  • Center aligned to the superview’s vertical axis.
  • Leading edge greater than or equal to 16 of the safe area leading edge.
  • Trailing edge greater than or equal to 16 of the safe area trailing edge.
  • Top edge greater than or equal to 16 of the safe area top edge.
  • Bottom edge greater than or equal to 16 of the safe area bottom edge.

With these constraints, you center the Stack View within the container and set limitations on how much the stack view can grow, so that it avoids having its content spill beyond the visible screen area.

For Artwork Image View and Music Genre Label, you have a greater than or equal to width relation constraint between the two. This gives the label an equal width to the image view while also giving it the flexibility to outgrow the image view’s width.

If the label’s intrinsic content width fits within the image view’s width, it looks like this:

img

If the label’s intrinsic content width is greater than the image view’s width, it instead looks like this:

img

These constraints are applied to MusicPlayerViewController. Your next task is to configure an external display window.

Configuring an external display window

For every display that you plan to show to your users, you contain it inside of a view. A window is the parent of all views for a display, whether it’s an iPhone, iPad or external display. For supporting an additional external display, it’s paramount that you correctly create and manage the external display’s window to show separate content and present a smooth user experience to your users.

In your project, you have the main window that displays your app’s main content. Similarly, to display content on an external display, you’ll need to create an additional window.

Open MusicSamplingController.swift and add the following method:

private func makeWindow(from screen: UIScreen) -> UIWindow {
  // 1
  let bounds = screen.bounds
  let window = UIWindow(frame: bounds)
  // 2
  window.screen = screen
  // 3
  window.rootViewController = musicPlayerViewController
  // 4
  window.isHidden = false
  return window
}

A few things are occurring here:

  1. Given a UIScreen, take its bounds as the size of the UIWindow.
  2. Set the window’s screen to the one in the argument. You do this to let the window know on which screen to show up.
  3. Set the window’s root view controller to musicPlayerViewController.
  4. Unhide the window before returning it.

Apple recommends as a best practice to provide the window with a screen before showing it because changing the screen of a visible window is an expensive operation.

The structure of the code you’ve added may remind you of programmatically initializing a view controller in earlier chapters. And that’s right! It’s very similar.

Here’s a sample chunk of code that programmatically initializes a view controller:

let storyboard = UIStoryboard(name: "Name", bundle: nil)
let viewController =
  storyboard.instantiateInitialViewController()
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = viewController
window?.makeKeyAndVisible()

The code above creates a window to display your app content on an iOS device. It creates a window using the size of the device. On the other hand, makeWindow(from:)creates a window using the external screen size.

Next, you’ll handle an existing external display connection and place your layout onto the screen.

Handling an existing external display connection

Your app can launch with or without an external display connection established. In this section, you’ll learn how to handle cases in which an external display is already connected.

Open MusicSamplingController.swift. Add the following code to configureExistingScreen():

// 1
let screens = UIScreen.screens
// 2
guard 
  screens.count > 1,
  let screen = screens.last 
  else { return }
// 3
let window = makeWindow(from: screen)
externalWindows.append(window)

Here are the configuration amendments:

  1. Reference all of the screens into a constant.
  2. Using the guard statement, ensure the screen count is greater than 1. If this condition passes, then safely unwrap the last screen in the array of available screens.
  3. Using the safely unwrapped screen, create a window. Then, append the window into the external windows array for future references.

With the simulated external display open and ready in the Simulator app, build and run.

You’ll see the following on the external display:

img

Close the external display window. Build and run the app again. After the app launches, open the simulated external display, and this time, you’ll see a black screen.

Currently, the app doesn’t know how to handle a new external display connection. It’s time to fix that!

Connecting a new external display

Although most people find at-most two external displays on their desk, Apple’s documentation doesn’t state the upper bound of simultaneously connected external displays. Whether you set up your app to support one or ten external displays, you need to implement business logic to deal with the connectivity of the displays. One way to do that is to observe external display event notifications.

To receive external display event notifications, you use NotificationCenter. This API broadcasts specific information to specific registered observers. The information sent to these observers depends on the notification name registered under the object. You can utilize a custom or system notification name to register an object to receive notifications. Since external display events are system events, you’ll use system notification names for convenience and standardization.

Add the following code to observeConnectedScreen() in MusicSamplingController.swift:

// 1
notificationCenter.addObserver(
  forName: UIScreen.didConnectNotification,
  object: nil, 
  queue: nil) { [weak self] notification in
    // 2
    guard 
      let self = self,
      let screen = notification.object as? UIScreen
      else { return }
    // 3
    let window = self.makeWindow(from: screen)
    self.externalWindows.append(window)
}

Here’s what you did:

  1. Observe the screen connection notification.
  2. Safely unwrap the notification object as UIScreen.
  3. Create the external display window and append the window into externalWindows.

Close the external display window. Build and run. After the app launches, open an external display. This time, you’ll see MusicPlayerViewController in the external display.

img

Great! You now have the new external display connection workflow established. Next up, you’ll handle external display disconnection events.

Disconnecting an external display

Similar to the previous implementation, you’ll use NotificationCenter to handle disconnecting an external display connection.

Add the following code to observeDisconnectedScreen() in MusicSamplingController.swift:

// 1
notificationCenter.addObserver(
  forName: UIScreen.didDisconnectNotification,
  object: nil, 
  queue: nil) { [weak self] notification in
    // 2
    guard 
      let self = self,
      let screen = notification.object as? UIScreen
      else { return }

    // 3
    for (index, window) in self.externalWindows.enumerated() {
      guard window.screen == screen else { continue }
      self.externalWindows.remove(at: index)
    }
}

With this code, you:

  1. Observe for a screen disconnection notification.
  2. Safely unwrap the notification object as a UIScreen.
  3. Enumerate the externalWindows. In the loop where the disconnected screen equals the current iteration screen, remove the window object at the loop index.

This completes the external display disconnection logic flow. Next, you’ll learn to accommodate for different external display resolutions.

Accommodating external display resolutions

Depending on the external display size, you may want to display the UI differently. In this section, you’ll set the music genre label’s visibility based on the display resolution.

Add the following method to MusicSamplingController in MusicSamplingController.swift:

// 1
private func setMusicGenreLabelVisibility(screen: UIScreen) {
  // 2
  guard let currentMode = screen.currentMode else { return }
  // 3
  screen.availableModes.forEach {
    print("Available mode:", $0)
  }
  // 4
  let lowerBoundSize = CGSize(width: 1024, height: 768)
  // 5
  self.musicPlayerViewController.musicGenreLabel.isHidden =
    currentMode.size.width <= lowerBoundSize.width
}

With this code, you:

  1. Create a method that takes a UIScreen parameter.
  2. Safely unwrap the screen’s current mode for the screen’s resolution.
  3. Print the available modes of the screen to the console, which lets you know about the available resolutions.
  4. Create a lower bound size object to compare width with the current screen mode size. The comparison will help decide which user interface to show/hide.
  5. The music genre label is hidden when the screen width is less than or equal to the lower bound size width.

Add the following line of code to observeConnectedScreen() as the last line inside the notification closure:

self.setMusicGenreLabelVisibility(screen: screen)

This ensures the external display connection event invokes the music genre label’s visibility presentation logic.

Finally, add the following code to observeScreenResolutionChanges():

// 1
notificationCenter.addObserver(
  forName: UIScreen.modeDidChangeNotification,
  object: nil, 
  queue: nil) { [weak self] notification in
    // 2
    guard 
      let self = self,
      let screen = notification.object as? UIScreen 
      else { return }

    // 3
    self.setMusicGenreLabelVisibility(screen: screen)
}

Here’s what you did:

  1. Observe for the screen resolution change notification.
  2. Safely unwrap the notification object as UIScreen.
  3. Call setMusicGenreLabelVisibility(screen:) and pass in the safely unwrapped screen object.

Build and run.

An app running on a 1024×768 resolution display will look like this:

img

And an app running on a 3840×2160 (4K) resolution display will look like this:

img

Note

If you’re testing on the simulator, launching an app with a connected external display defaults to the screen’s first screen mode. For example, a connected 1280×720 simulator external display two screen modes, 720×480 and 1280×720. The app defaults to the former screen mode.

There you go! You’ve learned how to extract a screen’s resolution, check the available resolutions and use screen resolution information to adjust your layout.

Now, ensure the rest of the app looks great.

Build and run. Tap the rock, jazz and pop buttons. You will see the following on the external display:

Rock:

img

Jazz:

img

Pop:

img

Excellent, the UI looks great on the external display. You’ve learned the intricacies of supporting external displays. By supporting an extensive range of iOS features, your users can expect seamless plug-and-play experiences from your apps.

Even when the display size is a 24” monitor, Auto Layout adapts accordingly. Your users can expect beautifully laid out user interfaces from your app no matter how or on which device they use it on.

Key points

  • The two native external display solutions in iOS are AirPlay and a physical cable.
  • Use NotificationCenter to observe existing, new and disconnected external display connections.
  • Extract and adapt the app’s UI to the screen resolution using the external display screen mode attribute.