diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 005d0e00be..cd2b032955 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -258,6 +258,7 @@ 84AA81E32A4A36FB000B658B /* SystemActionLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */; }; 84AA81E52A4A3981000B658B /* DeeplinkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */; }; 84AA81E72A4A4DEF000B658B /* PumpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81E62A4A4DEF000B658B /* PumpView.swift */; }; + 84AB0A833008A62400A13446 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AB0A823008A62400A13446 /* SceneDelegate.swift */; }; 84D2879F2AC756C8007ED283 /* ContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2879E2AC756C8007ED283 /* ContentMargin.swift */; }; 891B508524342BE1005DA578 /* CarbAndBolusFlowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */; }; 892A5D59222F0A27008961AB /* Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D58222F0A27008961AB /* Debug.swift */; }; @@ -1065,6 +1066,7 @@ 84AA81E22A4A36FB000B658B /* SystemActionLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemActionLink.swift; sourceTree = ""; }; 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = ""; }; 84AA81E62A4A4DEF000B658B /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = ""; }; + 84AB0A823008A62400A13446 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 84D2879E2AC756C8007ED283 /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = ""; }; 891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = ""; }; 892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; }; @@ -1745,6 +1747,7 @@ 43F5C2CE1B92A2A0003EB13D /* View Controllers */, 43F5C2CF1B92A2ED003EB13D /* Views */, 897A5A9724C22DCE00C4E71D /* View Models */, + 84AB0A823008A62400A13446 /* SceneDelegate.swift */, ); path = Loop; sourceTree = ""; @@ -3523,6 +3526,7 @@ A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */, 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */, A9B607B0247F000F00792BE4 /* UserNotifications+Loop.swift in Sources */, + 84AB0A833008A62400A13446 /* SceneDelegate.swift in Sources */, 43F89CA322BDFBBD006BB54E /* UIActivityIndicatorView.swift in Sources */, A999D40624663D18004C89D4 /* PumpManagerError.swift in Sources */, 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, @@ -4497,6 +4501,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "WatchApp Extension/Extensions/WatchApp Extension-Bridging-Header.h"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4524,6 +4529,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "WatchApp Extension/Extensions/WatchApp Extension-Bridging-Header.h"; SWIFT_PRECOMPILE_BRIDGING_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -4548,6 +4554,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4572,6 +4579,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -4598,6 +4606,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -4624,6 +4633,7 @@ SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_INSTALL_OBJC_HEADER = NO; TARGETED_DEVICE_FAMILY = 4; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; diff --git a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme index 6ab6be0246..e2a1660877 100644 --- a/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme +++ b/Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme @@ -94,7 +94,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> @@ -112,7 +111,6 @@ BuildableIdentifier = "primary" BlueprintIdentifier = "43A943711B926B7B0051FA24" BuildableName = "WatchApp.app" - BlueprintName = "WatchApp" ReferencedContainer = "container:Loop.xcodeproj"> diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index ebb05d5c12..f54306c3eb 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -9,10 +9,16 @@ import UIKit import LoopKit -final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { - var window: UIWindow? +final class AppDelegate: UIResponder, UIApplicationDelegate { + + /// The shared app manager. Per-scene lifecycle is driven by `SceneDelegate`, which + /// accesses this instance through the app delegate. + let loopAppManager = LoopAppManager() + + /// The launch options passed at `didFinishLaunchingWithOptions`, retained so the + /// scene delegate can forward them to `LoopAppManager` when it connects. + private(set) var launchOptions: [UIApplication.LaunchOptionsKey: Any]? - private let loopAppManager = LoopAppManager() private let log = DiagnosticLog(category: "AppDelegate") // MARK: - UIApplicationDelegate - Initialization @@ -24,35 +30,16 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { log.default("lastPathComponent = %{public}@", String(describing: Bundle.main.appStoreReceiptURL?.lastPathComponent)) - loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) - loopAppManager.launch() - return loopAppManager.isLaunchComplete - } - - // MARK: - UIApplicationDelegate - Life Cycle - - func applicationDidBecomeActive(_ application: UIApplication) { - log.default(#function) - - loopAppManager.didBecomeActive() - } - - func applicationWillResignActive(_ application: UIApplication) { - log.default(#function) - } - - func applicationDidEnterBackground(_ application: UIApplication) { - log.default(#function) + // The window is owned by the scene, so app manager initialization and launch are + // deferred to `SceneDelegate.scene(_:willConnectTo:options:)`. + self.launchOptions = launchOptions + return true } - func applicationWillEnterForeground(_ application: UIApplication) { - log.default(#function) - - loopAppManager.askUserToConfirmLoopReset() - } + // MARK: - UIApplicationDelegate - Scene Configuration - func applicationWillTerminate(_ application: UIApplication) { - log.default(#function) + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) } // MARK: - UIApplicationDelegate - Environment @@ -83,20 +70,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider { completionHandler(loopAppManager.handleRemoteNotification(userInfo as? [String: AnyObject]) ? .noData : .failed) } - - // MARK: - UIApplicationDelegate - Deeplinking - - func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { - loopAppManager.handle(url) - } - - // MARK: - UIApplicationDelegate - Continuity - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - log.default(#function) - - return loopAppManager.userActivity(userActivity, restorationHandler: restorationHandler) - } // MARK: - UIApplicationDelegate - Interface diff --git a/Loop/Info.plist b/Loop/Info.plist index db76e6e846..693d00d337 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -82,6 +82,25 @@ NewCarbEntryIntent ViewLoopStatus + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + UIBackgroundModes bluetooth-central @@ -93,8 +112,6 @@ UILaunchStoryboardName LaunchScreen - UIMainStoryboardFile - Main UIRequiredDeviceCapabilities armv7 diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 3f00104c5a..ade240c9f7 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -128,6 +128,8 @@ class LoopAppManager: NSObject { resumeLaunch() } + var isInitialized: Bool { state != .initialize } + var isLaunchPending: Bool { state == .checkProtectedDataAvailable } var isLaunchComplete: Bool { state == .launchComplete } diff --git a/Loop/SceneDelegate.swift b/Loop/SceneDelegate.swift new file mode 100644 index 0000000000..15e73ad9f5 --- /dev/null +++ b/Loop/SceneDelegate.swift @@ -0,0 +1,101 @@ +// +// SceneDelegate.swift +// Loop +// +// Copyright © 2026 LoopKit Authors. All rights reserved. +// + +import UIKit +import LoopKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate, WindowProvider { + var window: UIWindow? + + private let log = DiagnosticLog(category: "SceneDelegate") + + /// The app manager lives on the app delegate because it also serves app-level callbacks + /// (remote notifications, protected data availability). + private var loopAppManager: LoopAppManager? { + (UIApplication.shared.delegate as? AppDelegate)?.loopAppManager + } + + // MARK: - UIWindowSceneDelegate - Life Cycle + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + log.default(#function) + + guard let windowScene = scene as? UIWindowScene else { return } + + // When the scene is configured with a storyboard, UIKit creates the window and its + // root view controller and assigns them here. Create one defensively otherwise. + if window == nil { + let window = UIWindow(windowScene: windowScene) + self.window = window + window.makeKeyAndVisible() + } + + guard let loopAppManager else { return } + + let launchOptions = (UIApplication.shared.delegate as? AppDelegate)?.launchOptions + if !loopAppManager.isInitialized { + loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions) + } + if loopAppManager.isLaunchPending { + loopAppManager.launch() + } + + if let url = connectionOptions.urlContexts.first?.url { + _ = loopAppManager.handle(url) + } + + for userActivity in connectionOptions.userActivities { + restore(userActivity, with: loopAppManager) + } + } + + func sceneDidBecomeActive(_ scene: UIScene) { + log.default(#function) + + loopAppManager?.didBecomeActive() + } + + func sceneWillEnterForeground(_ scene: UIScene) { + log.default(#function) + + loopAppManager?.askUserToConfirmLoopReset() + } + + func sceneWillResignActive(_ scene: UIScene) { + log.default(#function) + } + + func sceneDidEnterBackground(_ scene: UIScene) { + log.default(#function) + } + + // MARK: - UIWindowSceneDelegate - Deeplinking + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + _ = loopAppManager?.handle(url) + } + + // MARK: - UIWindowSceneDelegate - Continuity + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + log.default(#function) + + guard let loopAppManager else { return } + restore(userActivity, with: loopAppManager) + } + + // MARK: - Private + + private func restore(_ userActivity: NSUserActivity, with loopAppManager: LoopAppManager) { + // Mirror UIKit's legacy behavior of invoking `restoreUserActivityState` on each + // object the app manager hands back through the restoration handler. + _ = loopAppManager.userActivity(userActivity, restorationHandler: { objects in + objects?.forEach { $0.restoreUserActivityState(userActivity) } + }) + } +} diff --git a/Loop/Views/FavoriteFoodDetailView.swift b/Loop/Views/FavoriteFoodDetailView.swift index 8c76e1fe8f..46c4105593 100644 --- a/Loop/Views/FavoriteFoodDetailView.swift +++ b/Loop/Views/FavoriteFoodDetailView.swift @@ -14,7 +14,7 @@ public struct FavoriteFoodDetailView: View { let food: StoredFavoriteFood? let onFoodDelete: (StoredFavoriteFood) -> Void - @State private var isConfirmingDelete = false + @State private var isConfirmingDelete: Bool let carbFormatter: QuantityFormatter let absorptionTimeFormatter: DateComponentsFormatter diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 2393e5afa4..b78eb1c592 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -126,6 +126,8 @@ fi # Gather submodule details. # We use git submodule foreach to output lines in the form: # submodule_name|branch_or_tag|commit_sha +# Tolerate failures here (e.g. a stale gitlink in the index that is no longer +# mapped in .gitmodules) so that gathering build metadata never fails the build. submodules_info=$(git submodule foreach --quiet ' sub_git_branch=$(git symbolic-ref --short -q HEAD || echo "") sub_git_tag=$(git describe --tags --exact-match 2>/dev/null || echo "") @@ -135,10 +137,12 @@ submodules_info=$(git submodule foreach --quiet ' sub_git_branch_or_tag="detached" fi echo "$name|$sub_git_branch_or_tag|$sub_git_commit_sha" -') +' 2>/dev/null) || warn "Unable to fully enumerate submodules for build details" # For each line, add a dictionary entry for that submodule. echo "${submodules_info}" | while IFS="|" read -r submodule_name sub_branch sub_sha; do + # Skip empty lines (e.g. when submodule enumeration produced no output) + [ -z "${submodule_name}" ] && continue # Create a dictionary for this submodule /usr/libexec/PlistBuddy -c "Add :${submodules_key}:${submodule_name} dict" "${info_plist_path}" /usr/libexec/PlistBuddy -c "Add :${submodules_key}:${submodule_name}:branch string ${sub_branch}" "${info_plist_path}"