diff --git a/TidepoolService.xcodeproj/project.pbxproj b/TidepoolService.xcodeproj/project.pbxproj index 96b7481..2b9bca8 100644 --- a/TidepoolService.xcodeproj/project.pbxproj +++ b/TidepoolService.xcodeproj/project.pbxproj @@ -62,6 +62,15 @@ A9DAAD6A22E7E81E00E76C9F /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A9DAAD6922E7E81E00E76C9F /* LoopKitUI.framework */; }; A9DAAD6D22E7EA8F00E76C9F /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */; }; A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */; }; + E93BA05E24A14CBA00C5D7E6 /* PrescriptionCodeEntryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93BA05D24A14CBA00C5D7E6 /* PrescriptionCodeEntryViewModel.swift */; }; + E93BA06024A15FA800C5D7E6 /* PrescriptionDeviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93BA05F24A15FA800C5D7E6 /* PrescriptionDeviceView.swift */; }; + E93BA06224A29C9C00C5D7E6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */; }; + E9692172249BC73600D9BE3B /* MockPrescriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9692171249BC73600D9BE3B /* MockPrescriptionManager.swift */; }; + E9692174249BF2A100D9BE3B /* MockPrescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9692173249BF2A100D9BE3B /* MockPrescription.swift */; }; + E9692176249C1AE700D9BE3B /* PrescriptionReviewUICoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9692175249C1AE700D9BE3B /* PrescriptionReviewUICoordinator.swift */; }; + E96AEB98249C2FF1003797B4 /* PrescriptionCodeEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96AEB97249C2FF1003797B4 /* PrescriptionCodeEntryView.swift */; }; + E991F1B724A548580059281B /* AdaptiveKeyboardPadding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E991F1B624A548580059281B /* AdaptiveKeyboardPadding.swift */; }; + E991F1BB24A654CC0059281B /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = E991F1BA24A654CC0059281B /* TimeInterval.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -168,6 +177,15 @@ A9DAAD6922E7E81E00E76C9F /* LoopKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = LoopKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentifiableClass.swift; sourceTree = ""; }; A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibLoadable.swift; sourceTree = ""; }; + E93BA05D24A14CBA00C5D7E6 /* PrescriptionCodeEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrescriptionCodeEntryViewModel.swift; sourceTree = ""; }; + E93BA05F24A15FA800C5D7E6 /* PrescriptionDeviceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrescriptionDeviceView.swift; sourceTree = ""; }; + E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + E9692171249BC73600D9BE3B /* MockPrescriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrescriptionManager.swift; sourceTree = ""; }; + E9692173249BF2A100D9BE3B /* MockPrescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPrescription.swift; sourceTree = ""; }; + E9692175249C1AE700D9BE3B /* PrescriptionReviewUICoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrescriptionReviewUICoordinator.swift; sourceTree = ""; }; + E96AEB97249C2FF1003797B4 /* PrescriptionCodeEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrescriptionCodeEntryView.swift; sourceTree = ""; }; + E991F1B624A548580059281B /* AdaptiveKeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveKeyboardPadding.swift; sourceTree = ""; }; + E991F1BA24A654CC0059281B /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -235,6 +253,7 @@ A9309CAE2436C52900E02268 /* StoredGlucoseSample.swift */, A97A60FA243818C900AD69A5 /* TDatum.swift */, A9309CA2243563CD00E02268 /* TOrigin.swift */, + E991F1BA24A654CC0059281B /* TimeInterval.swift */, ); path = Extensions; sourceTree = ""; @@ -303,6 +322,7 @@ A9DAAD0022E7987800E76C9F /* TidepoolServiceKit */ = { isa = PBXGroup; children = ( + E9692170249BC69B00D9BE3B /* Mocks */, A9DAAD0122E7987800E76C9F /* TidepoolServiceKit.h */, A9DAAD0222E7987800E76C9F /* Info.plist */, A9DAAD3522E7CAC100E76C9F /* TidepoolService.swift */, @@ -323,7 +343,10 @@ A9DAAD1C22E7988900E76C9F /* TidepoolServiceKitUI */ = { isa = PBXGroup; children = ( + E93BA05C24A126E000C5D7E6 /* View Models */, + E9692179249C1B9400D9BE3B /* Views */, A9DAAD1D22E7988900E76C9F /* TidepoolServiceKitUI.h */, + E9692177249C1B0F00D9BE3B /* View Controllers */, A9DAAD1E22E7988900E76C9F /* Info.plist */, A9DAAD6C22E7EA8F00E76C9F /* IdentifiableClass.swift */, A9DAAD6E22E7EA9700E76C9F /* NibLoadable.swift */, @@ -332,6 +355,7 @@ A92E770022E9181500591027 /* TidepoolServiceSetupViewController.swift */, A94AE4F6235A907C005CA320 /* Extensions */, A9DAAD4F22E7DFD400E76C9F /* Localizable.strings */, + E93BA06124A29C9C00C5D7E6 /* Assets.xcassets */, ); path = TidepoolServiceKitUI; sourceTree = ""; @@ -359,6 +383,41 @@ name = Frameworks; sourceTree = ""; }; + E93BA05C24A126E000C5D7E6 /* View Models */ = { + isa = PBXGroup; + children = ( + E93BA05D24A14CBA00C5D7E6 /* PrescriptionCodeEntryViewModel.swift */, + ); + path = "View Models"; + sourceTree = ""; + }; + E9692170249BC69B00D9BE3B /* Mocks */ = { + isa = PBXGroup; + children = ( + E9692173249BF2A100D9BE3B /* MockPrescription.swift */, + E9692171249BC73600D9BE3B /* MockPrescriptionManager.swift */, + ); + path = Mocks; + sourceTree = ""; + }; + E9692177249C1B0F00D9BE3B /* View Controllers */ = { + isa = PBXGroup; + children = ( + E9692175249C1AE700D9BE3B /* PrescriptionReviewUICoordinator.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; + E9692179249C1B9400D9BE3B /* Views */ = { + isa = PBXGroup; + children = ( + E96AEB97249C2FF1003797B4 /* PrescriptionCodeEntryView.swift */, + E93BA05F24A15FA800C5D7E6 /* PrescriptionDeviceView.swift */, + E991F1B624A548580059281B /* AdaptiveKeyboardPadding.swift */, + ); + path = Views; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -578,6 +637,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + E93BA06224A29C9C00C5D7E6 /* Assets.xcassets in Resources */, A9DAAD4D22E7DFD400E76C9F /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -693,11 +753,14 @@ A9309CA52435986300E02268 /* DeletedCarbEntry.swift in Sources */, A9309CAF2436C52900E02268 /* StoredGlucoseSample.swift in Sources */, A9D1107C242289720091C620 /* HKUnit.swift in Sources */, + E9692174249BF2A100D9BE3B /* MockPrescription.swift in Sources */, A9309CA72435987000E02268 /* StoredCarbEntry.swift in Sources */, A97A60FB243818C900AD69A5 /* TDatum.swift in Sources */, A9DAAD3322E7CA1A00E76C9F /* LocalizedString.swift in Sources */, A913B37D24200C97000805C4 /* Bundle.swift in Sources */, + E991F1BB24A654CC0059281B /* TimeInterval.swift in Sources */, A9309CA3243563CD00E02268 /* TOrigin.swift in Sources */, + E9692172249BC73600D9BE3B /* MockPrescriptionManager.swift in Sources */, A97651752421AA10002EB5D4 /* OSLog.swift in Sources */, A9DAAD3622E7CAC100E76C9F /* TidepoolService.swift in Sources */, ); @@ -716,10 +779,15 @@ files = ( A9DAAD6D22E7EA8F00E76C9F /* IdentifiableClass.swift in Sources */, A94AE4F8235A909A005CA320 /* UIColor.swift in Sources */, + E991F1B724A548580059281B /* AdaptiveKeyboardPadding.swift in Sources */, + E96AEB98249C2FF1003797B4 /* PrescriptionCodeEntryView.swift in Sources */, A97651762421AA11002EB5D4 /* OSLog.swift in Sources */, + E93BA06024A15FA800C5D7E6 /* PrescriptionDeviceView.swift in Sources */, + E9692176249C1AE700D9BE3B /* PrescriptionReviewUICoordinator.swift in Sources */, A9DAAD3422E7CA1A00E76C9F /* LocalizedString.swift in Sources */, A9DAAD3922E7DEE000E76C9F /* TidepoolService+UI.swift in Sources */, A9DAAD6F22E7EA9700E76C9F /* NibLoadable.swift in Sources */, + E93BA05E24A14CBA00C5D7E6 /* PrescriptionCodeEntryViewModel.swift in Sources */, A92E770122E9181500591027 /* TidepoolServiceSetupViewController.swift in Sources */, A9DAAD3B22E7DEF100E76C9F /* TidepoolServiceSettingsViewController.swift in Sources */, ); @@ -1188,6 +1256,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_PREVIEWS = YES; INFOPLIST_FILE = TidepoolServiceKitUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( @@ -1214,6 +1283,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_PREVIEWS = YES; INFOPLIST_FILE = TidepoolServiceKitUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/TidepoolServiceKit/Extensions/TimeInterval.swift b/TidepoolServiceKit/Extensions/TimeInterval.swift new file mode 100644 index 0000000..6b2656e --- /dev/null +++ b/TidepoolServiceKit/Extensions/TimeInterval.swift @@ -0,0 +1,19 @@ +// +// TimeInterval.swift +// TidepoolServiceKit +// +// Created by Anna Quinlan on 6/26/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import Foundation + +extension TimeInterval { + public static func minutes(_ minutes: Double) -> TimeInterval { + return TimeInterval(minutes * 60 /* seconds in minute */) + } + + public static func hours(_ hours: Double) -> TimeInterval { + return TimeInterval(hours * 60 /* minutes in hr */ * 60 /* seconds in minute */) + } +} diff --git a/TidepoolServiceKit/Mocks/MockPrescription.swift b/TidepoolServiceKit/Mocks/MockPrescription.swift new file mode 100644 index 0000000..f8f7b70 --- /dev/null +++ b/TidepoolServiceKit/Mocks/MockPrescription.swift @@ -0,0 +1,109 @@ +// +// Prescription.swift +// TidepoolServiceKit +// +// Created by Anna Quinlan on 6/18/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import Foundation +import HealthKit +import LoopKit + + +public enum CGMType: String, Codable { + case g6 +} + +public enum PumpType: String, Codable { + case dash +} + +public enum TrainingType: String, Codable { + case inPerson // Patient must have hands-on training with clinician/CDE + case inModule // Patient can train in-app + +} + +public enum BGUnit: String, Codable { + case mgdl + case mmol + + var hkUnit: HKUnit { + switch self { + case .mgdl: + return .milligramsPerDeciliter + case .mmol: + return .millimolesPerLiter + } + } +} + +public struct MockPrescription: Codable { + public let datePrescribed: Date // Date prescription was prescribed + public let providerName: String // Name of clinician prescribing + public let cgm: CGMType // CGM type (manufacturer & model) + public let pump: PumpType // Pump type (manufacturer & model) + public let bloodGlucoseUnit: BGUnit + public let basalRateSchedule: BasalRateSchedule + public let glucoseTargetRangeSchedule: GlucoseRangeSchedule + public let carbRatioSchedule: CarbRatioSchedule + public let insulinSensitivitySchedule: InsulinSensitivitySchedule + public let maximumBasalRatePerHour: Double + public let maximumBolus: Double + public let suspendThreshold: GlucoseThreshold + public let insulinModel: InsulinModel + public let preMealTargetRange: DoubleRange + public let workoutTargetRange: DoubleRange + + public init(datePrescribed: Date, + providerName: String, + cgmType: CGMType, + pumpType: PumpType, + bloodGlucoseUnit: BGUnit, + basalRateSchedule: BasalRateSchedule, + glucoseTargetRangeSchedule: GlucoseRangeSchedule, + carbRatioSchedule: CarbRatioSchedule, + insulinSensitivitySchedule: InsulinSensitivitySchedule, + maximumBasalRatePerHour: Double, + maximumBolus: Double, + suspendThreshold: GlucoseThreshold, + insulinModel: InsulinModel, + preMealTargetRange: DoubleRange, + workoutTargetRange: DoubleRange) { + self.datePrescribed = datePrescribed + self.providerName = providerName + self.cgm = cgmType + self.pump = pumpType + self.bloodGlucoseUnit = bloodGlucoseUnit + self.basalRateSchedule = basalRateSchedule + self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule + self.carbRatioSchedule = carbRatioSchedule + self.insulinSensitivitySchedule = insulinSensitivitySchedule + self.maximumBasalRatePerHour = maximumBasalRatePerHour + self.maximumBolus = maximumBolus + self.suspendThreshold = suspendThreshold + self.insulinModel = insulinModel + self.preMealTargetRange = preMealTargetRange + self.workoutTargetRange = workoutTargetRange + } + + public struct InsulinModel: Codable, Equatable { + public enum ModelType: String, Codable { + case fiasp + case rapidAdult + case rapidChild + case walsh + } + + public let modelType: ModelType + public let actionDuration: TimeInterval + public let peakActivity: TimeInterval? + + public init(modelType: ModelType, actionDuration: TimeInterval, peakActivity: TimeInterval? = nil) { + self.modelType = modelType + self.actionDuration = actionDuration + self.peakActivity = peakActivity + } + } +} diff --git a/TidepoolServiceKit/Mocks/MockPrescriptionManager.swift b/TidepoolServiceKit/Mocks/MockPrescriptionManager.swift new file mode 100644 index 0000000..3c8969b --- /dev/null +++ b/TidepoolServiceKit/Mocks/MockPrescriptionManager.swift @@ -0,0 +1,87 @@ +// +// MockPrescriptionManager.swift +// TidepoolServiceKit +// +// Created by Anna Quinlan on 6/18/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import Foundation +import LoopKit + +public class MockPrescriptionManager { + private var prescription: MockPrescription + + public init(prescription: MockPrescription? = nil) { + if let prescription = prescription { + self.prescription = prescription + } else { + let timeZone = TimeZone(identifier: "America/Los_Angeles")! + let glucoseTargetRangeSchedule = GlucoseRangeSchedule( + rangeSchedule: DailyQuantitySchedule(unit: .milligramsPerDeciliter, + dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: DoubleRange(minValue: 100.0, maxValue: 110.0)), + RepeatingScheduleValue(startTime: .hours(8), value: DoubleRange(minValue: 95.0, maxValue: 105.0)), + RepeatingScheduleValue(startTime: .hours(14), value: DoubleRange(minValue: 95.0, maxValue: 105.0)), + RepeatingScheduleValue(startTime: .hours(16), value: DoubleRange(minValue: 100.0, maxValue: 110.0)), + RepeatingScheduleValue(startTime: .hours(18), value: DoubleRange(minValue: 90.0, maxValue: 100.0)), + RepeatingScheduleValue(startTime: .hours(21), value: DoubleRange(minValue: 110.0, maxValue: 120.0))], + timeZone: timeZone)!, + override: GlucoseRangeSchedule.Override(value: DoubleRange(minValue: 80.0, maxValue: 90.0), + start: Date().addingTimeInterval(.minutes(-30)), + end: Date().addingTimeInterval(.minutes(30))) + ) + let basalRateSchedule = BasalRateSchedule( + dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 1.0), + RepeatingScheduleValue(startTime: .hours(8), value: 1.125), + RepeatingScheduleValue(startTime: .hours(10), value: 1.25), + RepeatingScheduleValue(startTime: .hours(12), value: 1.5), + RepeatingScheduleValue(startTime: .hours(14), value: 1.25), + RepeatingScheduleValue(startTime: .hours(16), value: 1.5), + RepeatingScheduleValue(startTime: .hours(18), value: 1.25), + RepeatingScheduleValue(startTime: .hours(21), value: 1.0)], + timeZone: timeZone)! + let insulinSensitivitySchedule = InsulinSensitivitySchedule( + unit: .milligramsPerDeciliter, + dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 45.0), + RepeatingScheduleValue(startTime: .hours(8), value: 40.0), + RepeatingScheduleValue(startTime: .hours(10), value: 35.0), + RepeatingScheduleValue(startTime: .hours(12), value: 30.0), + RepeatingScheduleValue(startTime: .hours(14), value: 35.0), + RepeatingScheduleValue(startTime: .hours(16), value: 40.0)], + timeZone: timeZone)! + let carbRatioSchedule = CarbRatioSchedule( + unit: .gram(), + + dailyItems: [RepeatingScheduleValue(startTime: .hours(0), value: 10.0), + RepeatingScheduleValue(startTime: .hours(8), value: 12.0), + RepeatingScheduleValue(startTime: .hours(10), value: 9.0), + RepeatingScheduleValue(startTime: .hours(12), value: 10.0), + RepeatingScheduleValue(startTime: .hours(14), value: 11.0), + RepeatingScheduleValue(startTime: .hours(16), value: 12.0), + RepeatingScheduleValue(startTime: .hours(18), value: 8.0), + RepeatingScheduleValue(startTime: .hours(21), value: 10.0)], + timeZone: timeZone)! + + self.prescription = MockPrescription( + datePrescribed: Date(), + providerName: "Sally Seastar", + cgmType: CGMType.g6, + pumpType: PumpType.dash, + bloodGlucoseUnit: .mgdl, + basalRateSchedule: basalRateSchedule, + glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, + carbRatioSchedule: carbRatioSchedule, + insulinSensitivitySchedule: insulinSensitivitySchedule, + maximumBasalRatePerHour: 3.0, + maximumBolus: 5.0, + suspendThreshold: GlucoseThreshold(unit: .milligramsPerDeciliter, value: 70), + insulinModel: MockPrescription.InsulinModel(modelType: .rapidAdult, actionDuration: .hours(6), peakActivity: .hours(3)), + preMealTargetRange: DoubleRange(minValue: 80.0, maxValue: 90.0), + workoutTargetRange: DoubleRange(minValue: 80.0, maxValue: 90.0)) + } + } + + public func getPrescriptionData(completion: @escaping (Result) -> Void) { + completion(.success(self.prescription)) + } +} diff --git a/TidepoolServiceKit/TidepoolService.swift b/TidepoolServiceKit/TidepoolService.swift index 6fec8b4..8d607fd 100644 --- a/TidepoolServiceKit/TidepoolService.swift +++ b/TidepoolServiceKit/TidepoolService.swift @@ -12,6 +12,7 @@ import TidepoolKit public enum TidepoolServiceError: Error { case configuration + case noPrescriptionDataAvailable } public protocol SessionStorage { @@ -145,6 +146,13 @@ public final class TidepoolService: Service { } } } + + public func getPrescriptionData(completion: @escaping (Result) -> Void) { + MockPrescriptionManager().getPrescriptionData { result in + completion(result) + } + // TODO: add in proper query to backend + } private var sessionService: String { "org.tidepool.TidepoolService.\(id)" } } diff --git a/TidepoolServiceKitUI/Assets.xcassets/Contents.json b/TidepoolServiceKitUI/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/TidepoolServiceKitUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TidepoolServiceKitUI/Assets.xcassets/blue gray.colorset/Contents.json b/TidepoolServiceKitUI/Assets.xcassets/blue gray.colorset/Contents.json new file mode 100644 index 0000000..6ae1fa9 --- /dev/null +++ b/TidepoolServiceKitUI/Assets.xcassets/blue gray.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x8D", + "green" : "0x78", + "red" : "0x6A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/Contents.json b/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/Contents.json new file mode 100644 index 0000000..07a24cb --- /dev/null +++ b/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dash.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/dash.pdf b/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/dash.pdf new file mode 100644 index 0000000..0407693 Binary files /dev/null and b/TidepoolServiceKitUI/Assets.xcassets/dash.imageset/dash.pdf differ diff --git a/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/Contents.json b/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/Contents.json new file mode 100644 index 0000000..f989182 --- /dev/null +++ b/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "dexcom.pdf", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/dexcom.pdf b/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/dexcom.pdf new file mode 100644 index 0000000..81968bc Binary files /dev/null and b/TidepoolServiceKitUI/Assets.xcassets/dexcom.imageset/dexcom.pdf differ diff --git a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift b/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift index 17cd38f..340d153 100644 --- a/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift +++ b/TidepoolServiceKitUI/TidepoolServiceSetupViewController.swift @@ -7,6 +7,7 @@ // import LoopKitUI +import SwiftUI import TidepoolKit import TidepoolKitUI import TidepoolServiceKit @@ -31,6 +32,7 @@ final class TidepoolServiceSetupViewController: UIViewController, TLoginSignupDe title = service.localizedTitle navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(startFlow)) var loginSignupViewController = service.tapi.loginSignupViewController() loginSignupViewController.delegate = self @@ -45,6 +47,13 @@ final class TidepoolServiceSetupViewController: UIViewController, TLoginSignupDe @objc private func cancel() { notifyComplete() } + + + @objc private func startFlow() { + let setupViewController = PrescriptionReviewUICoordinator() + setupViewController.completionDelegate = self + self.present(setupViewController, animated: true, completion: nil) + } func loginSignup(_ loginSignup: TLoginSignup, didCreateSession session: TSession, completion: @escaping (Error?) -> Void) { service.completeCreate(withSession: session) { error in @@ -68,3 +77,11 @@ final class TidepoolServiceSetupViewController: UIViewController, TLoginSignupDe } } } + +extension TidepoolServiceSetupViewController: CompletionDelegate { + func completionNotifyingDidComplete(_ object: CompletionNotifying) { + if let vc = object as? UIViewController, presentedViewController === vc { + dismiss(animated: true, completion: nil) + } + } +} diff --git a/TidepoolServiceKitUI/View Controllers/PrescriptionReviewUICoordinator.swift b/TidepoolServiceKitUI/View Controllers/PrescriptionReviewUICoordinator.swift new file mode 100644 index 0000000..967f48a --- /dev/null +++ b/TidepoolServiceKitUI/View Controllers/PrescriptionReviewUICoordinator.swift @@ -0,0 +1,114 @@ +// +// PrescriptionReviewUICoordinator.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/18/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import Foundation +import SwiftUI +import LoopKitUI + +enum PrescriptionReviewScreen { + case enterCode + case reviewDevices + + func next() -> PrescriptionReviewScreen? { + switch self { + case .enterCode: + return .reviewDevices + case .reviewDevices: + return nil + } + } +} + +class PrescriptionReviewUICoordinator: UINavigationController, CompletionNotifying, UINavigationControllerDelegate { + var screenStack = [PrescriptionReviewScreen]() + weak var completionDelegate: CompletionDelegate? + + let viewModel = PrescriptionCodeEntryViewModel() + + var currentScreen: PrescriptionReviewScreen { + return screenStack.last! + } + + // TODO: create delegate so we can add settings to LoopDataManager + init() { + super.init(navigationBarClass: UINavigationBar.self, toolbarClass: UIToolbar.self) + } + + override func viewDidLoad() { + super.viewDidLoad() + delegate = self + self.navigationBar.prefersLargeTitles = true // ensure nav bar text is displayed correctly + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func viewControllerForScreen(_ screen: PrescriptionReviewScreen) -> UIViewController { + switch screen { + case .enterCode: + viewModel.didCancel = { [weak self] in + self?.setupCanceled() + } + viewModel.didFinishStep = { [weak self] in + self?.stepFinished() + } + let view = PrescriptionCodeEntryView(viewModel: viewModel) + return DismissibleHostingController(rootView: view) + case .reviewDevices: + viewModel.didFinishStep = { [weak self] in + self?.stepFinished() + } + guard let prescription = viewModel.prescription else { + // Go back to code entry step if we don't have prescription + let view = PrescriptionCodeEntryView(viewModel: viewModel) + return DismissibleHostingController(rootView: view) + } + let view = PrescriptionDeviceView(viewModel: viewModel, prescription: prescription) + return DismissibleHostingController(rootView: view) + } + } + + public func navigationController(_ navigationController: UINavigationController, + willShow viewController: UIViewController, + animated: Bool) { + // Pop the current screen from the stack if we're navigating back + if viewControllers.count < screenStack.count { + // Navigation back + let _ = screenStack.popLast() + } + } + + private func determineFirstScreen() -> PrescriptionReviewScreen { + return .enterCode + } + + override func viewWillAppear(_ animated: Bool) { + screenStack = [determineFirstScreen()] + let viewController = viewControllerForScreen(currentScreen) + setViewControllers([viewController], animated: false) + } + + private func setupCanceled() { + completionDelegate?.completionNotifyingDidComplete(self) + } + + private func stepFinished() { + if let nextStep = currentScreen.next() { + navigate(to: nextStep) + } else { + completionDelegate?.completionNotifyingDidComplete(self) + } + } + + func navigate(to screen: PrescriptionReviewScreen) { + screenStack.append(screen) + let viewController = viewControllerForScreen(screen) + self.pushViewController(viewController, animated: true) + } +} diff --git a/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryModel.swift b/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryModel.swift new file mode 100644 index 0000000..68ca55e --- /dev/null +++ b/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryModel.swift @@ -0,0 +1,32 @@ +// +// PrescriptionCodeEntryModel.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/22/20. +// Copyright © 2020 LoopKit Authors. All rights reserved. +// + +import Foundation +import TidepoolServiceKit + +class PrescriptionCodeEntryViewModel: ObservableObject { + var didFinish: (() -> Void)? + var didCancel: (() -> Void)? + + var prescription: Prescription? + + func loadPrescriptionFromCode(prescriptionCode: String) { + #if targetEnvironment(simulator) + MockPrescriptionManager().getPrescriptionData { result in + switch result { + case .failure: + fatalError("Mock prescription manager should always return a prescription") + case .success(let prescription): + self.prescription = prescription + } + } + #else + // TODO: add in proper query to backend + #endif + } +} diff --git a/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryViewModel.swift b/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryViewModel.swift new file mode 100644 index 0000000..f6dc406 --- /dev/null +++ b/TidepoolServiceKitUI/View Models/PrescriptionCodeEntryViewModel.swift @@ -0,0 +1,52 @@ +// +// PrescriptionCodeEntryViewModel.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/22/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import Foundation +import TidepoolServiceKit + +class PrescriptionCodeEntryViewModel: ObservableObject { + var didFinishStep: (() -> Void) + var didCancel: (() -> Void)? + + var prescription: MockPrescription? + let prescriptionCodeLength = 4 + + init(finishedStepHandler: @escaping () -> Void = { }) { + self.didFinishStep = finishedStepHandler + } + + func entryNavigation(success: Bool) { + if success { + didFinishStep() + } else { + // TODO: handle error + } + } + + func validatePrescriptionCode(prescriptionCode: String) -> Bool { + return prescriptionCode.count == prescriptionCodeLength + } + + func loadPrescriptionFromCode(prescriptionCode: String) { + guard validatePrescriptionCode(prescriptionCode: prescriptionCode) else { + // TODO: handle error + return + } + + // TODO: call function to properly query the backend; if prescription couldn't be retrieved, raise unableToRetreivePrescription error + MockPrescriptionManager().getPrescriptionData { result in + switch result { + case .failure: + fatalError("Mock prescription manager should always return a prescription") + case .success(let prescription): + self.prescription = prescription + self.entryNavigation(success: true) + } + } + } +} diff --git a/TidepoolServiceKitUI/Views/AdaptiveKeyboardPadding.swift b/TidepoolServiceKitUI/Views/AdaptiveKeyboardPadding.swift new file mode 100644 index 0000000..3ac28a4 --- /dev/null +++ b/TidepoolServiceKitUI/Views/AdaptiveKeyboardPadding.swift @@ -0,0 +1,37 @@ +// +// AdaptiveKeyboardPadding.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/25/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// +import Combine +import SwiftUI + +public struct AdaptiveKeyboardPadding: ViewModifier { + @State private var keyboardHeight: CGFloat = 0 + + private var keyboardHeightPublisher: AnyPublisher { + Publishers.Merge( + NotificationCenter.default + .publisher(for: UIResponder.keyboardWillShowNotification) + .compactMap { $0.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue } + .map { $0.cgRectValue.height }, + NotificationCenter.default + .publisher(for: UIResponder.keyboardWillHideNotification) + .map { _ in CGFloat(0) } + ).eraseToAnyPublisher() + } + + public func body(content: Content) -> some View { + content + .padding(.bottom, keyboardHeight) + .onReceive(keyboardHeightPublisher) { self.keyboardHeight = $0 } + } +} + +extension View { + func adaptiveKeyboardPadding() -> some View { + ModifiedContent(content: self, modifier: AdaptiveKeyboardPadding()) + } +} diff --git a/TidepoolServiceKitUI/Views/PrescriptionCodeEntryView.swift b/TidepoolServiceKitUI/Views/PrescriptionCodeEntryView.swift new file mode 100644 index 0000000..fe2f8ee --- /dev/null +++ b/TidepoolServiceKitUI/Views/PrescriptionCodeEntryView.swift @@ -0,0 +1,132 @@ +// +// PrescriptionCodeEntryView.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/18/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import SwiftUI +import LoopKitUI + +struct PrescriptionCodeEntryView: View, HorizontalSizeClassOverride { + + @State private var prescriptionCode: String = "" + @ObservedObject var viewModel: PrescriptionCodeEntryViewModel + + let blueGray = Color("blue gray", bundle: Bundle(for: PrescriptionReviewUICoordinator.self)) + + var body: some View { + List { + VStack(alignment: .leading, spacing: 25) { + itemsNeededList + codeEntryRequest + } + .padding(.vertical) + submitCodeButton + requestPrescriptionButton + Spacer() + } + .adaptiveKeyboardPadding() // To ensure the keyboard doesn't obstruct the TextField + .buttonStyle(BorderlessButtonStyle()) // Fix for button click highlighting the whole cell + .environment(\.horizontalSizeClass, horizontalOverride) + .navigationBarItems(trailing: cancelButton) + .navigationBarTitle(Text(LocalizedString("Your Settings", comment: "Navigation view title"))) + .onAppear() { + UITableView.appearance().separatorStyle = .none // Remove lines between sections + } + } + + private var cancelButton: some View { + Button(action: { + self.viewModel.didCancel?() + }) { + Text(LocalizedString("Cancel", comment: "Button text to exit the prescription code entry screen")) + .foregroundColor(.accentColor) + } + } + + private var itemsNeededDescription: some View { + VStack (alignment: .leading, spacing: 10) { + Text(LocalizedString("What you'll need", comment: "Title for section describing items needed to review settings")) + .font(.headline) + Text(LocalizedString("For the next section, you'll want to have the following:", comment: "Subheader for items-needed section")) + .foregroundColor(blueGray) + .fixedSize(horizontal: false, vertical: true) // prevent text from being cut off + } + + } + + private var itemsNeededList: some View { + Section { + VStack (alignment: .leading, spacing: 10) { + itemsNeededDescription + InstructionList(instructions: [ + LocalizedString("Prescription activation code", comment: "Label text for the first needed prescription activation item"), + LocalizedString("Configuration settings for glucose targets and insulin delivery from your healthcare provider", comment: "Label text for the second needed prescription activation item") + ], + stepsColor: blueGray + ) + .foregroundColor(blueGray) + } + } + } + + private var codeEntryRequest: some View { + Section { + VStack (alignment: .leading, spacing: 10) { + Text(LocalizedString("Enter your prescription code", comment: "Title for section to enter your prescription code")) + .font(.headline) + Text(LocalizedString("If you have a prescription activation code, please enter it now.", comment: "Text requesting entry of activation code")) + .foregroundColor(blueGray) + prescriptionCodeInputField + } + } + + } + + private var prescriptionCodeInputField: some View { + TextField(LocalizedString("Activation code", comment: "Placeholder text before entering prescription code in text field"), text: $prescriptionCode) + .keyboardType(.default) + .disableAutocorrection(true) + .font(.body) + .multilineTextAlignment(.leading) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color.gray, lineWidth: 1) + + ) + } + + private var submitCodeButton: some View { + Button(action: { + self.viewModel.loadPrescriptionFromCode(prescriptionCode: self.prescriptionCode) + }) { + Text(LocalizedString("Submit activation code", comment: "Button title for submitting the prescription activation code to Tidepool")) + .actionButtonStyle(submitButtonStyle(enabled: prescriptionCode.count == self.viewModel.prescriptionCodeLength)) + .disabled(prescriptionCode.count != viewModel.prescriptionCodeLength) + } + } + + private func submitButtonStyle(enabled: Bool) -> ActionButton.ButtonType { + return enabled ? .primary : .secondary + } + + private var requestPrescriptionButton: some View { + Button(action: { + // TODO: open contact prescriber window + print("TODO") + }) { + Text(LocalizedString("Request activation code", comment:"Button title for requesting a prescription activation code from the prescriber")) + .actionButtonStyle(.secondary) + } + } +} + +struct PrescriptionCodeEntryView_Previews: PreviewProvider { + static var previews: some View { + PrescriptionCodeEntryView(viewModel: PrescriptionCodeEntryViewModel()) + } +} + diff --git a/TidepoolServiceKitUI/Views/PrescriptionDeviceView.swift b/TidepoolServiceKitUI/Views/PrescriptionDeviceView.swift new file mode 100644 index 0000000..312f163 --- /dev/null +++ b/TidepoolServiceKitUI/Views/PrescriptionDeviceView.swift @@ -0,0 +1,157 @@ +// +// PrescriptionDeviceView.swift +// TidepoolServiceKitUI +// +// Created by Anna Quinlan on 6/22/20. +// Copyright © 2020 Tidepool Project. All rights reserved. +// + +import SwiftUI +import LoopKitUI +import TidepoolServiceKit + +struct PrescriptionDeviceView: View, HorizontalSizeClassOverride { + @State private var prescriptionCode: String = "" + @ObservedObject var viewModel: PrescriptionCodeEntryViewModel + var prescription: MockPrescription + + let blueGray = Color("blue gray", bundle: Bundle(for: PrescriptionReviewUICoordinator.self)) + static let imageWidth: CGFloat = 48 + + var body: some View { + List { + VStack(alignment: .leading, spacing: 25) { + prescribedDeviceInfo + devicesList + disclaimer + } + .padding(.vertical) + approveDevicesButton + editDevicesButton + Spacer() + } + .buttonStyle(BorderlessButtonStyle()) // Fix for button click highlighting the whole cell + .environment(\.horizontalSizeClass, horizontalOverride) + .navigationBarTitle(Text(LocalizedString("Review your settings", comment: "Navigation view title"))) + .onAppear() { + UITableView.appearance().separatorStyle = .none // Remove lines between sections + } + } + + private var prescribedDeviceInfo: some View { + Section { + Text(LocalizedString("Since your provider included your recommended settings with your prescription, you'll have the chance to review and accept each of these settings now.", comment: "Text describing purpose of settings walk-through")) + .foregroundColor(blueGray) + .fixedSize(horizontal: false, vertical: true) + } + } + + private var devicesList: some View { + Section { + VStack(alignment: .leading, spacing: 20) { + Text(LocalizedString("Your prescription contains recommended settings for the following devices:", comment: "Title for devices prescribed section")) + .foregroundColor(blueGray) + .fixedSize(horizontal: false, vertical: true) // prevent text from being cut off + // TODO: get images and descriptions from pump manager + pumpStack + cgmStack + } + } + } + + private var pumpStack: some View { + switch prescription.pump { + case .dash: + return dashStack + } + } + + private var dashStack: some View { + HStack { + dashIcon + .padding(.horizontal) + VStack (alignment: .leading) { + Text(LocalizedString("Omnipod 5", comment: "Text describing insulin pump name")) + Text(LocalizedString("Insulin Pump", comment: "Insulin pump label")) + .font(.footnote) + .foregroundColor(blueGray) + } + Spacer() + } + } + + private var dashIcon: some View { + Image("dash", bundle: Bundle(for: PrescriptionReviewUICoordinator.self)) + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: ContentMode.fit) + .frame(width: Self.imageWidth, height: 50) + .foregroundColor(.accentColor) + } + + private var cgmStack: some View { + switch prescription.cgm { + case .g6: + return dexcomStack + } + } + + private var dexcomStack: some View { + HStack { + dexcomIcon + .padding(.horizontal) + VStack (alignment: .leading) { + Text(LocalizedString("Dexcom G6", comment: "Text describing CGM name")) + Text(LocalizedString("Continuous Glucose Monitor", comment: "CGM label")) + .font(.footnote) + .foregroundColor(blueGray) + } + Spacer() + } + } + + private var dexcomIcon: some View { + Image("dexcom", bundle: Bundle(for: PrescriptionReviewUICoordinator.self)) + .resizable() + .aspectRatio(contentMode: ContentMode.fit) + .frame(width: Self.imageWidth) + } + + private var disclaimer: some View { + Section { + VStack (alignment: .leading) { + Text(LocalizedString("Note", comment: "Title for disclaimer section")) + .font(.headline) + VStack (alignment: .leading, spacing: 10) { + Text(LocalizedString("Tidepool Loop does NOT automatically adjust or recommend changes to your settings", comment: "Text describing that Tidepool Loop doesn't automatically change settings")) + .italic() + .fixedSize(horizontal: false, vertical: true) // prevent text from being cut off + .padding(.vertical) + Text(LocalizedString("Work with your healthcare provider to find the right settings for you", comment: "Text describing determining settings with your doctor")) + .italic() + } + .fixedSize(horizontal: false, vertical: true) // prevent text from being cut off + .foregroundColor(blueGray) + } + } + } + + private var approveDevicesButton: some View { + Button(action: { + self.viewModel.didFinishStep() + }) { + Text(LocalizedString("Next: Review Settings", comment: "Button title for approving devices")) + .actionButtonStyle(.primary) + } + } + + private var editDevicesButton: some View { + Button(action: { + // TODO: open window to edit the devices + print("TODO") + }) { + Text(LocalizedString("Edit devices", comment: "Button title for editing the prescribed devices")) + .actionButtonStyle(.secondary) + } + } +}