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
8 changes: 4 additions & 4 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@
847F23432E4543140035C864 /* ActivePresetBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847F23422E4543140035C864 /* ActivePresetBanner.swift */; };
849466D02EF1EAD300A90718 /* LocalizablePlural.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4C6D2412EA7E38C006F5755 /* LocalizablePlural.xcstrings */; };
8496F7312B5711C4003E672C /* ContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8496F7302B5711C4003E672C /* ContentMargin.swift */; };
84A7B5502D2D972C00B6D202 /* Image+Optional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A7B54F2D2D972C00B6D202 /* Image+Optional.swift */; };
84AA81D32A4A27A3000B658B /* LoopWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81D22A4A27A3000B658B /* LoopWidgets.swift */; };
84AA81D62A4A28AF000B658B /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81D52A4A28AF000B658B /* WidgetBackground.swift */; };
84AA81D82A4A2910000B658B /* StatusWidgetTimelimeEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AA81D72A4A2910000B658B /* StatusWidgetTimelimeEntry.swift */; };
Expand All @@ -260,6 +259,7 @@
84DF48BF2F6A0AE500BEDB40 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48BE2F6A0AE500BEDB40 /* VideoView.swift */; };
84DF48C12F6A0AED00BEDB40 /* Double+Closest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48C02F6A0AED00BEDB40 /* Double+Closest.swift */; };
84DF48C32F6A0AF600BEDB40 /* Image+Crop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48C22F6A0AF600BEDB40 /* Image+Crop.swift */; };
84E0EB0B2FD8063100D6FBB6 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E0EB0A2FD8063100D6FBB6 /* SceneDelegate.swift */; };
84E8BBCA2CCA16290078E6CF /* PresetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8BBC92CCA16290078E6CF /* PresetsView.swift */; };
84EC162E2C9115CA00D220C5 /* DIYLoopUnitTestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 84EC162D2C9115CA00D220C5 /* DIYLoopUnitTestPlan.xctestplan */; };
84F20DFD2D0B9C3A0089DF02 /* EditPresetDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20DFC2D0B9C3A0089DF02 /* EditPresetDurationView.swift */; };
Expand Down Expand Up @@ -1134,7 +1134,6 @@
8446319E2F5A2AA9003825AE /* PresetsPerformanceHistoryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetsPerformanceHistoryViewModel.swift; sourceTree = "<group>"; };
847F23422E4543140035C864 /* ActivePresetBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePresetBanner.swift; sourceTree = "<group>"; };
8496F7302B5711C4003E672C /* ContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentMargin.swift; sourceTree = "<group>"; };
84A7B54F2D2D972C00B6D202 /* Image+Optional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Optional.swift"; sourceTree = "<group>"; };
84AA81D22A4A27A3000B658B /* LoopWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopWidgets.swift; sourceTree = "<group>"; };
84AA81D52A4A28AF000B658B /* WidgetBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBackground.swift; sourceTree = "<group>"; };
84AA81D72A4A2910000B658B /* StatusWidgetTimelimeEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWidgetTimelimeEntry.swift; sourceTree = "<group>"; };
Expand All @@ -1154,6 +1153,7 @@
84DF48BE2F6A0AE500BEDB40 /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = "<group>"; };
84DF48C02F6A0AED00BEDB40 /* Double+Closest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Closest.swift"; sourceTree = "<group>"; };
84DF48C22F6A0AF600BEDB40 /* Image+Crop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image+Crop.swift"; sourceTree = "<group>"; };
84E0EB0A2FD8063100D6FBB6 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
84E8BBC92CCA16290078E6CF /* PresetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetsView.swift; sourceTree = "<group>"; };
84EC162D2C9115CA00D220C5 /* DIYLoopUnitTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DIYLoopUnitTestPlan.xctestplan; sourceTree = "<group>"; };
84F20DFC2D0B9C3A0089DF02 /* EditPresetDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPresetDurationView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1963,6 +1963,7 @@
43F5C2CE1B92A2A0003EB13D /* View Controllers */,
43F5C2CF1B92A2ED003EB13D /* Views */,
897A5A9724C22DCE00C4E71D /* View Models */,
84E0EB0A2FD8063100D6FBB6 /* SceneDelegate.swift */,
);
path = Loop;
sourceTree = "<group>";
Expand Down Expand Up @@ -2161,7 +2162,6 @@
C13DA2AF24F6C7690098BB29 /* UIViewController.swift */,
430B29922041F5B200BA9F93 /* UserDefaults+Loop.swift */,
A9B607AF247F000F00792BE4 /* UserNotifications+Loop.swift */,
84A7B54F2D2D972C00B6D202 /* Image+Optional.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -3700,6 +3700,7 @@
4F11D3C220DD80B3006E072C /* WatchHistoricalGlucose.swift in Sources */,
14ED83F62C6421F9008B4A5C /* FavoriteFoodInsightsCardView.swift in Sources */,
C174233C259BEB0F00399C9D /* ManualEntryDoseViewModel.swift in Sources */,
84E0EB0B2FD8063100D6FBB6 /* SceneDelegate.swift in Sources */,
1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */,
84FA9D332CF7FD0D004162B4 /* PresetsHistoryView.swift in Sources */,
841306BB2F7F0D9C00AF0320 /* ReferencesView.swift in Sources */,
Expand Down Expand Up @@ -3728,7 +3729,6 @@
439706E622D2E84900C81566 /* PredictionSettingTableViewCell.swift in Sources */,
430D85891F44037000AF2D4F /* HUDViewTableViewCell.swift in Sources */,
43A51E211EB6DBDD000736CC /* LoopChartsTableViewController.swift in Sources */,
84A7B5502D2D972C00B6D202 /* Image+Optional.swift in Sources */,
8968B1122408B3520074BB48 /* UIFont.swift in Sources */,
1452F4A92A851C9400F8B9E4 /* FavoriteFoodAddEditViewModel.swift in Sources */,
438D42FB1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift in Sources */,
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 @@ -84,7 +84,6 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "43A943711B926B7B0051FA24"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:Loop.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
Expand All @@ -102,7 +101,6 @@
BuildableIdentifier = "primary"
BlueprintIdentifier = "43A943711B926B7B0051FA24"
BuildableName = "WatchApp.app"
BlueprintName = "WatchApp"
ReferencedContainer = "container:Loop.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
Expand Down
65 changes: 15 additions & 50 deletions Loop/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,17 @@
import UIKit
import LoopKit

final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider {
var window: UIWindow?
final class AppDelegate: UIResponder, UIApplicationDelegate {

/// The shared app manager. Owned by the app delegate so that application-level
/// callbacks (remote notifications, protected data) can reach it, while the
/// window-tied lifecycle is driven by `SceneDelegate`.
let loopAppManager = LoopAppManager()

/// Launch options captured at process launch, forwarded to `loopAppManager`
/// when the scene connects (the window does not yet exist at launch time).
private(set) var launchOptions: [UIApplication.LaunchOptionsKey: Any]?

private let loopAppManager = LoopAppManager()
private let log = DiagnosticLog(category: "AppDelegate")

// MARK: - UIApplicationDelegate - Initialization
Expand All @@ -22,40 +29,12 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, WindowProvider {

setenv("CFNETWORK_DIAGNOSTICS", "3", 1)

// Avoid doing full initialization when running tests
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
loopAppManager.initialize(windowProvider: self, launchOptions: launchOptions)
loopAppManager.launch()
return loopAppManager.isLaunchComplete
} else {
return true
}
}

// MARK: - UIApplicationDelegate - Life Cycle

func applicationDidBecomeActive(_ application: UIApplication) {
log.default(#function)
// The window is created and owned by the scene, so initialization of the
// app manager is deferred to `SceneDelegate.scene(_:willConnectTo:options:)`.
// Stash the launch options so the scene can forward them.
self.launchOptions = launchOptions

loopAppManager.didBecomeActive()
}

func applicationWillResignActive(_ application: UIApplication) {
log.default(#function)
}

func applicationDidEnterBackground(_ application: UIApplication) {
log.default(#function)
}

func applicationWillEnterForeground(_ application: UIApplication) {
log.default(#function)

loopAppManager.askUserToConfirmLoopReset()
}

func applicationWillTerminate(_ application: UIApplication) {
log.default(#function)
return true
}

// MARK: - UIApplicationDelegate - Environment
Expand Down Expand Up @@ -86,20 +65,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
20 changes: 0 additions & 20 deletions Loop/Extensions/Image+Optional.swift

This file was deleted.

23 changes: 19 additions & 4 deletions Loop/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,33 @@
<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>
<string>processing</string>
<string>remote-notification</string>
</array>
<key>UIDesignRequiresCompatibility</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
Expand Down
4 changes: 4 additions & 0 deletions Loop/Managers/LoopAppManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ class LoopAppManager: NSObject {

var isLaunchComplete: Bool { state == .launchComplete }

/// True until `initialize(windowProvider:launchOptions:)` has been called. Used by
/// `SceneDelegate` to ensure the app manager is initialized only once per process.
var isInInitialState: Bool { state == .initialize }

private func resumeLaunch() async {
if state == .checkProtectedDataAvailable {
checkProtectedDataAvailable()
Expand Down
106 changes: 106 additions & 0 deletions Loop/SceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// SceneDelegate.swift
// Loop
//
// Copyright © 2026 LoopKit Authors. All rights reserved.
//

import UIKit
import LoopKit

/// Drives the window-tied lifecycle for the app under the UIScene life cycle.
///
/// The window is created automatically by UIKit from `Main.storyboard` (declared
/// via `UISceneStoryboardFile` in the scene manifest) and assigned to `window`
/// before `scene(_:willConnectTo:options:)` is called. The shared `LoopAppManager`
/// continues to be owned by `AppDelegate` so that application-level callbacks
/// (remote notifications, protected data) can reach it.
final class SceneDelegate: UIResponder, UIWindowSceneDelegate, WindowProvider {

var window: UIWindow?

private let log = DiagnosticLog(category: "SceneDelegate")

private var loopAppManager: LoopAppManager? {
(UIApplication.shared.delegate as? AppDelegate)?.loopAppManager
}

// MARK: - UIWindowSceneDelegate - Connection

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
log.default(#function)

// Avoid doing full initialization when running tests.
guard ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil else {
return
}

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let loopAppManager = appDelegate.loopAppManager

// The app manager only initializes once per process. Multiple scenes are not
// supported, so guard against a scene reconnecting after initialization.
guard loopAppManager.isInInitialState else {
return
}

loopAppManager.initialize(windowProvider: self, launchOptions: appDelegate.launchOptions)
loopAppManager.launch()

// Handle any URLs or user activities delivered at connection time.
if let url = connectionOptions.urlContexts.first?.url {
_ = loopAppManager.handle(url)
}
for userActivity in connectionOptions.userActivities {
_ = loopAppManager.userActivity(userActivity, restorationHandler: { _ in })
}
}

// MARK: - UIWindowSceneDelegate - Life Cycle

func sceneDidBecomeActive(_ scene: UIScene) {
log.default(#function)

loopAppManager?.didBecomeActive()
}

func sceneWillResignActive(_ scene: UIScene) {
log.default(#function)
}

func sceneWillEnterForeground(_ scene: UIScene) {
log.default(#function)

// Unlike the legacy `applicationWillEnterForeground(_:)`, this is also called as
// part of cold launch, before the managers are initialized. `resumeLaunch()`
// performs this check itself once launch completes, so here it only applies to
// subsequent foreground transitions.
guard let loopAppManager, loopAppManager.isLaunchComplete else {
return
}
loopAppManager.askUserToConfirmLoopReset()
}

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)

_ = loopAppManager?.userActivity(userActivity, restorationHandler: { _ in })
}
}
7 changes: 4 additions & 3 deletions Loop/View Controllers/StatusTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ final class StatusTableViewController: LoopChartsTableViewController {
super.viewWillAppear(animated)

navigationController?.setNavigationBarHidden(true, animated: animated)
navigationController?.setToolbarHidden(false, animated: animated)
navigationController?.setToolbarHidden(true, animated: animated)

alertPermissionsChecker.checkNow()

Expand Down Expand Up @@ -643,7 +643,8 @@ final class StatusTableViewController: LoopChartsTableViewController {
let statusRowMode = self.determineStatusRowMode()

updateBannerAndHUDandStatusRows(statusRowMode: statusRowMode, newSize: currentContext.newSize, animated: animated)

tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: UIDevice.current.orientation.isLandscape ? 0 : 52, right: 0)

redrawCharts()

reloading = false
Expand Down Expand Up @@ -1208,7 +1209,7 @@ final class StatusTableViewController: LoopChartsTableViewController {
// Compute the height of the HUD, defaulting to 70
let hudHeight = ceil(hudView?.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize).height ?? 74)
var availableSize = max(tableView.bounds.width, tableView.bounds.height)
availableSize -= (tableView.safeAreaInsets.top + tableView.safeAreaInsets.bottom + hudHeight)
availableSize -= (tableView.safeAreaInsets.top + tableView.safeAreaInsets.bottom + tableView.contentInset.bottom + hudHeight)

switch ChartRow(rawValue: indexPath.row)! {
case .glucose:
Expand Down
4 changes: 2 additions & 2 deletions Loop/Views/IOSFocusModesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct IOSFocusModesView: View {
// MARK: To be removed before next DIY Sync
if appName.contains("Tidepool") {
VStack(alignment: .leading, spacing: 8) {
Image("focus-mode-1")
Image.optional("focus-mode-1")

Text(
String(
Expand All @@ -64,7 +64,7 @@ struct IOSFocusModesView: View {
.fixedSize(horizontal: false, vertical: true)

VStack(alignment: .leading, spacing: 8) {
Image("focus-mode-2")
Image.optional("focus-mode-2")

Text(
NSLocalizedString(
Expand Down
Loading