From 210d97c1043f0662d470b19872614d5ac61427bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 11 Nov 2026 20:03:22 +0100 Subject: [PATCH] Import every therapy profile from Nightscout and show the ones left out --- LoopOnboardingKitUI/Localizable.xcstrings | 21 ++++ .../OnboardingUICoordinator.swift | 32 +++++-- .../Views/ImportSettingsView.swift | 95 ++++++++++++++++--- 3 files changed, 129 insertions(+), 19 deletions(-) diff --git a/LoopOnboardingKitUI/Localizable.xcstrings b/LoopOnboardingKitUI/Localizable.xcstrings index d35d6cd..1fdd9ed 100644 --- a/LoopOnboardingKitUI/Localizable.xcstrings +++ b/LoopOnboardingKitUI/Localizable.xcstrings @@ -3,6 +3,15 @@ "strings" : { "" : { + }, + "%1$@ (active)" : { + "comment" : "Imported profile name of the active profile (1: profile name)" + }, + "%1$@ (paused)" : { + "comment" : "Imported scheduled profile switch that is paused (1: scheduled switch description)" + }, + "%1$@: %2$@" : { + "comment" : "A therapy profile or scheduled switch that will not be imported (1: profile name or scheduled switch description, 2: reason)" }, "Apple Health" : { "comment" : "Title on AppleHealthAuthView", @@ -674,6 +683,9 @@ } } }, + "Profiles: %1$@" : { + "comment" : "List of imported therapy profiles (1: profile names)" + }, "Save Settings" : { "comment" : "Therapy settings save button title", "localizations" : { @@ -751,6 +763,9 @@ } } }, + "Scheduled switches: %1$@\n\nEach switch first runs at its next time after the import. You can change or pause them under Settings → Profiles." : { + "comment" : "List of imported scheduled profile switches (1: scheduled switch descriptions)" + }, "Settings Found" : { "comment" : "Title on ImportSettingsView", "localizations" : { @@ -982,6 +997,12 @@ } } }, + "These profiles will not be imported:" : { + "comment" : "Heading of the therapy profiles that will not be imported" + }, + "These scheduled switches will not be imported:" : { + "comment" : "Heading of the scheduled profile switches that will not be imported" + }, "Use Nightscout with Loop" : { "comment" : "Button title for choosing onboarding with nightscout", "localizations" : { diff --git a/LoopOnboardingKitUI/View Controllers/OnboardingUICoordinator.swift b/LoopOnboardingKitUI/View Controllers/OnboardingUICoordinator.swift index 0db16e8..aad97c9 100644 --- a/LoopOnboardingKitUI/View Controllers/OnboardingUICoordinator.swift +++ b/LoopOnboardingKitUI/View Controllers/OnboardingUICoordinator.swift @@ -60,8 +60,7 @@ class OnboardingUICoordinator: UINavigationController, CGMManagerOnboarding, Pum private var nightscoutOnboardingViewController: UIViewController? - private var importedTherapySettings: TherapySettings? - private var importedTherapySettingsDate: Date? + private var importedTherapySettings: ImportedTherapySettings? private var shouldUseImportedSettings: Bool = false private let displayGlucosePreference: DisplayGlucosePreference @@ -156,7 +155,8 @@ class OnboardingUICoordinator: UINavigationController, CGMManagerOnboarding, Pum let view = OnboardingChooserView(setupWithNightscout: setupWithNightscout, setupWithoutNightscout: setupWithoutNightscout) return hostingController(rootView: view) case .importSettings: - let view = ImportSettingsView(settingsDate: importedTherapySettingsDate!) { [weak self] (shouldImport) in + let view = ImportSettingsView(importedTherapySettings: importedTherapySettings!) { [weak self] (shouldImport) in + self?.log.default("Settings from Nightscout %{public}@", shouldImport ? "imported" : "not imported") self?.shouldUseImportedSettings = shouldImport self?.stepFinished() } @@ -164,7 +164,7 @@ class OnboardingUICoordinator: UINavigationController, CGMManagerOnboarding, Pum case .suspendThresholdInfo: let therapySettings: TherapySettings if let importedTherapySettings = importedTherapySettings, shouldUseImportedSettings { - therapySettings = importedTherapySettings + therapySettings = importedTherapySettings.therapySettings } else { therapySettings = initialTherapySettings } @@ -325,11 +325,12 @@ class OnboardingUICoordinator: UINavigationController, CGMManagerOnboarding, Pum nightscoutService.fetchStoredTherapySettings { result in DispatchQueue.main.async { switch result { - case .success((let settings, let date)): - self.importedTherapySettings = settings - self.importedTherapySettingsDate = date + case .success(let importedTherapySettings): + self.logImportOffer(importedTherapySettings) + self.importedTherapySettings = importedTherapySettings self.navigate(to: .importSettings) - case .failure: + case .failure(let error): + self.log.error("No settings to import from Nightscout: %{public}@", String(describing: error)) // TODO: Show error? Maybe user was expecting import option and wants to know why it didn't show. self.stepFinished() break @@ -342,6 +343,21 @@ class OnboardingUICoordinator: UINavigationController, CGMManagerOnboarding, Pum } + private func logImportOffer(_ imported: ImportedTherapySettings) { + let therapySettings = imported.therapySettings + log.default("Settings from Nightscout of %{public}@ offered: %d profiles, active %{public}@, %d scheduled switches", + String(describing: imported.date), + therapySettings.therapyProfiles?.count ?? 0, + TherapyProfiles.activeName(therapySettings.activeProfileName), + therapySettings.scheduledSwitches?.count ?? 0) + for skipped in imported.skippedProfiles { + log.default("Profile %{public}@ not imported: %{public}@", skipped.name, skipped.reason) + } + for skipped in imported.skippedScheduledSwitches { + log.default("Scheduled switch %{public}@ not imported: %{public}@", skipped.name, skipped.reason) + } + } + private func constructTherapySettingsViewModel(therapySettings: TherapySettings) -> TherapySettingsViewModel? { return TherapySettingsViewModel(therapySettings: therapySettings, pumpSupportedIncrements: nil, prescription: nil, delegate: self) } diff --git a/LoopOnboardingKitUI/Views/ImportSettingsView.swift b/LoopOnboardingKitUI/Views/ImportSettingsView.swift index 8577ef5..077cd96 100644 --- a/LoopOnboardingKitUI/Views/ImportSettingsView.swift +++ b/LoopOnboardingKitUI/Views/ImportSettingsView.swift @@ -6,32 +6,105 @@ // import SwiftUI +import LoopKit import LoopKitUI +import NightscoutServiceKit struct ImportSettingsView: View { + @Environment(\.guidanceColors) private var guidanceColors private var didFinish: ((Bool) -> Void)? - private var settingsDate: Date + private var importedTherapySettings: ImportedTherapySettings private var settingsAgeFormatter: RelativeDateTimeFormatter = { let formatter = RelativeDateTimeFormatter() return formatter }() - init(settingsDate: Date, didFinish: ((Bool) -> Void)?) { - self.settingsDate = settingsDate + init(importedTherapySettings: ImportedTherapySettings, didFinish: ((Bool) -> Void)?) { + self.importedTherapySettings = importedTherapySettings self.didFinish = didFinish } + private var settingsDate: Date { + importedTherapySettings.date + } + + /// The imported profiles, the active one marked, when there is more than Default. + private var profilesText: String? { + let therapySettings = importedTherapySettings.therapySettings + guard let profiles = therapySettings.therapyProfiles, profiles.count > 1 else { + return nil + } + let activeName = TherapyProfiles.activeName(therapySettings.activeProfileName) + let names = profiles.map { profile in + profile.name == activeName ? String(format: LocalizedString("%1$@ (active)", comment: "Imported profile name of the active profile (1: profile name)"), profile.name) : profile.name + } + return String(format: LocalizedString("Profiles: %1$@", comment: "List of imported therapy profiles (1: profile names)"), names.joined(separator: ", ")) + } + + /// The imported scheduled switches as a week from Monday, the paused ones marked. + private var scheduledSwitchesText: String? { + guard let entries = importedTherapySettings.therapySettings.scheduledSwitches, !entries.isEmpty else { + return nil + } + let descriptions = ScheduledProfileSwitches.sortedAsWeek(entries).map { entry in + entry.paused ? String(format: LocalizedString("%1$@ (paused)", comment: "Imported scheduled profile switch that is paused (1: scheduled switch description)"), entry.localizedDescription) : entry.localizedDescription + } + return String(format: LocalizedString("Scheduled switches: %1$@\n\nEach switch first runs at its next time after the import. You can change or pause them under Settings → Profiles.", comment: "List of imported scheduled profile switches (1: scheduled switch descriptions)"), descriptions.joined(separator: ", ")) + } + + private var skippedProfilesText: String? { + skippedText(heading: LocalizedString("These profiles will not be imported:", comment: "Heading of the therapy profiles that will not be imported"), items: importedTherapySettings.skippedProfiles) + } + + private var skippedScheduledSwitchesText: String? { + skippedText(heading: LocalizedString("These scheduled switches will not be imported:", comment: "Heading of the scheduled profile switches that will not be imported"), items: importedTherapySettings.skippedScheduledSwitches) + } + + private func skippedText(heading: String, items: [ImportedTherapySettings.SkippedProfile]) -> String? { + guard !items.isEmpty else { + return nil + } + let reasons = items.map { String(format: LocalizedString("%1$@: %2$@", comment: "A therapy profile or scheduled switch that will not be imported (1: profile name or scheduled switch description, 2: reason)"), $0.name, $0.reason) } + return heading + "\n" + reasons.joined(separator: "\n") + } + var body: some View { VStack(alignment: .center, spacing: 20) { - Text(LocalizedString("Settings Found", comment: "Title on ImportSettingsView")) - .font(.largeTitle) - .fontWeight(.semibold) - Text(String(format: LocalizedString("We've detected Loop settings stored in your Nightscout! They were last updated %1$@. Would you like to import them?\n\nAfter importing, you will still need to review the imported settings in the following screens and verify that they are correct.", comment: "Format string for main guidance text on ImportSettingsView (1: age of settings)"), settingsAgeFormatter.localizedString(fromTimeInterval: settingsDate.timeIntervalSinceNow))) - .fixedSize(horizontal: false, vertical: true) - .foregroundColor(.secondary) - Spacer() + ScrollView { + VStack(alignment: .center, spacing: 20) { + Text(LocalizedString("Settings Found", comment: "Title on ImportSettingsView")) + .font(.largeTitle) + .fontWeight(.semibold) + Text(String(format: LocalizedString("We've detected Loop settings stored in your Nightscout! They were last updated %1$@. Would you like to import them?\n\nAfter importing, you will still need to review the imported settings in the following screens and verify that they are correct.", comment: "Format string for main guidance text on ImportSettingsView (1: age of settings)"), settingsAgeFormatter.localizedString(fromTimeInterval: settingsDate.timeIntervalSinceNow))) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.secondary) + if let profilesText { + Text(profilesText) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.secondary) + } + if let scheduledSwitchesText { + Text(scheduledSwitchesText) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.secondary) + .accessibilityIdentifier("text_ScheduledSwitches") + } + if let skippedProfilesText { + Text(skippedProfilesText) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(guidanceColors.warning) + .accessibilityIdentifier("text_SkippedProfiles") + } + if let skippedScheduledSwitchesText { + Text(skippedScheduledSwitchesText) + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(guidanceColors.warning) + .accessibilityIdentifier("text_SkippedScheduledSwitches") + } + } + } Button(action: { self.didFinish?(true) }) { @@ -54,7 +127,7 @@ struct ImportSettingsView: View { struct ImportSettingsView_Previews: PreviewProvider { static var previews: some View { NavigationView { - ImportSettingsView(settingsDate: Date().addingTimeInterval(-24 * 60 * 60)) { (shouldImport) in + ImportSettingsView(importedTherapySettings: ImportedTherapySettings(therapySettings: TherapySettings(), date: Date().addingTimeInterval(-24 * 60 * 60), skippedProfiles: [])) { (shouldImport) in print("Should import: \(shouldImport)") } }