Adding External Display Support To Your iOS App Is Ridiculously Easy
Get Ready For The Big Screen(s)
Apple made a big splash this week with the new iPad Pro.
In the promo videos, they’ve shown off using the USB-C port to connect the iPad to an external display for creative tasks.
This is a feature that few people know already exists on all iOS devices.
You can connect an external display via a lightning adapter or AirPlay Screen Mirroring to an Apple TV.
With this small amount of code, you can listen for the connection/disconnection of displays and set up a separate window and view controller hierarchy for the external display to augment your app’s main content.
import UIKit
class ViewController: UIViewController {
// For demo purposes. We're just showing a string description
// of each UIScreen object on each screen's view controller
@IBOutlet var screenLabel: UILabel!
static func makeFromStoryboard() -> ViewController {
return UIStoryboard(name: "Main",
bundle: nil)
.instantiateInitialViewController() as! ViewController
}
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// The main window shown on the device's display
// The main storyboard will set this up automatically
var window: UIWindow?
// References to our windows that we're creating
var windowsForScreens = [UIScreen: UIWindow]()
// Create our view controller and add text to our test label
private func addViewController(to window: UIWindow, text: String) {
let vc = ViewController.makeFromStoryboard()
// When we need to finish loading the view before accessing
// the label outlet on the view controller
vc.loadViewIfNeeded()
vc.screenLabel.text = text
window.rootViewController = vc
}
// Create and set up a new window with our view controller as the root
private func setupWindow(for screen: UIScreen) {
let window = UIWindow()
addViewController(to: window, text: String(describing: screen))
window.screen = screen
window.makeKeyAndVisible()
windowsForScreens[screen] = window
}
// Hide the window and remove our reference to it so it will be deallocated
private func tearDownWindow(for screen: UIScreen) {
guard let window = windowsForScreens[screen] else { return }
window.isHidden = true
windowsForScreens[screen] = nil
}
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Set up the device's main screen UI
addViewController(to: window!, text: String(describing: UIScreen.main))
// We need to set up the other screens that are already connected
let otherScreens = UIScreen.screens.filter { $0 != UIScreen.main }
otherScreens.forEach { (screen) in
setupWindow(for: screen)
}
// Listen for the screen connection notification
// then set up the new window and attach it to the screen
NotificationCenter.default
.addObserver(forName: UIScreen.didConnectNotification,
object: nil,
queue: .main) { (notification) in
// UIKit is nice enough to hand us the screen object
// that represents the newly connected display
let newScreen = notification.object as! UIScreen
self.setupWindow(for: newScreen)
}
// Listen for the screen disconnection notification.
NotificationCenter.default.addObserver(forName: UIScreen.didDisconnectNotification,
object: nil,
queue: .main) { (notification) in
let newScreen = notification.object as! UIScreen
self.tearDownWindow(for: newScreen)
}
return true
}
}
Oh Wow.
Yeah, it’s wildly simple.
Make a window, add a root view controller to it, and set the window’s screen to the external screen.
When the screen’s disconnected, just hide and nil out the reference to the window so it can be deallocated.
Transition Gracefully
You could mix mirroring the UI and setting up a dedicated external interface by setting up and tearing down the external window during specific phases of interaction with your app.
For example: a social media app could keep the default mirroring behavior for most of the UI, but present a full screen gallery slideshow when viewing a user’s photos.
When the external window isn’t set up, iOS will default to mirroring the screen, so tearing the window down will automatically show the mirrored screen again.
Keep in mind that the user could connect or disconnect a display at any time during your app’s execution.
Apple recommends listening for these connection/disconnection notifications and gracefully transitioning your UI.
The example they give is a photo gallery app, where you can select photos from a grid to view them in fullscreen.
If the user is viewing a photo in fullscreen then plugs in a display, the app’s main screen should pop back to the photo grid and highlight the selected photo, while the photo is shown fullscreen on the external display.
The transition makes it obvious that the user is now controlling what is displayed on the second screen with the device in their hands.
When the display is disconnected, the app should push the selected photo back into fullscreen on the iOS device’s screen.
More Considerations
A few caveats to know about when adding external display support to your app:
- On iPad, only the primary app in multitasking can access external displays. If your app supports multitasking, make sure to account for this and communicate it to your users so they understand this edge-case.
- The external display doesn’t receive any input events, so don’t put interactive content on that display.
- The device renders the external display’s content. This could be a performance hit if your app is already using a lot of CPU or GPU power, so make sure to profile your app and optimize as much as possible.
- If using AirPlay mirroring, the device will be sending compressed video over the network. There will be some streaming artifacts on the external display. It may be appropriate for your app to make users aware of this if they are expecting perfect fidelity (for instance, in a pro content creation app).
- External display resolutions need to be accounted for. Check out the documentation on UIScreen for more info on UIScreenModes.