Search code examples
iosswiftswiftuiios13

Implementing external monitor support in SwiftUI


I'm confused about implementing external monitor support via Airplay with SwiftUI.

In SceneDelegate.swift I'm using UIScreen.didConnectNotification observer and it actually detects a new screen being attached but I'm unable to assign a custom UIScene to the screen.

I found a few good examples using Swift with iOS12 and lower, but none of them work in SwiftUI, since the whole paradigm has been changed to use UIScene instead of UIScreen. Here's the list:

https://www.bignerdranch.com/blog/adding-external-display-support-to-your-ios-app-is-ridiculously-easy/

https://developer.apple.com/documentation/uikit/windows_and_screens/displaying_content_on_a_connected_screen

https://www.swiftjectivec.com/supporting-external-displays/

Apple even spoke about it last year

Perhaps something changed and now there is a new way to do this properly. Moreover, setting UIWindow.screen = screen has been deprecated in iOS13.

Has anyone already tried implementing an external screen support with SwiftUI. Any help is much appreciated.


Solution

  • 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.com/posts/how-to-use-external-display-in-ios/.