Using the CarPlay framework in iOS 14 and later to develop a CarPlay app must use UIScene (Apple introduced UIScene in iOS 13 for building multi-window applications), so your project must change from traditional UIWindow and AppDelegate to SceneDelegate. You can skip this step if your project is already compatible with UIScene.
References:
What is UIScene?
Before iOS 13, in terms of functional responsibilities, UIApplication was responsible for App state, and UIApplicationDelegate (AppDelegate) was responsible for App events and life cycle, including process and UI.
It is okay for single-window apps, but this division of functional responsibilities is no longer supported when developing multi-window iPad apps or Mac Catalyst apps.
Therefore, Apple introduced UIScene for building multi-window apps in iOS 13 and split the functional responsibilities, handing over UI-related states, events, and life cycles to UIWindowScene and UIWindowSceneDelegate (SceneDelegate), and UISceneSession is responsible for persistent UI state.
Compatible with UIScene
Since UIScene is only available on iOS 13 and above, you cannot fully use SceneDelegate if your app's minimum version supports lower than iOS 13.
Summarize:
iOS 13 and above: Use AppDelegate + SceneDelegate
Lower than iOS 13: Only AppDelegate can be used
Steps to implement UIScene:
1. Add the following key-value to Info.plist
Enable Multiple Windows
Please set it to YES.
UIWindowSceneSessionRoleApplication
It's an array that configures your app's scenes, each with four parameters:
UISceneClassName: The name of class
UISceneConfigurationName: The name of current configuration
UISceneDelegateClassName: The name of the delegate is associated with
UISceneStoryboardFile: The name of storyBoard if has
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegateName</string>
</dict>
</array>
</dict>
</dict>
2. Modify AppDelegate
Due to changes in the functionality and responsibilities of the classes, some previous implementations in AppDelegate need to be migrated to SceneDelegate.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
// MARK: Compatible with UIScene
extension AppDelegate {
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: SceneDelegate.configurationName, 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.
}
}
3. Adding SceneDelegate
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
static let configurationName = "Default Configuration"
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene), session.configuration.name == Self.configurationName else { return }
setupKeyWindow(in: windowScene)
}
func setupKeyWindow(in scene: UIWindowScene) {
// 1. create window
let window = UIWindow(windowScene: scene)
window.makeKeyAndVisible()
window.overrideUserInterfaceStyle = .dark
// 2. do something after window created
}
func sceneDidDisconnect(_ scene: UIScene) {
guard scene.session.configuration.name == Self.configurationName else { return }
}
func sceneDidBecomeActive(_ scene: UIScene) {
guard scene.session.configuration.name == Self.configurationName else { return }
UIApplication.shared.delegate?.applicationDidBecomeActive?(UIApplication.shared)
}
func sceneWillResignActive(_ scene: UIScene) {
guard scene.session.configuration.name == Self.configurationName else { return }
UIApplication.shared.delegate?.applicationWillResignActive?(UIApplication.shared)
}
func sceneWillEnterForeground(_ scene: UIScene) {
guard scene.session.configuration.name == Self.configurationName else { return }
UIApplication.shared.delegate?.applicationWillEnterForeground?(UIApplication.shared)
}
func sceneDidEnterBackground(_ scene: UIScene) {
guard scene.session.configuration.name == Self.configurationName else { return }
UIApplication.shared.delegate?.applicationDidEnterBackground?(UIApplication.shared)
}
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard scene.session.configuration.name == Self.configurationName else { return }
guard let url = URLContexts.first?.url else { return }
_ = UIApplication.shared.delegate?.application?(UIApplication.shared, open: url, options: [:])
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard scene.session.configuration.name == Self.configurationName else { return }
_ = UIApplication.shared.delegate?.application?(UIApplication.shared, continue: userActivity, restorationHandler: { _ in })
}
}
4. SceneHelper: used to get the scene
import UIKit
import CarPlay
final class SceneHelper {
private static var connectedScenes: Set<UIScene> {
UIApplication.shared.connectedScenes
}
static var main: UIWindowScene? {
connectedScenes
.first(where: { $0 is UIWindowScene && $0.session.configuration.name == SceneDelegate.configurationName })
.flatMap({ $0 as? UIWindowScene })
}
static var carPlay: CPTemplateApplicationScene? {
connectedScenes
.first(where: { $0 is CPTemplateApplicationScene })
.flatMap({ $0 as? CPTemplateApplicationScene })
}
}
5. Summarize
After using UIScene, the UI hierarchy has undergone some changes, and a layer of UIWindowScene has been added to the original UIScreen and UIWindow layers.
The UIWindow also adds a windowScene property and the windowScene constructor. A UIWindow must be initialized with windowScene or set the windowScene property to be displayed on the screen.
Follow me on:
Comments