Implementing external monitor support in SwiftUI

I modified the example from the Big Nerd Ranch blog to work as follows.

  1. Remove Main Storyboard: I removed the main storyboard from a new project. Under deployment info, I set Main interface to an empty string.

  2. Editing plist: Define your two scenes (Default and External) and their Scene Delegates in the Application Scene Manifest section of your plist.

    <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <true/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneConfigurationName</key>
                    <string>Default Configuration</string>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                </dict>
            </array>
            <key>UIWindowSceneSessionRoleExternalDisplay</key>
            <array>
                <dict>
                    <key>UISceneDelegateClassName</key>
                    <string>$(PRODUCT_MODULE_NAME).ExtSceneDelegate</string>
                    <key>UISceneConfigurationName</key>
                    <string>External Configuration</string>
                </dict>
            </array>
        </dict>
    </dict>
  1. Edit View Controller to show a simple string:
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .blue
        view.addSubview(screenLabel)
    }

    var screenLabel: UILabel = {
        let label = UILabel()
        label.textColor = UIColor.white
        label.font = UIFont(name: "Helvetica-Bold", size: 22)
        return label
    }()

    override func viewDidLayoutSubviews() {
        /* Set the frame when the layout is changed */
        screenLabel.frame = CGRect(x: 0,
                                y: 0,
                                width: view.frame.width - 30,
                                height: 24)
    }
}
  1. Modify scene(_:willConnectTo:options:) in SceneDelegate to display information in the main (iPad) window.
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
        guard let windowScene = (scene as? UIWindowScene) else { return }

        window = UIWindow(frame: windowScene.coordinateSpace.bounds)
        window?.windowScene = windowScene
        let vc = ViewController()
        vc.loadViewIfNeeded()
        vc.screenLabel.text = String(describing: window)
        window?.rootViewController = vc
        window?.makeKeyAndVisible()
        window?.isHidden = false
    }
  1. Make a scene delegate for your external screen. I made a new Swift file ExtSceneDelegate.swift that contained the same text as SceneDelegate.swift, changing the name of the class from SceneDelegate to ExtSceneDelegate.

  2. Modify application(_:configurationForConnecting:options:) in AppDelegate. Others have suggested that everything will be fine if you just comment this out. For debugging, I found it helpful to change it to:

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {

        // This is not necessary; however, I found it useful for debugging
        switch connectingSceneSession.role.rawValue {
            case "UIWindowSceneSessionRoleApplication":
                return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
            case "UIWindowSceneSessionRoleExternalDisplay":
                return UISceneConfiguration(name: "External Configuration", sessionRole: connectingSceneSession.role)
            default:
                fatalError("Unknown Configuration \(connectingSceneSession.role.rawValue)")
            }
    }
  1. Build and run the app on iOS. You should see an ugly blue screen with information about the UIWindow written. I then used screen mirroring to connect to an Apple TV. You should see a similarly ugly blue screen with different UIWindow information on the external screen.

For me, the key reference for figuring all of this out was https://onmyway133.github.io/blog/How-to-use-external-display-in-iOS/.