top of page
Writer's pictureHui Wang

47. CarPlay: Compatible with UIScene

Updated: Sep 13, 2022


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


bottom of page