I have a SwiftUI and iPadOS app supporting multiple scenes and many launches. In this post, let's consider only two launch types:
- Normal launch (user taps on the app icon)
- File launch (user taps on the associated file)
I hope to include other launches like launched by quick action, launched by widget, launched by notification etc.
The App
struct conformer is defined as follows:
@main
struct IOSFileAssociationMSApp: App {
@UIApplicationDelegateAdaptor
var appDelegate: AppDelegate
var body: some Scene {
// The default initial scene
DefaultContentScene()
// Launch file content scene
FileContentScene()
.handlesExternalEvents(matching: ["file launch"])
// Scenes for other launch methods
}
init() {
Log("IOSFileAssociationMSApp.init()")
}
}
I have two scene delegates - DefaultContentSceneDelegate
and FileContentSceneDelegate
.
I'm setting one of them based on the type of launch in application(_:configurationForConnecting:options:)
.
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
Log("application(_:configurationForConnecting:options:)")
let sceneConfig: UISceneConfiguration = UISceneConfiguration(name: "mainWindow", sessionRole: connectingSceneSession.role)
let urls: Set<UIOpenURLContext> = options.urlContexts
if(urls.isEmpty) {
Log("Loading default view...")
sceneConfig.delegateClass = DefaultContentSceneDelegate.self
} else {
Log("Loading file view...")
sceneConfig.delegateClass = FileContentSceneDelegate.self
}
return sceneConfig
}
When launched by tapping a file, the scene delegate is FileContentSceneDelegate
(verified from the logs) and its lifecycle methods are getting invoked. But the UI is that of the DefaultContentScene
, not the FileContentScene
. It's weird. I am updating the ViewController
in FileContentSceneDelegate
, but the DefaultContentScene
's ViewController
is displayed.
I'm aware that by default, the scene defined first in the App
struct is launched. But how can I ensure that when the app is launched by file, FileContentScene
is launched? How to route external events to its specific scene?
Here is the rest of the code:
DefaultContentScene, DefaultContentSceneDelegate, DefaultContentView, DefaultContentViewController
:
struct DefaultContentScene: Scene {
var body: some Scene {
WindowGroup {
DefaultContentView()
}
}
init() {
Log("DefaultContentScene.init()")
}
}
class DefaultContentSceneDelegate: UIResponder, UIWindowSceneDelegate {
// Scene is constructed
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Log("DefaultContentSceneDelegate.scene(_:willConnectTo:options:)")
Log("scene.session.persistentIdentifier = " + scene.session.persistentIdentifier)
if (connectionOptions.urlContexts.isEmpty) {
Log("Not launched using file!")
} else {
// Flow should not reach here.
Log("Launched using file!")
if (connectionOptions.urlContexts.count == 1) {
let fileUrl: URL = connectionOptions.urlContexts.first!.url
Log("fileUrl = " + fileUrl.path())
StaticContext.fileUrl = fileUrl
} else {
Log("There are multiple url inputs!")
}
}
}
// Other scene delegate methods
}
// I'm using a ViewControllerRepresentable to attach a UIKit view to a SwiftUI view.
struct DefaultContentView: UIViewControllerRepresentable {
init() {
Log("DefaultContentView.init()")
}
func makeUIViewController(context: Context) -> some UIViewController {
Log("DefaultContentView.makeUIViewController(_:)")
let viewController: DefaultContentViewController = StaticContext.defaultContentViewController
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
Log("DefaultContentView.updateUIViewController(_:)")
}
}
class DefaultContentViewController: UIViewController {
override func viewDidLoad() {
Log("DefaultContentViewController.viewDidLoad()")
updateView()
}
func updateView() {
let label: UILabel = UILabel()
label.frame = CGRectMake(self.view.bounds.size.width/2,50,self.view.bounds.size.width, self.view.bounds.size.height)
label.font = UIFont(name:"HelveticaNeue-Bold", size: 25.0)
label.textAlignment = .center
label.center = self.view.center
label.text = StaticContext.displayMessage
view.backgroundColor = .green
// Remove previously added view.
if(view.subviews.count != 0) {
Log(String.init(format: "Number of subviews = %d", view.subviews.count))
Log("Removing old views...")
view.subviews[0].removeFromSuperview()
}
view.addSubview(label)
}
}
Similarly, FileContentScene, FileContentSceneDelegate, FileContentView, FileContentViewController
:
struct FileContentScene: Scene {
var body: some Scene {
WindowGroup {
FileContentView()
}
}
init() {
Log("FileContentScene.init()")
}
}
class FileContentSceneDelegate: UIResponder, UIWindowSceneDelegate {
// Scene is constructed
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
Log("FileContentSceneDelegate.scene(_:willConnectTo:options:)")
Log("scene.session.persistentIdentifier = " + scene.session.persistentIdentifier)
if (connectionOptions.urlContexts.isEmpty) {
// Flow should not reach here
Log("Not launched using file!")
} else {
Log("Launched using file!")
for (index, urlContext) in connectionOptions.urlContexts.enumerated() {
let url: URL = urlContext.url
Log(String(format: "file[%d] = %@", index, url.path()))
StaticContext.fileUrl = url
StaticContext.fileContentViewController.updateView()
}
}
}
// Handle file launch
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
Log("FileContentSceneDelegate.scene(_:openURLContexts:)")
for (index, urlContext) in URLContexts.enumerated() {
let url: URL = urlContext.url
Log(String(format: "file[%d] = %@", index, url.path()))
StaticContext.fileUrl = url
StaticContext.fileContentViewController.updateView()
}
}
}
struct FileContentView: UIViewControllerRepresentable {
init() {
Log("FileContentView.init()")
}
func makeUIViewController(context: Context) -> some UIViewController {
Log("FileContentView.makeUIViewController(_:)")
let viewController: FileContentViewController = StaticContext.fileContentViewController
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
Log("FileContentView.updateUIViewController(_:)")
}
}
class FileContentViewController: UIViewController {
override func viewDidLoad() {
Log("viewDidLoad()")
updateView()
}
func updateView() {
let label: UILabel = UILabel()
label.frame = CGRectMake(self.view.bounds.size.width/2,50,self.view.bounds.size.width, self.view.bounds.size.height)
label.font = UIFont(name:"HelveticaNeue-Bold", size: 25.0)
label.textAlignment = .center
label.center = self.view.center
if let file: URL = StaticContext.fileUrl {
label.text = "File name = " + file.lastPathComponent
view.backgroundColor = .cyan
} else {
// Flow shouldn't reach here
label.text = "File name = nil"
view.backgroundColor = .red
}
// Remove previously added view.
if(view.subviews.count != 0) {
Log(String.init(format: "Number of subviews = %d", view.subviews.count))
Log("Removing old views...")
view.subviews[0].removeFromSuperview()
}
view.addSubview(label)
}
}
When I launch the app by tapping a file, I have the following logs:
[IOSFileAssociationMS]: IOSFileAssociationMSApp.init()
[IOSFileAssociationMS]: application(_:willFinishLaunchingWithOptions:)
[IOSFileAssociationMS]: RegisterForLifecycleEvents()
[IOSFileAssociationMS]: application(_:didFinishLaunchingWithOptions:)
[IOSFileAssociationMS]: application(_:configurationForConnecting:options:)
[IOSFileAssociationMS]: Loading file view... // Perfect. The FileContentSceneDelegate is set.
[IOSFileAssociationMS]: DefaultContentScene.init() // Not sure why the DefaultContentScene is initialised?
[IOSFileAssociationMS]: FileContentScene.init()
[IOSFileAssociationMS]: DefaultContentView.init()
[IOSFileAssociationMS]: FileContentView.init()
[IOSFileAssociationMS]: FileContentSceneDelegate.scene(_:willConnectTo:options:)
[IOSFileAssociationMS]: Launched using file!
[IOSFileAssociationMS]: fileUrl = /Users/charan.karthick/Library/Developer/CoreSimulator/Devices/3BD55EE4-217F-4E06-887C-17C92BBC869E/data/Library/Mobile 0ocuments/com~apple~CloudDocs/CSKFile.csk
[IOSFileAssociationMS]: FileContentSceneDelegate.sceneWillEnterForeground(_:)
[IOSFileAssociationMS]: Received NSNotificationName(_rawValue: UIApplicationWillEnterForegroundNotification)
[IOSFileAssociationMS]: DefaultContentView.makeUIViewController(_:) // Why? Despite the FileContentSceneDelegate invoked, DefaultContentView is used.
[IOSFileAssociationMS]: DefaultContentViewController.viewDidLoad()
[IOSFileAssociationMS]: DefaultContentView.updateUIViewController(_:)
[IOSFileAssociationMS]: FileContentSceneDelegate.sceneDidBecomeActive(_:)
[IOSFileAssociationMS]: Received NSNotificationName(_rawValue: UIApplicationDidBecomeActiveNotification)
Clearly, setting the SceneDelegate is not enough and I'm missing some other setting, but not sure what it is.