Reputation: 2956
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.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.
Upvotes: 7
Views: 2932
Reputation: 178
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>
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)
}
}
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)")
}
}
For me, the key reference for figuring all of this out was https://onmyway133.com/posts/how-to-use-external-display-in-ios/.
Upvotes: 4
Reputation: 191
I was trying the same thing in my SceneDelegate
, but then I realized that UISceneSession
is being defined in UIAppDelegate.application(_:configurationForConnecting:options:)
, which is called when an external screen connects, just like UIScreen.didConnectNotification
. So I added the following code to that existing method:
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
self.handleSessionConnect(sceneSession: connectingSceneSession, options: options)
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func handleSessionConnect(sceneSession: UISceneSession, options: UIScene.ConnectionOptions) {
let scene = UIWindowScene(session: sceneSession, connectionOptions: options)
let win = UIWindow(frame: scene.screen.bounds)
win.rootViewController = UIHostingController(rootView: SecondView())
win.windowScene = scene
win.isHidden = false
managedWindows.append(win)
}
The second screen is connecting correctly. My only uncertainty is that application(_:didDiscardSceneSessions:)
doesn't seem to get called, so I'm not sure how best to manage the windows as they disconnect.
** Follow-up Edit **
I realize that I can use the original UIScreen.didDisconnectNotification
to listen for disconnects.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
NotificationCenter.default.addObserver(forName: UIScreen.didDisconnectNotification, object: nil, queue: nil) { (notification) in
if let screen = notification.object as? UIScreen {
self.handleScreenDisconnect(screen)
}
}
return true
}
func handleScreenDisconnect(_ screen: UIScreen) {
for window in managedWindows {
if window.screen == screen {
if let index = managedWindows.firstIndex(of: window) {
managedWindows.remove(at: index)
}
}
}
}
But since the actual scene session disconnect method isn't being called, I'm not sure if this is incorrect or unnecessary.
Upvotes: 1
Reputation: 2069
Don't know about SwiftUI (I'm die hard ObjectiveC) but in iOS13 you handle application:configurationForConnectingSceneSession:options in the application delegate then look for [connectingSceneSession.role isEqualToString:UIWindowSceneSessionRoleExternalDisplay]
In there you create a new UISceneConfiguration and set its delegateClass to a UIWindowSceneDelegate derived class of your choice (the one you want to manage content on that external display.)
I reckon you can also associate UIWindowSceneSessionRoleExternalDisplay with your UIWindowSceneDelegate in the info.plist file (but I prefer coding it!)
Upvotes: 1