Search

Adding External Display Support To Your iOS App Is Ridiculously Easy

Andrew Fitzpatrick

4 min read

Nov 26, 2018

iOS

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.

Josh Justice

Reviewer Big Nerd Ranch

Josh Justice has worked as a developer since 2004 across backend, frontend, and native mobile platforms. Josh values creating maintainable systems via testing, refactoring, and evolutionary design, and mentoring others to do the same. He currently serves as the Web Platform Lead at Big Nerd Ranch.

Speak with a Nerd

Schedule a call today! Our team of Nerds are ready to help

Let's Talk

We are ready to discuss your needs.

Not applicable? Click here to schedule a call.

Stay in Touch WITH Big Nerd Ranch News