Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -1065,6 +1066,7 @@
84AA81E22A4A36FB000B658B /* SystemActionLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemActionLink.swift; sourceTree = "<group>"; };
84AA81E42A4A3981000B658B /* DeeplinkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeeplinkManager.swift; sourceTree = "<group>"; };
84AA81E62A4A4DEF000B658B /* PumpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpView.swift; sourceTree = "<group>"; };
84AB0A823008A62400A13446 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
84D2879E2AC756C8007ED283 /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = "<group>"; };
891B508424342BE1005DA578 /* CarbAndBolusFlowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbAndBolusFlowViewModel.swift; sourceTree = "<group>"; };
892A5D29222EF60A008961AB /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; name = MockKit.framework; path = Carthage/Build/iOS/MockKit.framework; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -1745,6 +1747,7 @@
43F5C2CE1B92A2A0003EB13D /* View Controllers */,
43F5C2CF1B92A2ED003EB13D /* Views */,
897A5A9724C22DCE00C4E71D /* View Models */,
84AB0A823008A62400A13446 /* SceneDelegate.swift */,
);
path = Loop;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -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;
};
Expand All @@ -4548,6 +4554,7 @@
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 9.0;
};
name = Debug;
};
Expand All @@ -4572,6 +4579,7 @@
SDKROOT = watchos;
SKIP_INSTALL = YES;
TARGETED_DEVICE_FAMILY = 4;
WATCHOS_DEPLOYMENT_TARGET = 9.0;
};
name = Release;
};
Expand All @@ -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;
};
Expand All @@ -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;
};
Expand Down
2 changes: 0 additions & 2 deletions Loop.xcodeproj/xcshareddata/xcschemes/WatchApp.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "43A943711B926B7B0051FA24"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:Loop.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
Expand All @@ -112,7 +111,6 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "43A943711B926B7B0051FA24"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:Loop.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
Expand Down
59 changes: 16 additions & 43 deletions Loop/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
21 changes: 19 additions & 2 deletions Loop/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@
<string>NewCarbEntryIntent</string>
<string>ViewLoopStatus</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string>
Expand All @@ -93,8 +112,6 @@
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
Expand Down
2 changes: 2 additions & 0 deletions Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class LoopAppManager: NSObject {
resumeLaunch()
}

var isInitialized: Bool { state != .initialize }

var isLaunchPending: Bool { state == .checkProtectedDataAvailable }

var isLaunchComplete: Bool { state == .launchComplete }
Expand Down
101 changes: 101 additions & 0 deletions Loop/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -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<UIOpenURLContext>) {
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) }
})
}
}
2 changes: 1 addition & 1 deletion Loop/Views/FavoriteFoodDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Scripts/capture-build-details.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 "")
Expand All @@ -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}"
Expand Down