From 52a68e28ddaaf94155295ddfa205007b23bb5452 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Mon, 25 Jul 2022 17:22:11 -0500 Subject: [PATCH] Switches to UIScene-based app lifecycle --- Delta.xcodeproj/project.pbxproj | 4 + Delta/AppDelegate.swift | 18 ++ Delta/Deep Linking/DeepLinkController.swift | 12 +- Delta/SceneDelegate.swift | 175 ++++++++++++++++++++ Delta/Supporting Files/Info.plist | 21 +++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 Delta/SceneDelegate.swift diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 7552c89..f63e803 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -169,6 +169,7 @@ D524F4A3273DE9C000D500B2 /* ProcessInfo+JIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */; }; D524F4A5273DEBB400D500B2 /* ServerManager+Delta.swift in Sources */ = {isa = PBXBuildFile; fileRef = D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */; }; D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; }; + D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -365,6 +366,7 @@ D524F4A2273DE9C000D500B2 /* ProcessInfo+JIT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+JIT.swift"; sourceTree = ""; }; D524F4A4273DEBB400D500B2 /* ServerManager+Delta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ServerManager+Delta.swift"; sourceTree = ""; }; D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = ""; }; + D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -828,6 +830,7 @@ isa = PBXGroup; children = ( BFFA71DC1AAC406100EE9DD1 /* AppDelegate.swift */, + D5A98CE1284EF14B00E023E5 /* SceneDelegate.swift */, BFFA71E01AAC406100EE9DD1 /* Main.storyboard */, BFFC46211D59848000AF2CC6 /* Launch */, BF46894D1AAC469800A2586D /* Game Selection */, @@ -1192,6 +1195,7 @@ BF797A2D1C2D339F00F1A000 /* UILabel+FontSize.swift in Sources */, BF80E1D21F13117000847008 /* ControllerInputsViewController.swift in Sources */, BF5942641E09BBB10051894B /* LoadControllerSkinImageOperation.swift in Sources */, + D5A98CE2284EF14B00E023E5 /* SceneDelegate.swift in Sources */, BFE56E1923EB7BE00014FECD /* UIImage+SymbolFallback.swift in Sources */, BF6EE5E91F7C5F860051AD6C /* _GameControllerInputMapping.swift in Sources */, BF5942871E09BC8B0051894B /* _ControllerSkin.swift in Sources */, diff --git a/Delta/AppDelegate.swift b/Delta/AppDelegate.swift index aade6e3..c4f7169 100644 --- a/Delta/AppDelegate.swift +++ b/Delta/AppDelegate.swift @@ -114,6 +114,24 @@ class AppDelegate: UIResponder, UIApplicationDelegate } } +@available(iOS 13, *) +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: "Main", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) + { + // 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. + } +} + private extension AppDelegate { func registerCores() diff --git a/Delta/Deep Linking/DeepLinkController.swift b/Delta/Deep Linking/DeepLinkController.swift index a64d9db..446edc1 100644 --- a/Delta/Deep Linking/DeepLinkController.swift +++ b/Delta/Deep Linking/DeepLinkController.swift @@ -23,8 +23,16 @@ extension UIViewController struct DeepLinkController { private var window: UIWindow? { - guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil } - return window + if #available(iOS 13, *) + { + guard let delegate = UIApplication.shared.connectedScenes.lazy.compactMap({ $0.delegate as? UIWindowSceneDelegate }).first, let window = delegate.window else { return nil } + return window + } + else + { + guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil } + return window + } } private var topViewController: UIViewController? { diff --git a/Delta/SceneDelegate.swift b/Delta/SceneDelegate.swift new file mode 100644 index 0000000..6b0ba89 --- /dev/null +++ b/Delta/SceneDelegate.swift @@ -0,0 +1,175 @@ +// +// SceneDelegate.swift +// Delta +// +// Created by Riley Testut on 6/6/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import UIKit + +import DeltaCore +import Harmony + +@objc(SceneDelegate) @available(iOS 13, *) +class SceneDelegate: UIResponder, UIWindowSceneDelegate +{ + var window: UIWindow? + + private let deepLinkController = DeepLinkController() + + 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). + + self.window?.tintColor = .deltaPurple + + if let context = connectionOptions.urlContexts.first + { + self.handle(.url(context.url)) + } + + if let shortcutItem = connectionOptions.shortcutItem + { + self.handle(.shortcut(shortcutItem)) + } + } + + 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 necessarily 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. + } +} + +@available(iOS 13, *) +extension SceneDelegate +{ + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) + { + guard let context = URLContexts.first else { return } + self.handle(.url(context.url)) + } + + func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) + { + self.handle(.shortcut(shortcutItem)) + completionHandler(true) + } +} + +@available(iOS 13, *) +private extension SceneDelegate +{ + func handle(_ deepLink: DeepLink) + { + guard DatabaseManager.shared.isStarted else { + // Wait until DatabaseManager is ready before handling deep link. + + // NotificationCenter.default.notifications requires iOS 15 or later :( + // _ = await NotificationCenter.default.notifications(named: DatabaseManager.didStartNotification).first(where: { _ in true }) + + var observer: NSObjectProtocol? + observer = NotificationCenter.default.addObserver(forName: DatabaseManager.didStartNotification, object: DatabaseManager.shared, queue: .main) { [weak observer] _ in + observer.map { NotificationCenter.default.removeObserver($0) } + self.handle(deepLink) + } + + return + } + + DispatchQueue.main.async { + // DeepLinkController expects to be called from main thread. + + switch deepLink + { + case .shortcut: + _ = self.deepLinkController.handle(deepLink) + + case .url(let url): + if url.isFileURL + { + if GameType(fileExtension: url.pathExtension) != nil || url.pathExtension.lowercased() == "zip" + { + self.importGame(at: url) + } + else if url.pathExtension.lowercased() == "deltaskin" + { + self.importControllerSkin(at: url) + } + } + else if url.scheme?.hasPrefix("db-") == true + { + _ = DropboxService.shared.handleDropboxURL(url) + } + else if url.scheme?.lowercased() == "delta" + { + _ = self.deepLinkController.handle(deepLink) + } + } + } + } + + func importGame(at url: URL) + { + DatabaseManager.shared.importGames(at: [url]) { (games, errors) in + if errors.count > 0 + { + let alertController = UIAlertController.alertController(for: .games, with: errors) + self.present(alertController) + } + } + } + + func importControllerSkin(at url: URL) + { + DatabaseManager.shared.importControllerSkins(at: [url]) { (games, errors) in + if errors.count > 0 + { + let alertController = UIAlertController.alertController(for: .controllerSkins, with: errors) + self.present(alertController) + } + } + } + + func present(_ alertController: UIAlertController) + { + var rootViewController = self.window?.rootViewController + + while rootViewController?.presentedViewController != nil + { + rootViewController = rootViewController?.presentedViewController + } + + rootViewController?.present(alertController, animated: true, completion: nil) + } +} diff --git a/Delta/Supporting Files/Info.plist b/Delta/Supporting Files/Info.plist index b60ad4e..d1344a3 100644 --- a/Delta/Supporting Files/Info.plist +++ b/Delta/Supporting Files/Info.plist @@ -203,6 +203,27 @@ Delta uses your microphone to emulate the Nintendo DS microphone. NSPhotoLibraryUsageDescription Press "OK" to allow Delta to use images from your Photo Library as game artwork. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UILaunchStoryboardName + LaunchScreen + UISceneConfigurationName + Main + UISceneDelegateClassName + SceneDelegate + UISceneStoryboardFile + Main + + + + UIBackgroundModes UIFileSharingEnabled