Implementing external monitor support in SwiftUI
I modified the example from the Big Nerd Ranch blog to work as follows.
Remove Main Storyboard: I removed the main storyboard from a new project. Under deployment info, I set Main interface to an empty string.
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>
- 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)
}
}
- 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
}
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.
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)")
}
}
- 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/.