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.
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/.
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!)
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.
Related
I have an old Xcode project without UISceneDelegate methods. Is it possible to migrate an old Xcode project to a new one with UISceneDelegate methods BUT still maintaining compatibility with iOS 12?
If so, how? Because I see a lot of bugs in iOS 14 for which the only workaround is using UISceneDelegate methods.
EDIT 1:
Make sure you query for windowOrientation, after View Controller's view is rendered. Typically in viewDidLoad() and viewWillAppear(_:), view.window is nil, check this answer. Just check value for windowOrientation in viewDidAppear(_:).
If you have some issues to access this value even before try the following definition
private var windowOrientation: UIInterfaceOrientation {
if #available(iOS 13.0, *) {
return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .unknown
} else {
// Fallback on earlier versions
return UIApplication.shared.statusBarOrientation
}
}
I am not sure if your app uses multiple windows or not but if there is only one(since you are not creating any window programatically), the following definition should work fine.
var hasTopNotch: Bool {
return UIApplication.shared.windows.first?.safeAreaInsets.top ?? 0 > 20
}
Original Answer:
UISceneDelegate has been introduced in iOS 13.0 so no way to be compatible with iOS 12, you need to depend on UIApplicationDelegate totally. To support UISceneDelegate in for iOS 13.x, you need to add explicit availability checking to avoid compilation error.
Steps 1: Add Scene Manifest in Info.plist
Open Info.plist as Source Code and add the following
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<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>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
Step 2: Create SceneDelegate.swift file with the following content
import UIKit
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
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 _ = (scene as? UIWindowScene) else { return }
}
func sceneDidDisconnect(_ scene: UIScene) {
// Called as the scene is being released by the system.
// This occurs shortly after the scene enters the background, or when its session is discarded.
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}
func sceneDidBecomeActive(_ scene: UIScene) {
// Called when the scene has moved from an inactive state to an active state.
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
}
func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}
func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}
func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
}
}
Step 3: Update AppDelegate
Add UISceneSession Lifecycle methods.
// MARK: UISceneSession Lifecycle
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
#available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
ii) Finally refer to Apple Documentation. You may refer to https://dev.to/kevinmaarek/add-a-scene-delegate-to-your-current-project-5on for additional clean up and setup tasks.
If I compile onto an iOS 12 device (doesn't use UIScene) and AirPlay Mirror to my Apple TV the app is mirrored as expected to the TV.
On an iOS 13 device, it seems to treat it as an external display where it's formatted to fit the screen (but I have no way to control it).
I'd prefer the old functionality of just mirroring it.
How do I accomplish mirroring on iOS 13? I'm digging around in the docs for:
application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
And in the UISceneConfiguration there's a role property (it has UISceneSession.Role.windowExternalDisplay when I try to AirPlay Mirror) but it doesn't seem to have any value like UISceneSession.Role.windowMirror.
I've been playing around with mirroring and external displays and various possibilities exist with just the right combination of code/settings but certain functionality doesn't seem possible.
Under iOS 13 (with an app built with a Base SDK of iOS 13), you can get your app to be mirrored on an external display. But making this work prevents your app from showing different content on an external display. Basically your app only mirrors or it only shows a unique scene for an external display.
If you wish to only have your app be mirrored, then ensure the following:
Remove the application(_:configurationForConnecting:options:) from your App Delegate.
In the Info.plist, make sure there is no entry for the "External Display Session Role" under the "Scene Configuration" section of the "Application Scene Manifest".
If neither of those two things are part of your app then your app will simple mirror to any external screen when you activate Screen Mirroring on the iOS device.
Just ran into this issue myself. My solution actually came from within my UIWindowSceneDelegate class.
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
// External displays should not get assigned a window. When a window isn't assigned, the default behavior is mirroring.
guard session.role != .windowExternalDisplay else { return }
/* the rest of your setup */
}
When you don't assign a window, it seems that mirroring becomes the default option. Before that change, my external displays (screen mirroring) were given their own unique UIWindow instance.
I don't see this documented anywhere, and it is not intuitive. Because of this, I'm somewhat fearful that it will break in the future.
Hope it still helps.
I found that with Objective-C implementation, you can achieve the screen mirroring behavior by returning nil in application:configurationForConnectingSceneSession:options:.
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options {
if (connectingSceneSession.role == UIWindowSceneSessionRoleExternalDisplay) {
return nil;
}
UISceneConfiguration *configuration = [[UISceneConfiguration alloc] initWithName:#"Main" sessionRole:connectingSceneSession.role];
configuration.storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
configuration.delegateClass = [SceneDelegate class];
configuration.sceneClass = [UIWindowScene class];
return configuration;
}
Be aware that this is not a documented way and may break in the future.
Edited:
In Swift, you can achieve this via method swizzling:
#UIApplicationMain
class AppDelegate : UIResponder, UIApplicationDelegate {
override init() {
_ = AppDelegate.performSceneConfigurationSwizzle
super.init()
}
private static let performSceneConfigurationSwizzle: Void = {
method_exchangeImplementations(
class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.application(_:configurationForConnecting:options:)))!,
class_getInstanceMethod(AppDelegate.self, #selector(AppDelegate.swizzle_application(_:configurationForConnecting:options:)))!
)
}()
#objc func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
fatalError("Should never reach.")
}
#objc private func swizzle_application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration? {
if connectingSceneSession.role == .windowExternalDisplay {
return nil
}
// build scene configuration as usual…
}
}
Instead of implementing the AppDelegate scene configuration method in iOS 13:
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
configuration.delegateClass = SceneDelegate.self
return configuration
}
I instead switched to using the Info.plist variant (and removed the above code) where you effectively specify all the above in your Info.plist instead. (For an up to date version of what's expected in the Info.plist file, simply create a New Project in Xcode and copy the contents from the new Info.plist file for the Application Scene Manifest key).
It now works perfectly and AirPlay Mirror mirrors as expected. I did try changing the role to windowApplication as iOS seemingly does with the Info.plist variant but it still doesn't work.
This question already has answers here:
Why is manually setup root view controller showing black screen?
(3 answers)
Closed 3 years ago.
I want to initialize window inside appDelegate to show specific ViewController depend on some cases. Now I have this code:
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow()
let rootNavigationController = UIViewController()
window?.rootViewController = rootNavigationController
window?.rootViewController?.view.backgroundColor = .green
window?.makeKeyAndVisible()
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
I'm using XCode11 and have created new project. SceneDelegate file I removed cause it hasn't effect on this. Also removed Main from Info.plist and from deployment info
As result on device I see black screen but debugger show me that rootNavigationController as should be
image from debugger
How fix it or implement this logic for XCode11?
Solution:
1)Inside manifest(plist) file remove Storyboard Name field
2)inside SceneDelegate.swift implement:
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(windowScene: windowScene)
let rootNavigationController = UIViewController()
window?.rootViewController = rootNavigationController
window?.rootViewController?.view.backgroundColor = .green
window?.makeKeyAndVisible()
}
And it will work for ios 13 , if you want support ios 12 and lower you need also implement this logic in AppDelegate
I'm setting up my app with multiple windows. It's working well. But when I open my app from the springboard, it creates a new window every time.
I'm on the latest Xcode & iPadOS 13.0 betas. All my viewcontrollers, views, etc, are made programmatically. My only storyboard is the LaunchScreen.
Info.plist
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default</string>
<key>UISceneDelegateClassName</key>
<string>ComicReader.SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
AppDelegate.swift
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
UISceneConfiguration(name: "Default", sessionRole: connectingSceneSession.role)
}
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let scene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: scene)
window?.rootViewController = DelegateHelper.rootViewController()
window?.makeKeyAndVisible()
}
}
In the Apple's Gallery sample, if open the app, swipe to the home screen, then open the app again, I'm back where I were, without calling scene(_:willConnectTo) again. On my own app, scene(_:willConnectTo) is called each time that I open the app, and putting breakpoints shows me that I indeed receive differents UIScene & UISceneSession objects at each launch.
I didn't show any NSUserActivity code because I first though it was because I didn't have any state restoration yet. Implemeting it doesn't change a thing.
If you have some ideas, I'm happy to read you!
So, I'm looking for this since last week. Today, I decided to comment all my AppDelegate, SceneDelegate, & keep only one scene configuration in Info.plist. Rewrite AppDelegate & SceneDelegate from the default template to mine progressively.
It works on first try with default template. I rewrite everything identical… stills works.
The problem? The "Default" configuration in Info.plist in UIWindowSceneSessionRoleApplication array was "Item 1", not "Item 0". git stash everything and only reorder that made it work too.
I hope this helps someone.
I have an app made in Xcode 10 using Main.storyboard and would like to migrate it to use Apple's new framework: SwiftUI.
Is that already possible?
I have already tried to add the UIApplicationSceneManifest key in Info.plist, I changed the AppDelegate.swift to use scenes, I created the SceneDelegate.swift and even then I could not
I assume you're using Xcode 11 GM and macOS Mojave or Catalina.
Along with the changes in the plist, you have to add UISceneSession lifecycle functions in the application delegate.
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// The name must match the one in the Info.plist
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
}
Also, you need to make sure the window is created correctly in the SceneDelegate.
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else {
return
}
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
where ContentView is the main SwiftUI view you want to display.
P.S. Make sure the plist specifies $(PRODUCT_MODULE_NAME).SceneDelegate as delegate class name, and the scene delegate is called SceneDelegate
Example:
If you're on Catalina, you can turn on Previews in the build settings for your target.
Build Options -> Enable Previews
Addendum I:
Make sure you remove the Storyboard key from the Info.Plist and that you're targeting iOS 13.
Addendum II:
Clean Derived Data, as many devs in the comments suggest.
It's a correct only minor change
In SceneDelegate.swift replace
let window = UIWindow(frame: UIScreen.main.bounds)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
with
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
TAKEN FROM HERE
My solution turned out to be different. In my case, I had everything in place, but when the code attempted to load the UISceneConfiguration, it failed to load the config in the Info.plist and gave me a secondary window config instead with no scene delegate set. If I asked for the correct configuration from the debug console it would load as expected. I was confused.
I double checked everything and tried all of the suggestions here but none worked. In the end I did 'Hardware' - 'Erase all contents and settings...' on the simulator and that solved it.
My guess is that because I'd been running the pre-SwiftUI version of the app on the simulator, something in that caused the SwiftUI version to behave differently.