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
21 changes: 21 additions & 0 deletions LoopOnboardingKitUI/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -674,6 +683,9 @@
}
}
},
"Profiles: %1$@" : {
"comment" : "List of imported therapy profiles (1: profile names)"
},
"Save Settings" : {
"comment" : "Therapy settings save button title",
"localizations" : {
Expand Down Expand Up @@ -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" : {
Expand Down Expand Up @@ -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" : {
Expand Down
32 changes: 24 additions & 8 deletions LoopOnboardingKitUI/View Controllers/OnboardingUICoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -156,15 +155,16 @@ 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()
}
return hostingController(rootView: view)
case .suspendThresholdInfo:
let therapySettings: TherapySettings
if let importedTherapySettings = importedTherapySettings, shouldUseImportedSettings {
therapySettings = importedTherapySettings
therapySettings = importedTherapySettings.therapySettings
} else {
therapySettings = initialTherapySettings
}
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
95 changes: 84 additions & 11 deletions LoopOnboardingKitUI/Views/ImportSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}) {
Expand All @@ -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)")
}
}
Expand Down