From bcae313dab108ea1dee84d2d95c596ba39c31d04 Mon Sep 17 00:00:00 2001 From: Raul Riera Date: Thu, 26 Feb 2026 10:39:38 -0500 Subject: [PATCH] refactor: adopt UIScene lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move all UI setup, lifecycle callbacks, and deep link handling from AppDelegate into a new SceneDelegate. AppDelegate retains only non-UI init (analytics, error reporting, fonts, appearance) and push notification token registration. Adds UIApplicationSceneManifest to Info.plist with multiple scenes disabled. Required for iOS 26 SDK compatibility — apps without scene support log a warning now and will crash on launch with the iOS 27 SDK. --- Code.xcodeproj/project.pbxproj | 4 + Flipcash/Core/AppDelegate.swift | 159 ++--------------------- Flipcash/Core/SceneDelegate.swift | 182 +++++++++++++++++++++++++++ Flipcash/Supporting Files/Info.plist | 17 +++ 4 files changed, 215 insertions(+), 147 deletions(-) create mode 100644 Flipcash/Core/SceneDelegate.swift diff --git a/Code.xcodeproj/project.pbxproj b/Code.xcodeproj/project.pbxproj index 8f453e9d..39f4d5cf 100644 --- a/Code.xcodeproj/project.pbxproj +++ b/Code.xcodeproj/project.pbxproj @@ -267,6 +267,7 @@ 9AEABBF62B76D27F00667DA4 /* String+Padding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1EBC5126246EFD003F8B19 /* String+Padding.swift */; }; 9AEB5F4F25CDD294004B3680 /* CodeServices in Frameworks */ = {isa = PBXBuildFile; productRef = 9AEB5F4E25CDD294004B3680 /* CodeServices */; }; 9AED1104258BE1310088D902 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AED1103258BE1310088D902 /* AppDelegate.swift */; }; + 396C42CC7AD65B152D6DEFA2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDC9675F891E851B42A09A5 /* SceneDelegate.swift */; }; 9AED1108258BE1320088D902 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9AED1107258BE1320088D902 /* Assets.xcassets */; }; 9AED110B258BE1320088D902 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9AED110A258BE1320088D902 /* Preview Assets.xcassets */; }; 9AED1116258BE1320088D902 /* EncodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AED1115258BE1320088D902 /* EncodingTests.swift */; }; @@ -569,6 +570,7 @@ 9AEABBF22B76CF0A00667DA4 /* NotificationModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationModifier.swift; sourceTree = ""; }; 9AED1100258BE1310088D902 /* Code.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Code.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9AED1103258BE1310088D902 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + FBDC9675F891E851B42A09A5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 9AED1107258BE1320088D902 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9AED110A258BE1320088D902 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 9AED110C258BE1320088D902 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1173,6 +1175,7 @@ 9AA3907A274416E300234899 /* Code.entitlements */, 9ACC074E28342BF1001F0081 /* Guards.swift */, 9AED1103258BE1310088D902 /* AppDelegate.swift */, + FBDC9675F891E851B42A09A5 /* SceneDelegate.swift */, 9A829A3F28A57852005F4F87 /* AppContainer.swift */, 9AACF3B32C1C8D1A00F48721 /* Actors */, 9A4283BF2A02BA0400792BFD /* Share */, @@ -2197,6 +2200,7 @@ 9A2B16C52B73FF86008DB36D /* DepositUSDCScreen.swift in Sources */, 9ACFA004261DFDA6007F97B4 /* Environment.swift in Sources */, 9AED1104258BE1310088D902 /* AppDelegate.swift in Sources */, + 396C42CC7AD65B152D6DEFA2 /* SceneDelegate.swift in Sources */, 9A8B26612C1A32BC009FB349 /* Preferences.swift in Sources */, 9A103F8225B9BF180026EA62 /* Session.swift in Sources */, 9A1CE1D12C90C61E00B596CC /* MessageList.swift in Sources */, diff --git a/Flipcash/Core/AppDelegate.swift b/Flipcash/Core/AppDelegate.swift index f7e06897..f49ec4a2 100644 --- a/Flipcash/Core/AppDelegate.swift +++ b/Flipcash/Core/AppDelegate.swift @@ -6,179 +6,44 @@ // import UIKit -import SwiftUI import FlipcashUI import FlipcashCore @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - - var window: UIWindow? - + let container = Container() - - private var resetInterval: TimeInterval = 60.0 - private var lastActiveDate: Date? - - private var hasBeenBackgrounded: Bool = false - - private var sessionContainer: SessionContainer? { - if case .loggedIn(let container) = container.sessionAuthenticator.state { - return container - } - return nil - } - + // MARK: - Launch - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - window = UIWindow(frame: UIScreen.main.bounds) - + Analytics.initialize() ErrorReporting.initialize() FontBook.registerApplicationFonts() setupAppearance() - - assignHost() - NotificationCenter.default.addObserver( - self, - selector: #selector(handlePushDeepLinkNotification(_:)), - name: .pushDeepLinkReceived, - object: nil - ) - return true } - - private func assignHost() { - guard let window = window else { - return - } - let screen = ContainerScreen(container: container) - .injectingEnvironment(from: container) - .colorScheme(.dark) - .tint(Color.textMain) - - let controller = UIHostingController(rootView: screen) - controller.view.backgroundColor = UIColor(.backgroundMain) - window.rootViewController = controller - window.overrideUserInterfaceStyle = .dark - - window.makeKeyAndVisible() - } - - // MARK: - Lifecycle - - - func applicationWillResignActive(_ application: UIApplication) { - trace(.warning) - lastActiveDate = .now - -// appContainer.pushController.appWillResignActive() - -// beginBackgroundTask() - } - - func applicationDidEnterBackground(_ application: UIApplication) { - hasBeenBackgrounded = true - - if let sessionContainer { - sessionContainer.session.didEnterBackground() - } - - container.preferences.appDidEnterBackground() - } - - func applicationWillEnterForeground(_ application: UIApplication) { - trace(.warning) - -// appContainer.sessionAuthenticator.updateBiometricsState() - - if let _ = sessionContainer { // Logged in - if !UIApplication.isInterfaceResetDisabled { - if let interval = secondsSinceLastActive(), interval > resetInterval { - trace(.warning, components: "Resetting interface...") - assignHost() - // fadeOutOverlay(delay: 0.4) - } else { - // fadeOutOverlay(delay: 0.3) - } - } else { - trace(.warning, components: "Interface reset disabled.") - } - - } else { // Logged out -// destroyOverlay() - } - } - - private func secondsSinceLastActive() -> TimeInterval? { - guard let lastActiveDate = lastActiveDate else { - return nil - } + // MARK: - Scene Configuration - - return Date.now.timeIntervalSince1970 - lastActiveDate.timeIntervalSince1970 - } - - // MARK: - Deep Links - - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - - return handleOpenURL(url: url) - } - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - guard - userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let url = userActivity.webpageURL - else { - return false - } - - return handleOpenURL(url: url) - } - - private func handleOpenURL(url: URL) -> Bool { - let action = container.deepLinkController.handle(open: url) - - // Calling assignHost() during app launch (when the app - // hasn't been running) results in a double call making - // it hang for ~10 seconds. Still uncertain of the exact - // cause of the problem - if hasBeenBackgrounded && action?.preventUserInterfaceReset == false { - - // Reset the view in the event that the app handles - // any deep links to ensure a consistent experience - assignHost() - } - - Task { - try await action?.executeAction() - } - - return true + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config } - @objc private func handlePushDeepLinkNotification(_ notification: Notification) { - guard let url = notification.userInfo?["url"] as? URL else { - return - } - - _ = handleOpenURL(url: url) - } - // MARK: - Push Notifications - - + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { trace(.success, components: "Did register for remote notifications with token: \(deviceToken.hexString())") - - if let sessionContainer { + + if case .loggedIn(let sessionContainer) = container.sessionAuthenticator.state { sessionContainer.pushController.didReceiveRemoteNotificationToken(with: deviceToken) } } - + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { trace(.failure, components: "Push notification registration failed: \(error)") } diff --git a/Flipcash/Core/SceneDelegate.swift b/Flipcash/Core/SceneDelegate.swift new file mode 100644 index 00000000..403f1873 --- /dev/null +++ b/Flipcash/Core/SceneDelegate.swift @@ -0,0 +1,182 @@ +// +// SceneDelegate.swift +// Flipcash +// +// Created by Raul Riera on 2026-02-26. +// + +import UIKit +import SwiftUI +import FlipcashUI +import FlipcashCore + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + private var resetInterval: TimeInterval = 60.0 + private var lastActiveDate: Date? + + private var hasBeenBackgrounded: Bool = false + + private var appDelegate: AppDelegate { + UIApplication.shared.delegate as! AppDelegate + } + + private var container: Container { + appDelegate.container + } + + private var sessionContainer: SessionContainer? { + if case .loggedIn(let container) = container.sessionAuthenticator.state { + return container + } + return nil + } + + // MARK: - Scene Lifecycle - + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + let window = UIWindow(windowScene: windowScene) + self.window = window + + assignHost() + + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePushDeepLinkNotification(_:)), + name: .pushDeepLinkReceived, + object: nil + ) + + // Handle cold-launch URL deep links + if let urlContext = connectionOptions.urlContexts.first { + _ = handleOpenURL(url: urlContext.url) + } + + // Handle cold-launch universal links + if let userActivity = connectionOptions.userActivities.first(where: { $0.activityType == NSUserActivityTypeBrowsingWeb }), + let url = userActivity.webpageURL { + _ = handleOpenURL(url: url) + } + } + + // MARK: - Lifecycle - + + func sceneWillResignActive(_ scene: UIScene) { + trace(.warning) + lastActiveDate = .now + } + + func sceneDidEnterBackground(_ scene: UIScene) { + hasBeenBackgrounded = true + + if let sessionContainer { + sessionContainer.session.didEnterBackground() + } + + container.preferences.appDidEnterBackground() + } + + func sceneWillEnterForeground(_ scene: UIScene) { + trace(.warning) + + if let _ = sessionContainer { // Logged in + if !UIApplication.isInterfaceResetDisabled { + if let interval = secondsSinceLastActive(), interval > resetInterval { + trace(.warning, components: "Resetting interface...") + assignHost() + } else { + // No reset needed + } + } else { + trace(.warning, components: "Interface reset disabled.") + } + + } else { // Logged out + // No action needed + } + } + + // MARK: - Deep Links - + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { + return + } + + _ = handleOpenURL(url: url) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard + userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL + else { + return + } + + _ = handleOpenURL(url: url) + } + + // MARK: - Private - + + private func assignHost() { + guard let window else { + return + } + + let screen = ContainerScreen(container: container) + .injectingEnvironment(from: container) + .colorScheme(.dark) + .tint(Color.textMain) + + let controller = UIHostingController(rootView: screen) + controller.view.backgroundColor = UIColor(.backgroundMain) + window.rootViewController = controller + window.overrideUserInterfaceStyle = .dark + + window.makeKeyAndVisible() + } + + private func handleOpenURL(url: URL) -> Bool { + let action = container.deepLinkController.handle(open: url) + + // Calling assignHost() during app launch (when the app + // hasn't been running) results in a double call making + // it hang for ~10 seconds. Still uncertain of the exact + // cause of the problem + if hasBeenBackgrounded && action?.preventUserInterfaceReset == false { + + // Reset the view in the event that the app handles + // any deep links to ensure a consistent experience + assignHost() + } + + Task { + try await action?.executeAction() + } + + return true + } + + @objc private func handlePushDeepLinkNotification(_ notification: Notification) { + guard let url = notification.userInfo?["url"] as? URL else { + return + } + + _ = handleOpenURL(url: url) + } + + private func secondsSinceLastActive() -> TimeInterval? { + guard let lastActiveDate else { + return nil + } + + return Date.now.timeIntervalSince1970 - lastActiveDate.timeIntervalSince1970 + } +} diff --git a/Flipcash/Supporting Files/Info.plist b/Flipcash/Supporting Files/Info.plist index 0d2ed69b..09cf03ab 100644 --- a/Flipcash/Supporting Files/Info.plist +++ b/Flipcash/Supporting Files/Info.plist @@ -26,6 +26,23 @@ UIColorName background + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + UIBackgroundModes fetch