userDidAcceptCloudKitShareWith not being called - ios

func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {...}
does not get called after clicking "OK" on the link to a shared cloudkit record. The app will open, but this function (which is supposed to be called) is not.
CKSharingSupported:YES is listed in my info.plist
I've put the record into a zone called "sharedZone"
SharedZone is showing up in my zone list, on my Private database.
The shared record shows me as being "invited"
Tried deleting app and cloud data and reinstalling, no change
func application(_ application: UIApplication, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("you accepted something")
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas:[cloudKitShareMetadata])
//acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share,
error in
print("share was accepted")
}
acceptShareOperation.acceptSharesCompletionBlock = {
error in
/// Send your user to where they need to go in your app
print("share accepted completion block!")
}
CKContainer(identifier: cloudKitShareMetadata.containerIdentifier).add(acceptShareOperation)
}
Expected result: AT LEAST seeing the line "you accepted something" printed to the console.
UPDATE: Reverting back to IOS11.3 doesn't make it work.
UPDATE: I downloaded a tutorial someone had built that uses cloudkit and (after some finagling, got theirs to work. I then gutted their code and put mine in instead, and NOW the function works properly, so I think the issue may have something to do with either:
XCODE 11 beta, or
The new coredata + cloudkit syncing feature apple added.

If you use fresh Xcode 11 project, there is SceneDelegate.swift.
You need to implement there:
internal func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) { }
If you want to get userDidAcceptCloudKitShareWith called inside AppDelegate.swift you have to remove from Info.plist file next lines:
<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>
UPDATED
If you are trying to accept share on cold start using Scene Manifest, CKShare.Metadata will be passed to func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) in connectionOptions.

To make this work you have to use SceneDelegate.
There are 3 steps to get scenedelegate working.
1 - Add Appdelegate
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("abc")
return true
}
}
Don't forget to use a delegate adaptor if you use SwiftUI way of entering the program.
struct ExampleApp: App {
#UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
MainView()
}
}
Also don't forget to add this to your appdelegate class:
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
if options.userActivities.first?.activityType == "newWindow" {
let configuration = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
configuration.delegateClass = SceneDelegate.self
return configuration
} else {
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
2 - Configure info.plist to handle scenes through a SceneDelegate
add these to your info.plist
<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>
</dict>
</array>
</dict>
</dict>
3 - Now add SceneDelegate and get the delegate callbacks that you need
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func windowScene(_ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata) {
print("abcd")
}
}
}
note: in info.plist and in your appdelegate code there is a reference to "default configuration". You can change this to whatever you want (it's meant to enable management of more than one scene), but they should be the same off course.

After 2 days of trial and error I got it working.
The issue seems to be related to Apples new Core Data + Cloudkit syncing abilities. They seem to gloss over the appdelegate function I need to call.
I fixed the issue by rebuilding the app in Xcode 10, then opening it in XCode 11 without changing anything. I'll let Apple know too.
Thank me. I'm welcome.

Related

Xcode 12 project with iOS 13 SwiftUI support: SceneDelegate functions not invoked

I had created a project in Xcode 12 with iOS 14 as deployment target (bigger project). After some time I realized that I had to also be able to support iOS 13 devices. My project uses SwiftUI.
It is apparently not very straight-forward to simply change the deployment target to iOS 13 as some of the code generated by Xcode 12 is not backwards compatible with iOS 13.
So I essentially need to convert my Xcode 12-based project (currently with deployment target iOS 14.0) to also support iOS 13.
Here's what I did to prepare for my project's downgrade:
Install Xcode 11.7, create new project with SwiftUI support and ensure the deployment target is iOS 13.0. Thereby I know what Xcode believes the "iOS 13"-way should be, i.e., using AppDelegate and SceneDelegate.
Run that app - works fine on my iOS 14 device.
Open Xcode 12, create new project with SwiftUI support, lower the deployment target to iOS 13.0 (because it's default set to 14.x), build it, and be presented with a bunch of compilation issues (as expected).
Changes then made to the Xcode 12 project:
Comment out everything in the <project>App.swift file.
Add AppDelegate.swift file with the same contents from the Xcode 11.7-generated project mentioned above.
Add SceneDelegate.swift file with the same contents from the Xcode 11.7-generated project mentioned above.
Add a LaunchScreen.storyboard file (because Xcode warned about its non-existence).
Run the app.
The app runs, but the contents of ContentView is not shown. Just a black screen (black is probably because my device is in dark mode). Also, as you'll notice in the code below, I've inserted debug logs but only "didFinishLaunchingWithOptions" is printed in the console - that is, the scene delegate methods are not invoked, it seems.
I've (of course) Googled a lot, tried various things with Info.plist, but nothing seems to help.
Code:
AppDelegate.swift
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
print("didFinishLaunchingWithOptions")
return true
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
print("configurationForConnecting")
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
print("didDiscardSceneSessions")
}
}
SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
print("scene willConnectTo")
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: ContentView())
self.window = window
window.makeKeyAndVisible()
}
func sceneDidDisconnect(_ scene: UIScene) {
print("sceneDidDisconnect")
}
func sceneDidBecomeActive(_ scene: UIScene) {
print("sceneDidBecomeActive")
}
func sceneWillResignActive(_ scene: UIScene) {
print("sceneWillResignActive")
}
func sceneWillEnterForeground(_ scene: UIScene) {
print("sceneWillEnterForeground")
}
func sceneDidEnterBackground(_ scene: UIScene) {
print("sceneDidEnterBackground")
}
}
ContentView.swift
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.foregroundColor(.blue)
.padding()
.onAppear {
print("ContentView")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string></string>
<key>LSApplicationCategoryType</key>
<string></string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<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>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
If you've already built app using SwiftApp before implementation SceneDelegate, just uninstall and rebuild it. And then, you can resolve this problem.
persistentIdentifier will be generated by the system for creating new Scene when an app is launched at the first time. After that, the identifier will be same one every app open. So the app doesn't need to create new Scene.
That's why configurationForConnecting wasn't invoke but set up no rootview so it happens that your app shows black window.
The following WWDC19 session video(around 31:40) says about the identifier.
https://developer.apple.com/videos/play/wwdc2019/212/?time=1900.
Hope it will help your problem.
Thank you.
I'm going to guess that you forgot to add the scene delegate file to the app target. When you build and run, look in the console and see if there's a complaint about not finding the scene delegate. That would explain the phenomenon.

Migrating old Xcode project to new one with UISceneDelegate

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.

black screen when I run my iOS application

I'm trying to do a new iOS app in Xcode. I made a main storyboard and I added a label on my ViewController. When I run my application, first second it show the label and then become the screen black without any errors.
I'm working on Xcode 11 (Swift 5) and this message appears on output:
[SceneConfiguration] Info.plist configuration "Default Configuration" for UIWindowSceneSessionRoleApplication contained UISceneDelegateClassName key, but could not load class with name "gina.SceneDelegate"
I don't know where my mistake is.
iOS 13 & above
Only if target is 13 or greater.
SceneDelegate is not supported before iOS 13. If you want to use SceneDelegate and also want to support iOS prior to iOS 13 then you a have to add some changes to your project.
Add availability attribute to the whole class in SceneDelegate.swift file.
#available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
...
}
AppDelegate.swift file has two new SceneDelegate method. Add availability attribute to them as well.
#available(iOS 13.0, *)
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
...
}
#available(iOS 13.0, *)
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
...
}
Lastly, add UIWindow object in AppDelegate.swift.
class AppDelegate: UIResponder, UIApplicationDelegate {
//Add this line
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
...
}
iOS 12 and earlier
AppDelegate needs a UIWindow property. iOS 13 uses SceneDelegate in new projects. Specify the UIWindow object and remove the SceneDelegate.swift file.
If you have removed the SceneDelegate from project, then you must remove the Application Scene Manifest dictionary from Info.plist.
You need to initialize the window like this:
let window = UIWindow(windowScene: scene as! UIWindowScene)
and add these in info.plist:
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
That's all you need to do.

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.
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.

iOS 13 app requests a new scene each time the app icon is tapped

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.

Resources