├── .gitignore ├── .gitmodules ├── Cartfile ├── Cartfile.resolved ├── Common ├── FrameworkLocalText.swift ├── IdentifiableClass.swift ├── LocalizedString.swift ├── NibLoadable.swift ├── NumberFormatter.swift ├── OSLog.swift ├── TimeInterval.swift └── TimeZone.swift ├── DashKit.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── PodUIDemo.xcscheme │ └── Shared.xcscheme ├── DashKit ├── BasalSchedule.swift ├── DashKit.h ├── DashPumpManager.swift ├── DashPumpManagerError.swift ├── DashPumpManagerState.swift ├── Extensions │ ├── AlarmCode.swift │ ├── Array.swift │ ├── ConnectionState.swift │ ├── PodAlerts.swift │ └── PodCommError.swift ├── Info.plist ├── Mocks │ ├── MockPodAlarm.swift │ ├── MockPodCommManager.swift │ ├── MockPodStatus.swift │ └── MockPodVersion.swift ├── PendingCommand.swift ├── Pod.swift ├── PodDoseProgressTimerEstimator.swift ├── PodSDKProtocol │ ├── CannulaInserter.swift │ ├── PDMRegistrator.swift │ ├── PodCommManager.swift │ ├── PodDeactivater.swift │ ├── PodPairer.swift │ ├── PodSDKLoggingShim.swift │ └── PodSDKProtocol.swift ├── PodStatusExtension.swift ├── PumpManagerAlert.swift ├── ReservoirLevel.swift └── UnfinalizedDose.swift ├── DashKitPlugin ├── DashKitPlugin.swift └── Info.plist ├── DashKitTests ├── BasalProgramTests.swift ├── DashPumpManagerTests.swift ├── Info.plist └── UnfinalizedDoseTests.swift ├── DashKitUI ├── Assets.xcassets │ ├── Cannula Inserted.imageset │ │ ├── CannulaInserted.png │ │ └── Contents.json │ ├── Contents.json │ ├── Fill Pod.dataset │ │ ├── Contents.json │ │ └── fillPod.gif │ ├── No Pod.imageset │ │ ├── Contents.json │ │ ├── NoPod-1.png │ │ ├── NoPod-2.png │ │ └── NoPod.png │ ├── Onboarding.imageset │ │ ├── Contents.json │ │ ├── Onboarding.png │ │ ├── Onboarding@2x.png │ │ └── Onboarding@3x.png │ ├── Pod Finish Activation.imageset │ │ ├── Contents.json │ │ └── Pod-FinishActivation.pdf │ ├── Pod Start Activation.imageset │ │ ├── Contents.json │ │ └── Pod-StartActivation.pdf │ ├── Pod.imageset │ │ ├── Contents.json │ │ ├── pod@3x-1.png │ │ ├── pod@3x-2.png │ │ └── pod@3x.png │ ├── Prep Pod.dataset │ │ ├── Contents.json │ │ └── prepPod.gif │ ├── pod_reservoir_mask_swiftui.imageset │ │ ├── Contents.json │ │ └── pod_reservoir_mask.svg │ └── pod_reservoir_swiftui.imageset │ │ ├── Contents.json │ │ └── pod_reservoir.svg ├── DashKitUI.h ├── DashPumpManager.storyboard ├── Extensions │ ├── Image.swift │ ├── UIImage.swift │ ├── UITableViewCell.swift │ └── UIViewController.swift ├── Info.plist ├── Mocks │ ├── MockCannulaInserter.swift │ ├── MockNavigator.swift │ ├── MockPodDeactivater.swift │ ├── MockPodDetails.swift │ ├── MockPodPairer.swift │ └── MockRegistrationManager.swift ├── Protocols │ └── PodDetails .swift ├── PumpManager │ ├── DashHUDProvider.swift │ └── DashPumpManager+UI.swift ├── UIViews │ ├── ExpirationReminderDateTableViewCell.swift │ ├── HUDAssets.xcassets │ │ ├── Contents.json │ │ ├── pod_life │ │ │ ├── Contents.json │ │ │ └── pod_life.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── pod_life.pdf │ │ └── reservoir │ │ │ ├── Contents.json │ │ │ ├── pod_reservoir.imageset │ │ │ ├── Contents.json │ │ │ └── pod_reservoir.pdf │ │ │ └── pod_reservoir_mask.imageset │ │ │ ├── Contents.json │ │ │ └── pod_reservoir_mask.pdf │ ├── InsulinStatusTableViewCell.swift │ ├── OmnipodReservoirView.swift │ ├── OmnipodReservoirView.xib │ └── PodExpirationTableViewCell.swift ├── View Controllers │ └── DashUICoordinator.swift ├── View Models │ ├── DashSettingsViewModel.swift │ ├── DeactivatePodViewModel.swift │ ├── DeliveryUncertaintyRecoveryViewModel.swift │ ├── InsertCannulaViewModel.swift │ ├── MockPodSettingsViewModel.swift │ ├── PairPodViewModel.swift │ ├── PodLifeState.swift │ └── RegistrationViewModel.swift └── Views │ ├── AttachPodView.swift │ ├── BasalStateView.swift │ ├── CheckInsertedCannulaView.swift │ ├── DashSettingsView.swift │ ├── DeactivatePodView.swift │ ├── DeliveryUncertaintyRecoveryView.swift │ ├── DesignElements │ ├── ErrorView.swift │ ├── LeadingImage.swift │ └── RoundedCard.swift │ ├── ExpirationReminderPickerView.swift │ ├── ExpirationReminderSetupView.swift │ ├── InsertCannulaView.swift │ ├── LowReservoirReminderEditView.swift │ ├── LowReservoirReminderSetupView.swift │ ├── MockPodSettingsView.swift │ ├── NotificationSettingsView.swift │ ├── PairPodView.swift │ ├── PodDetailsView.swift │ ├── PodSetupView.swift │ ├── RegisterView.swift │ ├── ScheduledExpirationReminderEditView.swift │ ├── SetupCompleteView.swift │ ├── SetupGuideView.swift │ ├── TimeView.swift │ └── UncertaintyRecoveredView.swift ├── DashKitUITests ├── DashKitUITests.swift ├── DashSettingsViewModelTests.swift ├── DeactivatePodViewModelTests.swift ├── Info.plist ├── InsertCannulaViewModelTests.swift └── PairPodViewModelTests.swift ├── LICENSE ├── MockPodPlugin ├── Info.plist ├── MockPodPlugin.swift └── MockPodPumpManager.swift ├── PodUIDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist ├── SceneDelegate.swift └── ViewController.swift ├── README.md └── ReservoirLevelHighlightState.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | build/ 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | *.xccheckout 14 | *.moved-aside 15 | DerivedData 16 | *.hmap 17 | *.ipa 18 | *.xcuserstate 19 | *.xcscmblueprint 20 | project.xcworkspace 21 | .DS_Store 22 | *.log 23 | project.xcworkspace 24 | 25 | # Carthage 26 | Carthage/ 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "dash-sdk-ios"] 2 | path = dash-sdk-ios 3 | url = git@github.com:tidepool-org/dash-sdk-ios.git 4 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "tidepool-org/LoopKit" "dev" 2 | github "maxkonovalov/MKRingProgressView" "f548a5c6" 3 | github "ReactiveX/RxSwift" ~> 5.0 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "ReactiveX/RxSwift" "5.0.1" 2 | github "maxkonovalov/MKRingProgressView" "f548a5c64832be2d37d7c91b5800e284887a2a0a" 3 | github "tidepool-org/LoopKit" "d63248b9c453b2624fa4c94a7fd641e5164c2929" 4 | -------------------------------------------------------------------------------- /Common/FrameworkLocalText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FrameworkLocalText.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 7/21/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | private class FrameworkReferenceClass { 13 | static let bundle = Bundle(for: FrameworkReferenceClass.self) 14 | } 15 | 16 | func FrameworkLocalText(_ key: LocalizedStringKey, comment: StaticString) -> Text { 17 | return Text(key, bundle: FrameworkReferenceClass.bundle, comment: comment) 18 | } 19 | -------------------------------------------------------------------------------- /Common/IdentifiableClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IdentifiableClass.swift 3 | // DashKit 4 | // 5 | // From Naterade 6 | // Created by Nathan Racklyeft on 2/9/16. 7 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | 12 | 13 | protocol IdentifiableClass: AnyObject { 14 | static var className: String { get } 15 | } 16 | 17 | 18 | extension IdentifiableClass { 19 | static var className: String { 20 | return NSStringFromClass(self).components(separatedBy: ".").last! 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Common/LocalizedString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizedString.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 4/18/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | private class FrameworkBundle { 12 | static let main = Bundle(for: FrameworkBundle.self) 13 | } 14 | 15 | func LocalizedString(_ key: String, tableName: String? = nil, value: String? = nil, comment: String) -> String { 16 | if let value = value { 17 | return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, value: value, comment: comment) 18 | } else { 19 | return NSLocalizedString(key, tableName: tableName, bundle: FrameworkBundle.main, comment: comment) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Common/NibLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NibLoadable.swift 3 | // 4 | // Created by Nate Racklyeft on 7/2/16. 5 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | 11 | protocol NibLoadable: IdentifiableClass { 12 | static func nib() -> UINib 13 | } 14 | 15 | 16 | extension NibLoadable { 17 | static func nib() -> UINib { 18 | return UINib(nibName: className, bundle: Bundle(for: self)) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Common/NumberFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatter.swift 3 | // DashKit 4 | // 5 | // Copyright © 2017 Pete Schwamb. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | extension NumberFormatter { 11 | func string(from number: Double) -> String? { 12 | return string(from: NSNumber(value: number)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Common/OSLog.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OSLog.swift 3 | // DashKit 4 | // 5 | // Copyright © 2017 LoopKit Authors. All rights reserved. 6 | // 7 | 8 | import os.log 9 | 10 | 11 | extension OSLog { 12 | convenience init(category: String) { 13 | self.init(subsystem: "org.tidepool.DashKit", category: category) 14 | } 15 | 16 | func debug(_ message: StaticString, _ args: CVarArg...) { 17 | log(message, type: .debug, args) 18 | } 19 | 20 | func info(_ message: StaticString, _ args: CVarArg...) { 21 | log(message, type: .info, args) 22 | } 23 | 24 | func `default`(_ message: StaticString, _ args: CVarArg...) { 25 | log(message, type: .default, args) 26 | } 27 | 28 | func error(_ message: StaticString, _ args: CVarArg...) { 29 | log(message, type: .error, args) 30 | } 31 | 32 | private func log(_ message: StaticString, type: OSLogType, _ args: [CVarArg]) { 33 | switch args.count { 34 | case 0: 35 | os_log(message, log: self, type: type) 36 | case 1: 37 | os_log(message, log: self, type: type, args[0]) 38 | case 2: 39 | os_log(message, log: self, type: type, args[0], args[1]) 40 | case 3: 41 | os_log(message, log: self, type: type, args[0], args[1], args[2]) 42 | case 4: 43 | os_log(message, log: self, type: type, args[0], args[1], args[2], args[3]) 44 | case 5: 45 | os_log(message, log: self, type: type, args[0], args[1], args[2], args[3], args[4]) 46 | default: 47 | os_log(message, log: self, type: type, args) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Common/TimeInterval.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSTimeInterval.swift 3 | // DashKit 4 | // 5 | // Originally from Naterade 6 | // Created by Nathan Racklyeft on 1/9/16. 7 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | 12 | 13 | extension TimeInterval { 14 | 15 | static func days(_ days: Double) -> TimeInterval { 16 | return self.init(days: days) 17 | } 18 | 19 | static func hours(_ hours: Double) -> TimeInterval { 20 | return self.init(hours: hours) 21 | } 22 | 23 | static func minutes(_ minutes: Int) -> TimeInterval { 24 | return self.init(minutes: Double(minutes)) 25 | } 26 | 27 | static func minutes(_ minutes: Double) -> TimeInterval { 28 | return self.init(minutes: minutes) 29 | } 30 | 31 | static func seconds(_ seconds: Double) -> TimeInterval { 32 | return self.init(seconds) 33 | } 34 | 35 | static func milliseconds(_ milliseconds: Double) -> TimeInterval { 36 | return self.init(milliseconds / 1000) 37 | } 38 | 39 | init(days: Double) { 40 | self.init(hours: days * 24) 41 | } 42 | 43 | init(hours: Double) { 44 | self.init(minutes: hours * 60) 45 | } 46 | 47 | init(minutes: Double) { 48 | self.init(minutes * 60) 49 | } 50 | 51 | init(seconds: Double) { 52 | self.init(seconds) 53 | } 54 | 55 | init(milliseconds: Double) { 56 | self.init(milliseconds / 1000) 57 | } 58 | 59 | var milliseconds: Double { 60 | return self * 1000 61 | } 62 | 63 | init(hundredthsOfMilliseconds: Double) { 64 | self.init(hundredthsOfMilliseconds / 100000) 65 | } 66 | 67 | var hundredthsOfMilliseconds: Double { 68 | return self * 100000 69 | } 70 | 71 | var minutes: Double { 72 | return self / 60.0 73 | } 74 | 75 | var hours: Double { 76 | return minutes / 60.0 77 | } 78 | 79 | var days: Double { 80 | return hours / 24.0 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Common/TimeZone.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeZone.swift 3 | // DashKit 4 | // 5 | // Originally from RileyLink 6 | // Created by Nate Racklyeft on 10/2/16. 7 | // Copyright © 2019 Tidepool. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | 12 | extension TimeZone { 13 | static var currentFixed: TimeZone { 14 | return TimeZone(secondsFromGMT: TimeZone.current.secondsFromGMT())! 15 | } 16 | 17 | var fixed: TimeZone { 18 | return TimeZone(secondsFromGMT: secondsFromGMT())! 19 | } 20 | 21 | /// This only works for fixed utc offset timezones 22 | func scheduleOffset(forDate date: Date) -> TimeInterval { 23 | var calendar = Calendar.current 24 | calendar.timeZone = self 25 | let components = calendar.dateComponents([.day , .month, .year], from: date) 26 | guard let startOfSchedule = calendar.date(from: components) else { 27 | fatalError("invalid date") 28 | } 29 | return date.timeIntervalSince(startOfSchedule) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DashKit.xcodeproj/xcshareddata/xcschemes/Shared.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 65 | 66 | 67 | 68 | 70 | 76 | 77 | 78 | 79 | 80 | 90 | 91 | 97 | 98 | 99 | 100 | 106 | 107 | 113 | 114 | 115 | 116 | 118 | 119 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /DashKit/BasalSchedule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasalSchedule.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 5/9/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | import LoopKit 12 | 13 | extension BasalProgram { 14 | 15 | public init?(items: [RepeatingScheduleValue]) { 16 | var basalSegments = [BasalSegment]() 17 | 18 | let rates = items.map { $0.value } 19 | let startTimes = items.map { $0.startTime } 20 | var endTimes = startTimes.suffix(from: 1) 21 | endTimes.append(.hours(24)) 22 | 23 | for (rate, (start, end)) in zip(rates, zip(startTimes, endTimes)) { 24 | let podRate = Int(round(rate * Pod.podSDKInsulinMultiplier)) 25 | 26 | do { 27 | let segment = try BasalSegment(startTime: BasalProgram.indexFor(start), endTime: BasalProgram.indexFor(end), basalRate: podRate) 28 | basalSegments.append(segment) 29 | } catch { 30 | return nil 31 | } 32 | } 33 | 34 | do { 35 | try self.init(basalSegments: basalSegments) 36 | } catch { 37 | return nil 38 | } 39 | } 40 | 41 | private static func indexFor(_ interval: TimeInterval) -> Int { 42 | return Int(floor(interval/Pod.minimumBasalScheduleEntryDuration)) 43 | } 44 | 45 | // Only valid for fixed offset timezones 46 | public func currentRate(using calendar: Calendar, at date: Date = Date()) -> BasalSegment { 47 | let midnight = calendar.startOfDay(for: date) 48 | let index = BasalProgram.indexFor(date.timeIntervalSince(midnight)) 49 | return basalSegments.first { index >= $0.startTime && index < $0.endTime }! 50 | } 51 | } 52 | 53 | extension BasalSegment: RawRepresentable { 54 | public typealias RawValue = [String: Any] 55 | 56 | public init?(rawValue: RawValue) { 57 | guard 58 | let basalRate = rawValue["basalRate"] as? Int, 59 | let startTime = rawValue["startTime"] as? Int, 60 | let endTime = rawValue["endTime"] as? Int 61 | else { 62 | return nil 63 | } 64 | do { 65 | try self.init(startTime: startTime, endTime: endTime, basalRate: basalRate) 66 | } catch { 67 | return nil 68 | } 69 | } 70 | 71 | public var rawValue: RawValue { 72 | return [ 73 | "basalRate": basalRate, 74 | "startTime": startTime, 75 | "endTime": endTime, 76 | ] 77 | } 78 | 79 | public var basalRateUnitsPerHour: Double { 80 | return Double(basalRate) / Pod.podSDKInsulinMultiplier 81 | } 82 | } 83 | 84 | extension BasalProgram: RawRepresentable { 85 | public typealias RawValue = [String: Any] 86 | 87 | public init?(rawValue: RawValue) { 88 | guard let basalSegmentsRaw = rawValue["basalSegments"] as? [BasalSegment.RawValue] else { 89 | return nil 90 | } 91 | 92 | do { 93 | try self.init(basalSegments: basalSegmentsRaw.compactMap { BasalSegment(rawValue: $0) }) 94 | } catch { 95 | return nil 96 | } 97 | } 98 | 99 | public var rawValue: RawValue { 100 | return [ 101 | "basalSegments": basalSegments.map { $0.rawValue } 102 | ] 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /DashKit/DashKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // DashKit.h 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 4/18/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DashKit. 12 | FOUNDATION_EXPORT double DashKitVersionNumber; 13 | 14 | //! Project version string for DashKit. 15 | FOUNDATION_EXPORT const unsigned char DashKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /DashKit/DashPumpManagerError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashPumpManagerError.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 8/26/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | public enum DashPumpManagerError: Error, LocalizedError { 13 | case missingSettings 14 | case invalidBasalSchedule 15 | case invalidBolusVolume 16 | case invalidTempBasalRate 17 | case podCommError(PodCommError) 18 | case busy 19 | case acknowledgingAlertFailed 20 | 21 | /// A localized message describing what error occurred. 22 | public var errorDescription: String? { 23 | switch self { 24 | case .missingSettings: 25 | return LocalizedString("Missing settings.", comment: "Description of DashPumpManagerError for .missingSettings.") 26 | case .invalidBasalSchedule: 27 | return LocalizedString("Invalid basal schedule.", comment: "Description of DashPumpManagerError for .invalidBasalSchedule") 28 | case .invalidBolusVolume: 29 | return LocalizedString("Invalid bolus volume.", comment: "Description of DashPumpManagerError for .invalidBolusVolume") 30 | case .invalidTempBasalRate: 31 | return LocalizedString("Invalid temp basal rate.", comment: "Description of DashPumpManagerError for .invalidTempBasalRate") 32 | case .podCommError(let error): 33 | return error.errorDescription 34 | case .busy: 35 | return LocalizedString("Pod Busy.", comment: "Description of DashPumpManagerError error when pump manager is busy.") 36 | case .acknowledgingAlertFailed: 37 | return LocalizedString("Tidepool Loop was unable to clear the alert on your Pod, therefore you may continue to hear an audible beep.", comment: "Description of DashPumpManagerError error when acknowledging alert failed.") 38 | } 39 | } 40 | 41 | public var recoverySuggestion: String? { 42 | switch self { 43 | case .acknowledgingAlertFailed: 44 | return LocalizedString("You can try moving your device closer to the Pod. The app will continue trying to reach your Pod to clear the alert.", comment: "Recovery suggestion of DashPumpManagerError error when acknowledging alert failed.") 45 | default: 46 | return nil 47 | } 48 | } 49 | } 50 | 51 | extension DashPumpManagerError { 52 | init(_ podCommError: PodCommError) { 53 | self = .podCommError(podCommError) 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /DashKit/Extensions/AlarmCode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlarmCode.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 11/5/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | extension AlarmCode { 13 | public var notificationTitle: String { 14 | switch self { 15 | case .autoOff: 16 | return LocalizedString("Auto Off Alarm", comment: "The title for Auto-Off alarm notification") 17 | case .emptyReservoir: 18 | return LocalizedString("Empty Reservoir", comment: "The title for Empty Reservoir alarm notification") 19 | case .occlusion: 20 | return LocalizedString("Occlusion Detected", comment: "The title for Occlusion alarm notification") 21 | case .other: 22 | return LocalizedString("Pod Error", comment: "The title for AlarmCode.other notification") 23 | case .podExpired: 24 | return LocalizedString("Pod Expired", comment: "The title for Pod Expired alarm notification") 25 | } 26 | } 27 | 28 | public var notificationBody: String { 29 | return LocalizedString("Insulin delivery stopped. Change Pod now.", comment: "The default notification body for AlarmCodes") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DashKit/Extensions/Array.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Array.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 1/7/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | func makeInfiniteLoopIterator() -> AnyIterator { 13 | var index = self.startIndex 14 | 15 | return AnyIterator({ 16 | if self.isEmpty { 17 | return nil 18 | } 19 | 20 | let result = self[index] 21 | 22 | index = self.index(after: index) 23 | if index == self.endIndex { 24 | index = self.startIndex 25 | } 26 | 27 | return result 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DashKit/Extensions/ConnectionState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionState.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 8/29/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | extension ConnectionState { 13 | public var localizedDescription: String { 14 | switch self { 15 | case .connected: 16 | return LocalizedString("Connected", comment: "Description for pod connected state.") 17 | case .disconnected: 18 | return LocalizedString("Disconnected", comment: "Description for pod disconnected state.") 19 | case .tryConnecting: 20 | return LocalizedString("Connecting...", comment: "Description for pod connecting state.") 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DashKit/Extensions/PodAlerts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodAlerts.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 7/20/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | extension PodAlerts: CustomDebugStringConvertible { 13 | public var debugDescription: String { 14 | let allAlerts: [PodAlerts] = [.autoOff, .lowReservoir, .multiCommand, .podExpireImminent, .podExpiring, .suspendEnded, .suspendInProgress, .userPodExpiration] 15 | var alertDescriptions: [String] = [] 16 | for alert in allAlerts { 17 | if self.contains(alert) { 18 | var alertDescription = { () -> String in 19 | switch alert { 20 | case .autoOff: 21 | return "Auto-Off" 22 | case .lowReservoir: 23 | return "Low Reservoir" 24 | case .multiCommand: 25 | return "Multi-Command" 26 | case .podExpireImminent: 27 | return "Pod Expire Imminent" 28 | case .podExpiring: 29 | return "Pod Expiring" 30 | case .suspendEnded: 31 | return "Suspend Ended" 32 | case .suspendInProgress: 33 | return "Suspend In Progress" 34 | case .userPodExpiration: 35 | return "User Pod Expiration" 36 | default: 37 | fatalError() 38 | } 39 | }() 40 | if let alertTime = getAlertsTime(podAlert: alert) { 41 | alertDescription += ": \(alertTime)" 42 | } 43 | alertDescriptions.append(alertDescription) 44 | } 45 | } 46 | return "PodAlerts(\(alertDescriptions.joined(separator: ", ")))" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DashKit/Extensions/PodCommError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodCommError.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 3/2/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import PodSDK 11 | 12 | public extension PodCommError { 13 | var recoverable: Bool { 14 | switch self { 15 | case .podIsInAlarm: 16 | return false 17 | case .activationError(let activationErrorCode): 18 | switch activationErrorCode { 19 | case .podIsLumpOfCoal1Hour, .podIsLumpOfCoal2Hours: 20 | return false 21 | default: 22 | return true 23 | } 24 | case .internalError(.incompatibleProductId): 25 | return false 26 | case .systemError: 27 | return false 28 | default: 29 | return true 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DashKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /DashKit/Mocks/MockPodAlarm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodAlarm.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 3/31/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | public struct MockPodAlarm: PodAlarmDetail { 13 | public var alarmCode: AlarmCode 14 | 15 | public var alarmDescription: String 16 | 17 | public var podStatus: PartialPodStatus 18 | 19 | public var occlusionType: OcclusionType 20 | 21 | public var didErrorOccuredFetchingBolusInfo: Bool 22 | 23 | public var wasBolusActiveWhenPodAlarmed: Bool 24 | 25 | public var podStateWhenPodAlarmed: PodState 26 | 27 | public var alarmTime: Date? 28 | 29 | public var activationTime: Date 30 | 31 | public var referenceCode: String 32 | 33 | public init( 34 | alarmCode: AlarmCode = .podExpired, 35 | alarmDescription: String = "Pod Expired", 36 | podStatus: PodStatus = MockPodStatus.normal, 37 | occlusionType: OcclusionType = .none, 38 | didErrorOccuredFetchingBolusInfo: Bool = false, 39 | wasBolusActiveWhenPodAlarmed: Bool = false, 40 | podStateWhenPodAlarmed: PodState = .basalProgramRunning, 41 | alarmTime: Date? = Date(), 42 | activationTime: Date = Date() - 10 * 60 * 60, 43 | referenceCode: String = "123" 44 | ) { 45 | self.alarmCode = alarmCode 46 | self.alarmDescription = alarmDescription 47 | self.podStatus = podStatus 48 | self.occlusionType = occlusionType 49 | self.didErrorOccuredFetchingBolusInfo = didErrorOccuredFetchingBolusInfo 50 | self.wasBolusActiveWhenPodAlarmed = wasBolusActiveWhenPodAlarmed 51 | self.podStateWhenPodAlarmed = podStateWhenPodAlarmed 52 | self.alarmTime = alarmTime 53 | self.activationTime = activationTime 54 | self.referenceCode = referenceCode 55 | } 56 | 57 | public static var occlusion: MockPodAlarm { 58 | return MockPodAlarm( 59 | alarmCode: .occlusion, 60 | alarmDescription: "Occlusion", 61 | podStatus: MockPodStatus.normal, 62 | occlusionType: .stallDuringStartupWire1TimingOut, 63 | didErrorOccuredFetchingBolusInfo: false, 64 | wasBolusActiveWhenPodAlarmed: false, 65 | podStateWhenPodAlarmed: .runningAboveMinVolume, 66 | alarmTime: Date().addingTimeInterval(.minutes(10)), 67 | activationTime: Date().addingTimeInterval(.hours(24)), 68 | referenceCode: "123") 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /DashKit/Mocks/MockPodVersion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodVersion.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 5/7/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct MockPodVersion: PodVersionProtocol { 12 | public var lotNumber: Int 13 | public var sequenceNumber: Int 14 | public var majorVersion: Int 15 | public var minorVersion: Int 16 | public var interimVersion: Int 17 | public var bleMajorVersion: Int 18 | public var bleMinorVersion: Int 19 | public var bleInterimVersion: Int 20 | 21 | public init( 22 | lotNumber: Int, 23 | sequenceNumber: Int, 24 | majorVersion: Int, 25 | minorVersion: Int, 26 | interimVersion: Int, 27 | bleMajorVersion: Int, 28 | bleMinorVersion: Int, 29 | bleInterimVersion: Int 30 | ) { 31 | self.lotNumber = lotNumber 32 | self.sequenceNumber = sequenceNumber 33 | self.majorVersion = majorVersion 34 | self.minorVersion = minorVersion 35 | self.interimVersion = interimVersion 36 | self.bleMajorVersion = bleMajorVersion 37 | self.bleMinorVersion = bleMinorVersion 38 | self.bleInterimVersion = bleInterimVersion 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /DashKit/PendingCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PendingCommand.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 8/19/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | import LoopKit 12 | 13 | extension ProgramType: Equatable { 14 | public static func == (lhs: ProgramType, rhs: ProgramType) -> Bool { 15 | switch (lhs, rhs) { 16 | case (.basalProgram(let lhsBasal, let lhsSecondsSinceMidnight), .basalProgram(let rhsBasal, let rhsSecondsSinceMidnight)): 17 | return lhsBasal == rhsBasal && lhsSecondsSinceMidnight == rhsSecondsSinceMidnight 18 | case (.bolus(let lhsBolus), .bolus(let rhsBolus)): 19 | return lhsBolus == rhsBolus 20 | case (.tempBasal(let lhsTempBasal), .tempBasal(let rhsTempBasal)): 21 | return lhsTempBasal == rhsTempBasal 22 | default: 23 | return false 24 | } 25 | } 26 | } 27 | 28 | public enum PendingCommand: RawRepresentable, Equatable { 29 | public typealias RawValue = [String: Any] 30 | 31 | case program(ProgramType, Date) 32 | case stopProgram(StopProgramType, Date) 33 | 34 | private enum PendingCommandType: Int { 35 | case program, stopProgram 36 | } 37 | 38 | public var commandDate: Date { 39 | switch self { 40 | case .program(_, let date): 41 | return date 42 | case .stopProgram(_, let date): 43 | return date 44 | } 45 | } 46 | 47 | public init?(rawValue: RawValue) { 48 | guard let rawPendingCommandType = rawValue["type"] as? PendingCommandType.RawValue else { 49 | return nil 50 | } 51 | 52 | guard let commandDate = rawValue["date"] as? Date else { 53 | return nil 54 | } 55 | 56 | switch PendingCommandType(rawValue: rawPendingCommandType) { 57 | case .program?: 58 | guard let rawUnacknowledgedProgram = rawValue["unacknowledgedProgram"] as? JSONEncoder.Output else { 59 | return nil 60 | } 61 | let decoder = JSONDecoder() 62 | if let program = try? decoder.decode(ProgramType.self, from: rawUnacknowledgedProgram) { 63 | self = .program(program, commandDate) 64 | } else { 65 | return nil 66 | } 67 | case .stopProgram?: 68 | guard let rawUnacknowledgedStopProgram = rawValue["unacknowledgedStopProgram"] as? JSONEncoder.Output else { 69 | return nil 70 | } 71 | let decoder = JSONDecoder() 72 | if let stopProgram = try? decoder.decode(StopProgramType.self, from: rawUnacknowledgedStopProgram) { 73 | self = .stopProgram(stopProgram, commandDate) 74 | } else { 75 | return nil 76 | } 77 | default: 78 | return nil 79 | } 80 | } 81 | 82 | public var rawValue: RawValue { 83 | var rawValue: RawValue = [:] 84 | 85 | switch self { 86 | case .program(let program, let date): 87 | rawValue["type"] = PendingCommandType.program.rawValue 88 | rawValue["date"] = date 89 | let encoder = JSONEncoder() 90 | if let rawUnacknowledgedProgram = try? encoder.encode(program) { 91 | rawValue["unacknowledgedProgram"] = rawUnacknowledgedProgram 92 | } 93 | case .stopProgram(let stopProgram, let date): 94 | rawValue["type"] = PendingCommandType.stopProgram.rawValue 95 | rawValue["date"] = date 96 | let encoder = JSONEncoder() 97 | if let rawUnacknowledgedStopProgram = try? encoder.encode(stopProgram) { 98 | rawValue["unacknowledgedStopProgram"] = rawUnacknowledgedStopProgram 99 | } 100 | } 101 | return rawValue 102 | } 103 | 104 | public static func == (lhs: PendingCommand, rhs: PendingCommand) -> Bool { 105 | switch(lhs, rhs) { 106 | case (.program(let lhsProgram, let lhsDate), .program(let rhsProgram, let rhsDate)): 107 | return lhsProgram == rhsProgram && lhsDate == rhsDate 108 | case (.stopProgram(let lhsStopProgram, let lhsDate), .stopProgram(let rhsStopProgram, let rhsDate)): 109 | return lhsStopProgram == rhsStopProgram && lhsDate == rhsDate 110 | default: 111 | return false 112 | } 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /DashKit/Pod.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pod.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 4/18/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Pod { 12 | // Volume of insulin in one motor pulse 13 | public static let pulseSize: Double = 0.05 14 | 15 | // Number of pulses required to delivery one unit of insulin 16 | public static let pulsesPerUnit: Double = 1/pulseSize 17 | 18 | // Units per second 19 | public static let bolusDeliveryRate: Double = 0.025 20 | 21 | // Maximum reservoir level reading 22 | public static let maximumReservoirReading: Double = 50 23 | 24 | // Reservoir Capacity 25 | public static let reservoirCapacity: Double = 200 26 | 27 | // Supported basal rates 28 | public static let supportedBasalRates: [Double] = (1...600).map { Double($0) / Double(pulsesPerUnit) } 29 | 30 | // Maximum number of basal schedule entries supported 31 | public static let maximumBasalScheduleEntryCount: Int = 24 32 | 33 | // Minimum duration of a single basal schedule entry 34 | public static let minimumBasalScheduleEntryDuration = TimeInterval.minutes(30) 35 | 36 | // Time from pod activation until expiration 37 | public static let lifetime = TimeInterval(hours: 72) 38 | 39 | // Time from pod activation until podExpireImminent alert 40 | public static let expirationImminentInterval = TimeInterval(hours: 79) 41 | 42 | // Time from expiration until pod fault 43 | public static let expirationWindow = TimeInterval(hours: 8) 44 | 45 | // PodSDK insulin values are U * 100 46 | public static let podSDKInsulinMultiplier: Double = 100 47 | 48 | // Estimated time for priming to complete; SDK will send back an event when priming completes, 49 | // But this lets us provide an estimate to the user. 50 | public static let estimatedPrimingDuration = TimeInterval(35) 51 | 52 | // Estimated time for cannula insertion; SDK will send back an event that actually marks the end, 53 | // but this lets us provide an estimate to the user 54 | public static let estimatedCannulaInsertionDuration = TimeInterval(10) 55 | 56 | // Default low reservoir alert limit in Units 57 | public static let defaultLowReservoirReminder: Double = 10 58 | 59 | // Default expiration reminder offset 60 | public static let defaultExpirationReminderOffset = TimeInterval(hours: 4) 61 | 62 | // Support phone number (TODO: get from SDK?) 63 | public static let supportPhoneNumber: String = "1-800-591-3455" 64 | 65 | // Threshold used to display pod end of life warnings 66 | public static let timeRemainingWarningThreshold = TimeInterval(days: 1) 67 | 68 | // Allowed Low Reservoir reminder values 69 | public static let allowedLowReservoirReminderValues = Array(stride(from: 10, through: 50, by: 1)) 70 | } 71 | -------------------------------------------------------------------------------- /DashKit/PodDoseProgressTimerEstimator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodDoseProgressTimerEstimator.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 5/31/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoopKit 11 | 12 | class PodDoseProgressTimerEstimator: DoseProgressTimerEstimator { 13 | 14 | let dose: DoseEntry 15 | 16 | weak var pumpManager: PumpManager? 17 | 18 | override var progress: DoseProgress { 19 | let elapsed = -dose.startDate.timeIntervalSinceNow 20 | let duration = dose.endDate.timeIntervalSince(dose.startDate) 21 | let percentComplete = min(elapsed / duration, 1) 22 | let delivered = pumpManager?.roundToSupportedBolusVolume(units: percentComplete * dose.programmedUnits) ?? percentComplete * dose.programmedUnits 23 | return DoseProgress(deliveredUnits: delivered, percentComplete: percentComplete) 24 | } 25 | 26 | init(dose: DoseEntry, pumpManager: PumpManager, reportingQueue: DispatchQueue) { 27 | self.dose = dose 28 | self.pumpManager = pumpManager 29 | super.init(reportingQueue: reportingQueue) 30 | } 31 | 32 | override func timerParameters() -> (delay: TimeInterval, repeating: TimeInterval) { 33 | let timeSinceStart = dose.startDate.timeIntervalSinceNow 34 | let timeBetweenPulses: TimeInterval 35 | switch dose.type { 36 | case .bolus: 37 | timeBetweenPulses = Pod.pulseSize / Pod.bolusDeliveryRate 38 | case .basal, .tempBasal: 39 | timeBetweenPulses = Pod.pulseSize / (dose.unitsPerHour / TimeInterval(hours: 1)) 40 | default: 41 | fatalError("Can only estimate progress on basal rates or boluses.") 42 | } 43 | let delayUntilNextPulse = timeBetweenPulses - timeSinceStart.remainder(dividingBy: timeBetweenPulses) 44 | 45 | return (delay: delayUntilNextPulse, repeating: timeBetweenPulses) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /DashKit/PodSDKProtocol/CannulaInserter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CannulaInserter.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 3/10/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | public protocol CannulaInserter { 13 | func insertCannula(eventListener: @escaping (ActivationStatus) -> ()) 14 | } 15 | 16 | extension DashPumpManager: CannulaInserter { 17 | public func insertCannula(eventListener: @escaping (ActivationStatus) -> ()) { 18 | let autoOffAlert = try! AutoOffAlert(enable: false) 19 | finishPodActivation(autoOffAlert: autoOffAlert, eventListener: eventListener) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DashKit/PodSDKProtocol/PDMRegistrator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PDMRegistrator.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 2/11/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | public protocol PDMRegistrator { 13 | 14 | /** 15 | Starts a registration process without SMS validation. 16 | 17 | - parameters: 18 | - completion: A closure to be called when a `PodCommEvent` is issued by the comm. layer. 19 | - status: Registration status. 20 | 21 | - Note: Only use this API if your app does not require SMS validation (as per the Insulet Cloud configuration set for your team). 22 | */ 23 | func startRegistration(completion: @escaping (PodSDK.RegistrationStatus) -> ()) 24 | 25 | /// Registration is complete. 26 | func isRegistered() -> Bool 27 | 28 | } 29 | 30 | extension RegistrationManager: PDMRegistrator { } 31 | -------------------------------------------------------------------------------- /DashKit/PodSDKProtocol/PodCommManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodCommManager.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 6/26/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | extension PodCommManager: PodCommManagerProtocol { 13 | public var podVersionAbstracted: PodVersionProtocol? { 14 | return self.podVersion 15 | } 16 | } 17 | 18 | extension PodVersion: PodVersionProtocol {} 19 | -------------------------------------------------------------------------------- /DashKit/PodSDKProtocol/PodDeactivater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodDeactivater.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 3/10/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | 12 | public protocol PodDeactivater { 13 | func deactivatePod(completion: @escaping (PodCommResult) -> ()) 14 | func discardPod(completion: @escaping (PodCommResult) -> ()) 15 | } 16 | 17 | extension DashPumpManager: PodDeactivater { } 18 | -------------------------------------------------------------------------------- /DashKit/PodSDKProtocol/PodPairer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodPairer.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/5/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | import PodSDK 12 | 13 | public protocol PodPairer { 14 | func pair(eventListener: @escaping (ActivationStatus) -> ()) 15 | func discardPod(completion: @escaping (PodCommResult) -> ()) 16 | 17 | var podCommState: PodCommState { get } 18 | } 19 | 20 | extension DashPumpManager: PodPairer { 21 | public func pair(eventListener: @escaping (ActivationStatus) -> ()) { 22 | guard let podExpirationAlert = try? PodExpirationAlert(intervalBeforeExpiration: state.defaultExpirationReminderOffset) else { 23 | eventListener(.error(.invalidAlertSetting)) 24 | return 25 | } 26 | 27 | guard let lowReservoirAlert = try? LowReservoirAlert(reservoirVolumeBelow: Int(Double(state.lowReservoirReminderValue) * Pod.podSDKInsulinMultiplier)) else { 28 | eventListener(.error(.invalidAlertSetting)) 29 | return 30 | } 31 | 32 | startPodActivation( 33 | lowReservoirAlert: lowReservoirAlert, 34 | podExpirationAlert: podExpirationAlert, 35 | eventListener: eventListener) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DashKit/ReservoirLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReservoirLevel.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 5/31/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | 13 | public enum ReservoirLevel: RawRepresentable, Equatable { 14 | public typealias RawValue = Int 15 | 16 | public static let aboveThresholdMagicNumber: Int = 5115 17 | 18 | case valid(Double) 19 | case aboveThreshold 20 | 21 | public var percentage: Double { 22 | switch self { 23 | case .aboveThreshold: 24 | return 1 25 | case .valid(let value): 26 | // Set 50U as the halfway mark, even though pods can hold 200U. 27 | return min(1, max(0, value / 100)) 28 | } 29 | } 30 | 31 | public init(rawValue: RawValue) { 32 | switch rawValue { 33 | case Self.aboveThresholdMagicNumber: 34 | self = .aboveThreshold 35 | default: 36 | self = .valid(Double(rawValue) / Pod.podSDKInsulinMultiplier) 37 | } 38 | } 39 | 40 | public var rawValue: RawValue { 41 | switch self { 42 | case .valid(let value): 43 | return Int(round(value * Pod.podSDKInsulinMultiplier)) 44 | case .aboveThreshold: 45 | return Self.aboveThresholdMagicNumber 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DashKitPlugin/DashKitPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashKitPlugin.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 7/23/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import LoopKitUI 11 | import DashKit 12 | import DashKitUI 13 | 14 | class DashKitPlugin: NSObject, PumpManagerUIPlugin { 15 | private let log = OSLog(category: "DashKitPlugin") 16 | 17 | public var pumpManagerType: PumpManagerUI.Type? { 18 | return DashPumpManager.self 19 | } 20 | 21 | override init() { 22 | super.init() 23 | log.default("Instantiated") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DashKitPlugin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.loopkit.Loop.PumpManagerDisplayName 6 | Omnipod 7 | com.loopkit.Loop.PumpManagerIdentifier 8 | Omnipod 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | FMWK 21 | CFBundleShortVersionString 22 | 1.0 23 | CFBundleVersion 24 | 1 25 | NSHumanReadableCopyright 26 | Copyright © 2019 Tidepool. All rights reserved. 27 | NSPrincipalClass 28 | DashKitPlugin 29 | 30 | 31 | -------------------------------------------------------------------------------- /DashKitTests/BasalProgramTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasalProgramTests.swift 3 | // DashKitTests 4 | // 5 | // Created by Pete Schwamb on 5/14/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import LoopKit 11 | @testable import DashKit 12 | import PodSDK 13 | 14 | class BasalProgramTests: XCTestCase { 15 | 16 | func testCurrentRate() { 17 | 18 | let program = BasalProgram(items: [RepeatingScheduleValue(startTime: 0, value: 10.0), RepeatingScheduleValue(startTime: .hours(12), value: 15.0)])! 19 | 20 | var calendar = Calendar(identifier: .gregorian) 21 | calendar.timeZone = .currentFixed 22 | let midnight = calendar.startOfDay(for: Date()) 23 | 24 | XCTAssertEqual(10, program.currentRate(using: calendar, at: midnight.addingTimeInterval(.hours(0))).basalRateUnitsPerHour) 25 | XCTAssertEqual(10, program.currentRate(using: calendar, at: midnight.addingTimeInterval(.hours(0.25))).basalRateUnitsPerHour) 26 | XCTAssertEqual(15, program.currentRate(using: calendar, at: midnight.addingTimeInterval(.hours(12))).basalRateUnitsPerHour) 27 | XCTAssertEqual(15, program.currentRate(using: calendar, at: midnight.addingTimeInterval(.hours(23.75))).basalRateUnitsPerHour) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DashKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Cannula Inserted.imageset/CannulaInserted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Cannula Inserted.imageset/CannulaInserted.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Cannula Inserted.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "CannulaInserted.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Fill Pod.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties" : { 3 | "compression-type" : "lossless" 4 | }, 5 | "info" : { 6 | "version" : 1, 7 | "author" : "xcode" 8 | }, 9 | "data" : [ 10 | { 11 | "idiom" : "universal", 12 | "filename" : "fillPod.gif" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Fill Pod.dataset/fillPod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Fill Pod.dataset/fillPod.gif -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/No Pod.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NoPod-1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "NoPod.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "NoPod-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/No Pod.imageset/NoPod-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/No Pod.imageset/NoPod-1.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/No Pod.imageset/NoPod-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/No Pod.imageset/NoPod-2.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/No Pod.imageset/NoPod.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/No Pod.imageset/NoPod.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Onboarding.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Onboarding.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "Onboarding@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "Onboarding@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding@2x.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Onboarding.imageset/Onboarding@3x.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod Finish Activation.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Pod-FinishActivation.pdf", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod Finish Activation.imageset/Pod-FinishActivation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Pod Finish Activation.imageset/Pod-FinishActivation.pdf -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod Start Activation.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "Pod-StartActivation.pdf", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod Start Activation.imageset/Pod-StartActivation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Pod Start Activation.imageset/Pod-StartActivation.pdf -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pod@3x-1.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "pod@3x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "pod@3x-2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod.imageset/pod@3x-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Pod.imageset/pod@3x-1.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod.imageset/pod@3x-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Pod.imageset/pod@3x-2.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Pod.imageset/pod@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Pod.imageset/pod@3x.png -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Prep Pod.dataset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "data" : [ 7 | { 8 | "idiom" : "universal", 9 | "filename" : "prepPod.gif" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/Prep Pod.dataset/prepPod.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/Assets.xcassets/Prep Pod.dataset/prepPod.gif -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/pod_reservoir_mask_swiftui.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pod_reservoir_mask.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/pod_reservoir_mask_swiftui.imageset/pod_reservoir_mask.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml 56 | -------------------------------------------------------------------------------- /DashKitUI/Assets.xcassets/pod_reservoir_swiftui.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pod_reservoir.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DashKitUI/DashKitUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // DashKitUI.h 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/18/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for DashKitUI. 12 | FOUNDATION_EXPORT double DashKitUIVersionNumber; 13 | 14 | //! Project version string for DashKitUI. 15 | FOUNDATION_EXPORT const unsigned char DashKitUIVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /DashKitUI/Extensions/Image.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/7/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | private class FrameworkBundle { 12 | static let main = Bundle(for: FrameworkBundle.self) 13 | } 14 | 15 | extension Image { 16 | init(frameworkImage name: String, decorative: Bool = false) { 17 | if decorative { 18 | self.init(decorative: name, bundle: FrameworkBundle.main) 19 | } else { 20 | self.init(name, bundle: FrameworkBundle.main) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /DashKitUI/Extensions/UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/11/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private class FrameworkBundle { 12 | static let main = Bundle(for: FrameworkBundle.self) 13 | } 14 | 15 | extension UIImage { 16 | convenience init?(frameworkImage name: String) { 17 | self.init(named: name, in: FrameworkBundle.main, with: nil) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DashKitUI/Extensions/UITableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UITableViewCell.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/19/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension UITableViewCell: IdentifiableClass { } 13 | -------------------------------------------------------------------------------- /DashKitUI/Extensions/UIViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIViewControllerExtension.swift 3 | // SampleApp 4 | // 5 | // Copyright (C) 2019, Insulet Corporation 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software 8 | // and associated documentation files (the "Software"), to deal in the Software without restriction, 9 | // including without limitation the rights to use, copy, modify, merge, publish, distribute, 10 | // sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in all copies or substantial 14 | // portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 17 | // NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 19 | // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | // 22 | 23 | import UIKit 24 | var vSpinner : UIView? 25 | 26 | extension UITextField { 27 | func addDoneToolbar(onDone: (target: Any, action: Selector)? = nil) { 28 | let onDone = onDone ?? (target: self, action: #selector(doneButtonTapped)) 29 | let toolbar: UIToolbar = UIToolbar() 30 | toolbar.barStyle = .default 31 | toolbar.items = [ 32 | UIBarButtonItem(title: "Done", style: .done, target: onDone.target, action: onDone.action) 33 | ] 34 | toolbar.sizeToFit() 35 | self.inputAccessoryView = toolbar 36 | } 37 | @objc func doneButtonTapped() { self.resignFirstResponder() } 38 | } 39 | 40 | extension UIViewController { 41 | 42 | public func presentOkDialog(title: String, message: String) { 43 | let alertController = UIAlertController(title: title, message: message, 44 | preferredStyle: UIAlertController.Style.alert) 45 | let okAction = UIAlertAction(title: LocalizedString("OK", comment: "OK button in a dialog"), 46 | style: UIAlertAction.Style.default) { 47 | (result : UIAlertAction) -> Void in 48 | // user tapped OK 49 | } 50 | alertController.addAction(okAction) 51 | 52 | self.present(alertController, animated: true, completion: nil) 53 | } 54 | 55 | public func presentOkDialog(title: String, message: String, okButtonHandler: @escaping ((UIAlertAction) -> (Swift.Void))) { 56 | let alertController = UIAlertController(title: title, message: message, 57 | preferredStyle: UIAlertController.Style.alert) 58 | let okAction = UIAlertAction(title: LocalizedString("OK", comment: "OK button in a dialog"), 59 | style: UIAlertAction.Style.default, 60 | handler: okButtonHandler) 61 | alertController.addAction(okAction) 62 | 63 | self.present(alertController, animated: true, completion: nil) 64 | } 65 | 66 | public func presentOkCancelDialog(title: String, message: String, okHandler: @escaping ((UIAlertAction) -> (Swift.Void)), cancelHandler: ((UIAlertAction) -> (Swift.Void))?) { 67 | let alertController = UIAlertController(title: title, message: message, 68 | preferredStyle: UIAlertController.Style.alert) 69 | let okAction = UIAlertAction(title: LocalizedString("Try again", comment: "Try again button in a dialog"), 70 | style: UIAlertAction.Style.default, 71 | handler: okHandler) 72 | alertController.addAction(okAction) 73 | let cancelAction = UIAlertAction(title: LocalizedString("Deactivate Pod", comment: "Deactivate Pod button in a dialog"), 74 | style: UIAlertAction.Style.default, 75 | handler: cancelHandler) 76 | alertController.addAction(cancelAction) 77 | 78 | self.present(alertController, animated: true, completion: nil) 79 | } 80 | 81 | func showSpinner(onView : UIView) { 82 | let spinnerView = UIView.init(frame: onView.bounds) 83 | spinnerView.backgroundColor = UIColor.init(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5) 84 | let ai = UIActivityIndicatorView.init(style: .whiteLarge) 85 | ai.startAnimating() 86 | ai.center = spinnerView.center 87 | 88 | DispatchQueue.main.async { 89 | spinnerView.addSubview(ai) 90 | onView.addSubview(spinnerView) 91 | } 92 | vSpinner = spinnerView 93 | } 94 | 95 | func removeSpinner() { 96 | DispatchQueue.main.async { 97 | vSpinner?.removeFromSuperview() 98 | vSpinner = nil 99 | } 100 | } 101 | } 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /DashKitUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockCannulaInserter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockCannulaInserter.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/10/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import DashKit 10 | import PodSDK 11 | 12 | class MockCannulaInserter: CannulaInserter { 13 | 14 | private var attemptCount = 0 15 | var initialError: PodCommError = .bleCommunicationError 16 | 17 | func insertCannula(eventListener: @escaping (ActivationStatus) -> ()) { 18 | attemptCount += 1 19 | 20 | if attemptCount == 1 { 21 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 22 | eventListener(.error(.bleCommunicationError)) 23 | } 24 | } else { 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 26 | eventListener(.event(.insertingCannula)) 27 | } 28 | 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2 + Pod.estimatedCannulaInsertionDuration) { 30 | eventListener(.event(.step2Completed)) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockNavigator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNavigator.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/20/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class MockNavigator: DashUINavigator { 12 | func navigateTo(_ screen: DashUIScreen) { } 13 | } 14 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockPodDeactivater.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodDeactivater.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/9/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import DashKit 10 | import PodSDK 11 | 12 | class MockPodDeactivater: PodDeactivater { 13 | private var attemptCount = 0 14 | 15 | func deactivatePod(completion: @escaping (PodCommResult) -> ()) { 16 | attemptCount += 1 17 | 18 | if attemptCount == 1 { 19 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 20 | completion(.failure(.bleCommunicationError)) 21 | } 22 | } else { 23 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 24 | completion(.success(MockPodStatus.normal)) 25 | } 26 | } 27 | } 28 | 29 | func discardPod(completion: @escaping (PodCommResult) -> ()) { 30 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 31 | completion(.success(true)) 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockPodDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodDetails.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/14/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MockPodDetails: PodDetails { 12 | var podIdentifier = "123" 13 | 14 | var lotNumber = "L44716" 15 | 16 | var tid = "0531513" 17 | 18 | var piPmVersion = "2.9.0" 19 | 20 | var pdmIdentifier = "18859275929" 21 | 22 | var sdkVersion = "1.0.mock" 23 | } 24 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockPodPairer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodPairer.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/5/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import PodSDK 10 | import DashKit 11 | 12 | 13 | class MockPodPairer: PodPairer { 14 | private var attemptCount = 0 15 | 16 | var podCommState: PodCommState = .noPod 17 | 18 | //var initialError: PodCommError = .internalError(.incompatibleProductId) 19 | var initialError: PodCommError = .podNotAvailable 20 | 21 | func pair(eventListener: @escaping (ActivationStatus) -> ()) { 22 | attemptCount += 1 23 | 24 | if attemptCount == 1 { 25 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 26 | eventListener(.error(self.initialError)) 27 | } 28 | } else { 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 30 | eventListener(.event(.connecting)) 31 | } 32 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 33 | self.podCommState = .activating 34 | eventListener(.event(.primingPod)) 35 | } 36 | // Priming is normally 35s, but we'll send the completion faster in the mock 37 | DispatchQueue.main.asyncAfter(deadline: .now() + 10) { 38 | self.podCommState = .active 39 | eventListener(.event(.step1Completed)) 40 | } 41 | } 42 | } 43 | 44 | func discardPod(completion: @escaping (PodCommResult) -> ()) { 45 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 46 | self.podCommState = .noPod 47 | completion(.success(true)) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /DashKitUI/Mocks/MockRegistrationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRegistrationManager.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/11/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DashKit 11 | import PodSDK 12 | 13 | public class MockRegistrationManager: PDMRegistrator { 14 | 15 | public var initialResponse: RegistrationStatus = .connectionTimeout 16 | 17 | private var attemptCount: Int = 0 18 | 19 | private var _isRegistered: Bool 20 | 21 | public init(isRegistered: Bool = false) { 22 | self._isRegistered = isRegistered 23 | } 24 | 25 | func startRegistration(phoneNumber: String, completion: @escaping (RegistrationStatus) -> ()) { 26 | // not used 27 | completion(.invalidConfiguration) 28 | } 29 | 30 | public func startRegistration(completion: @escaping (RegistrationStatus) -> ()) { 31 | attemptCount += 1 32 | let localAttemptCount = attemptCount 33 | 34 | if _isRegistered { 35 | DispatchQueue.main.async { 36 | completion(.alreadyRegistered) 37 | } 38 | } else { 39 | DispatchQueue.main.asyncAfter(deadline: .now() + 2) { 40 | if localAttemptCount == 1 { 41 | completion(self.initialResponse) 42 | } else { 43 | completion(.registered) 44 | } 45 | } 46 | } 47 | } 48 | 49 | func finishRegistration(verificationCode: String, completion: @escaping (RegistrationStatus) -> ()) { 50 | // not used 51 | completion(.registered) 52 | } 53 | 54 | public func isRegistered() -> Bool { 55 | return _isRegistered 56 | } 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /DashKitUI/Protocols/PodDetails .swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodDetails .swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/14/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol PodDetails { 12 | var podIdentifier: String { get } 13 | var lotNumber: String { get } 14 | var tid: String { get } 15 | var piPmVersion: String { get } 16 | var pdmIdentifier: String { get } 17 | var sdkVersion: String { get } 18 | } 19 | -------------------------------------------------------------------------------- /DashKitUI/PumpManager/DashHUDProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashHUDProvider.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/19/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | import LoopKit 12 | import LoopKitUI 13 | import DashKit 14 | import PodSDK 15 | 16 | public enum ReservoirAlertState { 17 | case ok 18 | case lowReservoir 19 | case empty 20 | } 21 | 22 | internal class DashHUDProvider: NSObject, HUDProvider { 23 | var managerIdentifier: String { 24 | return pumpManager.managerIdentifier 25 | } 26 | 27 | private let pumpManager: DashPumpManager 28 | 29 | private var reservoirView: OmnipodReservoirView? 30 | 31 | private let bluetoothProvider: BluetoothProvider 32 | 33 | private let colorPalette: LoopUIColorPalette 34 | 35 | private var refreshTimer: Timer? 36 | 37 | private let allowedInsulinTypes: [InsulinType] 38 | 39 | var visible: Bool = false { 40 | didSet { 41 | if oldValue != visible && visible { 42 | hudDidAppear() 43 | } 44 | } 45 | } 46 | 47 | public init(pumpManager: DashPumpManager, bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowedInsulinTypes: [InsulinType]) { 48 | self.pumpManager = pumpManager 49 | self.bluetoothProvider = bluetoothProvider 50 | self.colorPalette = colorPalette 51 | self.allowedInsulinTypes = allowedInsulinTypes 52 | super.init() 53 | self.pumpManager.addPodStatusObserver(self, queue: .main) 54 | } 55 | 56 | public func createHUDView() -> BaseHUDView? { 57 | reservoirView = OmnipodReservoirView.instantiate() 58 | updateReservoirView() 59 | 60 | return reservoirView 61 | } 62 | 63 | public func didTapOnHUDView(_ view: BaseHUDView, allowDebugFeatures: Bool) -> HUDTapAction? { 64 | let vc = pumpManager.settingsViewController(bluetoothProvider: bluetoothProvider, colorPalette: colorPalette, allowDebugFeatures: allowDebugFeatures, allowedInsulinTypes: allowedInsulinTypes) 65 | return HUDTapAction.presentViewController(vc) 66 | } 67 | 68 | func hudDidAppear() { 69 | updateReservoirView() 70 | pumpManager.getPodStatus { (_) in } 71 | updateRefreshTimer() 72 | } 73 | 74 | public var hudViewRawState: HUDProvider.HUDViewRawState { 75 | var rawValue: HUDProvider.HUDViewRawState = [:] 76 | 77 | rawValue["lastStatusDate"] = pumpManager.lastStatusDate 78 | 79 | if let reservoirLevel = pumpManager.reservoirLevel { 80 | rawValue["reservoirLevel"] = reservoirLevel.rawValue 81 | } 82 | 83 | if let reservoirLevelHighlightState = pumpManager.reservoirLevelHighlightState { 84 | rawValue["reservoirLevelHighlightState"] = reservoirLevelHighlightState.rawValue 85 | } 86 | 87 | return rawValue 88 | } 89 | 90 | public static func createHUDView(rawValue: HUDProvider.HUDViewRawState) -> LevelHUDView? { 91 | guard let rawReservoirLevel = rawValue["reservoirLevel"] as? ReservoirLevel.RawValue, 92 | let rawReservoirLevelHighlightState = rawValue["reservoirLevelHighlightState"] as? ReservoirLevelHighlightState.RawValue, 93 | let reservoirLevelHighlightState = ReservoirLevelHighlightState(rawValue: rawReservoirLevelHighlightState) 94 | else { 95 | return nil 96 | } 97 | 98 | let reservoirView: OmnipodReservoirView? 99 | 100 | let reservoirLevel = ReservoirLevel(rawValue: rawReservoirLevel) 101 | 102 | if let lastStatusDate = rawValue["lastStatusDate"] as? Date { 103 | reservoirView = OmnipodReservoirView.instantiate() 104 | reservoirView!.update(level: reservoirLevel, at: lastStatusDate, reservoirLevelHighlightState: reservoirLevelHighlightState) 105 | } else { 106 | reservoirView = nil 107 | } 108 | 109 | return reservoirView 110 | } 111 | 112 | private func updateReservoirView() { 113 | guard let reservoirView = reservoirView, 114 | let lastStatusDate = pumpManager.lastStatusDate, 115 | let reservoirLevelHighlightState = pumpManager.reservoirLevelHighlightState else 116 | { 117 | return 118 | } 119 | 120 | reservoirView.update(level: pumpManager.reservoirLevel, at: lastStatusDate, reservoirLevelHighlightState: reservoirLevelHighlightState) 121 | } 122 | 123 | private func ensureRefreshTimerRunning() { 124 | guard refreshTimer == nil else { 125 | return 126 | } 127 | 128 | // 40 seconds is time for one unit 129 | refreshTimer = Timer(timeInterval: .seconds(40) , repeats: true) { _ in 130 | self.pumpManager.getPodStatus { _ in 131 | self.updateReservoirView() 132 | } 133 | } 134 | RunLoop.main.add(refreshTimer!, forMode: .default) 135 | } 136 | 137 | private func stopRefreshTimer() { 138 | refreshTimer?.invalidate() 139 | refreshTimer = nil 140 | } 141 | 142 | private func updateRefreshTimer() { 143 | if case .inProgress = pumpManager.status.bolusState, visible { 144 | ensureRefreshTimerRunning() 145 | } else { 146 | stopRefreshTimer() 147 | } 148 | } 149 | } 150 | 151 | extension DashHUDProvider: PodStatusObserver { 152 | func didUpdatePodStatus() { 153 | updateRefreshTimer() 154 | updateReservoirView() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /DashKitUI/PumpManager/DashPumpManager+UI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashPumpManager+UI.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/19/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LoopKit 11 | import LoopKitUI 12 | import DashKit 13 | import SwiftUI 14 | 15 | extension DashPumpManager: PumpManagerUI { 16 | 17 | 18 | public static var onboardingImage: UIImage? { 19 | return UIImage(named: "Onboarding", in: Bundle(for: DashSettingsViewModel.self), compatibleWith: nil) 20 | } 21 | 22 | public static func setupViewController(initialSettings settings: PumpManagerSetupSettings, bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, allowedInsulinTypes: [InsulinType]) -> SetupUIResult { 23 | let vc = DashUICoordinator(colorPalette: colorPalette, pumpManagerType: self, basalSchedule: settings.basalSchedule, allowDebugFeatures: allowDebugFeatures) 24 | return .userInteractionRequired(vc) 25 | } 26 | 27 | public func settingsViewController(bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, allowedInsulinTypes: [InsulinType]) -> PumpManagerViewController { 28 | return DashUICoordinator(pumpManager: self, colorPalette: colorPalette, allowDebugFeatures: allowDebugFeatures) 29 | } 30 | 31 | public func deliveryUncertaintyRecoveryViewController(colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> (UIViewController & CompletionNotifying) { 32 | return DashUICoordinator(pumpManager: self, colorPalette: colorPalette, allowDebugFeatures: allowDebugFeatures) 33 | } 34 | 35 | public var smallImage: UIImage? { 36 | return UIImage(named: "Pod", in: Bundle(for: DashSettingsViewModel.self), compatibleWith: nil)! 37 | } 38 | 39 | public func hudProvider(bluetoothProvider: BluetoothProvider, colorPalette: LoopUIColorPalette, allowedInsulinTypes: [InsulinType]) -> HUDProvider? { 40 | return DashHUDProvider(pumpManager: self, bluetoothProvider: bluetoothProvider, colorPalette: colorPalette, allowedInsulinTypes: allowedInsulinTypes) 41 | } 42 | 43 | public static func createHUDView(rawValue: HUDProvider.HUDViewRawState) -> BaseHUDView? { 44 | return DashHUDProvider.createHUDView(rawValue: rawValue) 45 | } 46 | 47 | } 48 | 49 | public enum DashStatusBadge: DeviceStatusBadge { 50 | case timeSyncNeeded 51 | 52 | public var image: UIImage? { 53 | switch self { 54 | case .timeSyncNeeded: 55 | return UIImage(systemName: "clock.fill") 56 | } 57 | } 58 | 59 | public var state: DeviceStatusBadgeState { 60 | switch self { 61 | case .timeSyncNeeded: 62 | return .warning 63 | } 64 | } 65 | } 66 | 67 | 68 | 69 | // MARK: - PumpStatusIndicator 70 | extension DashPumpManager { 71 | 72 | public var pumpStatusHighlight: DeviceStatusHighlight? { 73 | return buildPumpStatusHighlight(for: state) 74 | } 75 | 76 | public var pumpLifecycleProgress: DeviceLifecycleProgress? { 77 | return buildPumpLifecycleProgress(for: state) 78 | } 79 | 80 | public var pumpStatusBadge: DeviceStatusBadge? { 81 | if isClockOffset { 82 | return DashStatusBadge.timeSyncNeeded 83 | } else { 84 | return nil 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/ExpirationReminderDateTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpirationReminderDateTableViewCell.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/16/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LoopKitUI 11 | 12 | public class ExpirationReminderDateTableViewCell: DatePickerTableViewCell { 13 | 14 | public weak var delegate: DatePickerTableViewCellDelegate? 15 | 16 | @IBOutlet public weak var titleLabel: UILabel! 17 | 18 | @IBOutlet public weak var dateLabel: UILabel! 19 | 20 | var maximumDate: Date? { 21 | set { 22 | datePicker.maximumDate = newValue 23 | } 24 | get { 25 | return datePicker.maximumDate 26 | } 27 | } 28 | 29 | var minimumDate: Date? { 30 | set { 31 | datePicker.minimumDate = newValue 32 | } 33 | get { 34 | return datePicker.minimumDate 35 | } 36 | } 37 | 38 | private lazy var formatter: DateFormatter = { 39 | let formatter = DateFormatter() 40 | formatter.timeStyle = .short 41 | formatter.dateStyle = .medium 42 | formatter.doesRelativeDateFormatting = true 43 | 44 | return formatter 45 | }() 46 | 47 | public override func updateDateLabel() { 48 | dateLabel.text = formatter.string(from: date) 49 | } 50 | 51 | public override func dateChanged(_ sender: UIDatePicker) { 52 | super.dateChanged(sender) 53 | 54 | delegate?.datePickerTableViewCellDidUpdateDate(self) 55 | } 56 | } 57 | 58 | extension ExpirationReminderDateTableViewCell: NibLoadable { } 59 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/pod_life/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/pod_life/pod_life.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "pod_life.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template", 14 | "preserves-vector-representation" : true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/pod_life/pod_life.imageset/pod_life.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/UIViews/HUDAssets.xcassets/pod_life/pod_life.imageset/pod_life.pdf -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/reservoir/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/reservoir/pod_reservoir.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pod_reservoir.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "template-rendering-intent" : "template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "pod_reservoir_mask.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/pod_reservoir_mask.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tidepool-org/DashKit/2b4ef23671bcf916ac95e2b1914032c4171bb8e0/DashKitUI/UIViews/HUDAssets.xcassets/reservoir/pod_reservoir_mask.imageset/pod_reservoir_mask.pdf -------------------------------------------------------------------------------- /DashKitUI/UIViews/InsulinStatusTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsulinStatusTableViewCell.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/29/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DashKit 11 | import LoopKit 12 | import HealthKit 13 | 14 | public class InsulinStatusTableViewCell: UITableViewCell { 15 | 16 | private lazy var timeFormatter: DateFormatter = { 17 | let formatter = DateFormatter() 18 | formatter.dateStyle = .short 19 | formatter.timeStyle = .short 20 | formatter.doesRelativeDateFormatting = true 21 | formatter.formattingContext = .middleOfSentence 22 | 23 | return formatter 24 | }() 25 | 26 | private lazy var numberFormatter: NumberFormatter = { 27 | let formatter = NumberFormatter() 28 | formatter.numberStyle = .decimal 29 | formatter.maximumFractionDigits = 0 30 | 31 | return formatter 32 | }() 33 | 34 | fileprivate lazy var quantityFormatter: QuantityFormatter = { 35 | let quantityFormatter = QuantityFormatter() 36 | quantityFormatter.numberFormatter.minimumFractionDigits = 0 37 | quantityFormatter.numberFormatter.maximumFractionDigits = 0 38 | 39 | return quantityFormatter 40 | }() 41 | 42 | @IBOutlet public weak var insulinLabel: UILabel! 43 | 44 | @IBOutlet public weak var recencyLabel: UILabel! 45 | 46 | public func setReservoir(level: ReservoirLevel, validAt date: Date) { 47 | 48 | let time = timeFormatter.string(from: date) 49 | 50 | switch level { 51 | case .aboveThreshold: 52 | if let units = numberFormatter.string(from: Pod.maximumReservoirReading) { 53 | insulinLabel.text = String(format: LocalizedString("Pod Insulin: %@+ U", comment: "Format string for status page reservoir volume when above maximum reading. (1: The maximum reading)"), units) 54 | accessibilityValue = String(format: LocalizedString("Greater than %1$@ units remaining at %2$@", comment: "Accessibility format string for status page reservoir volume when reading is above maximum (1: localized volume)(2: time)"), units, time) 55 | } 56 | case .valid(let value): 57 | let unit: HKUnit = .internationalUnit() 58 | let quantity = HKQuantity(unit: unit, doubleValue: value) 59 | if let quantityString = quantityFormatter.string(from: quantity, for: unit) { 60 | insulinLabel.text = String(format: LocalizedString("Pod Insulin: %1$@", comment: "Format string for for status page reservoir volume. (1: The localized volume)"), quantityString) 61 | accessibilityValue = String(format: LocalizedString("%1$@ units remaining at %2$@", comment: "Accessibility format string for status page reservoir volume (1: localized volume)(2: time)"), quantityString, time) 62 | } 63 | } 64 | recencyLabel.text = String(format: LocalizedString("(updated %1$@)", comment: "Accessibility format string for (1: localized volume)(2: time)"), time) 65 | } 66 | } 67 | 68 | extension InsulinStatusTableViewCell: NibLoadable { } 69 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/OmnipodReservoirView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OmnipodReservoirView.swift 3 | // OmniKit 4 | // 5 | // Created by Pete Schwamb on 10/22/18. 6 | // Copyright © 2018 Pete Schwamb. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import LoopKitUI 11 | import DashKit 12 | 13 | public final class OmnipodReservoirView: LevelHUDView, NibLoadable { 14 | 15 | override public var orderPriority: HUDViewOrderPriority { 16 | return 11 17 | } 18 | 19 | @IBOutlet private weak var volumeLabel: UILabel! 20 | 21 | @IBOutlet private weak var alertLabel: UILabel! { 22 | didSet { 23 | alertLabel.alpha = 0 24 | alertLabel.textColor = UIColor.white 25 | alertLabel.layer.cornerRadius = 9 26 | alertLabel.clipsToBounds = true 27 | } 28 | } 29 | 30 | public class func instantiate() -> OmnipodReservoirView { 31 | return nib().instantiate(withOwner: nil, options: nil)[0] as! OmnipodReservoirView 32 | } 33 | 34 | override public func awakeFromNib() { 35 | super.awakeFromNib() 36 | 37 | volumeLabel.isHidden = true 38 | } 39 | 40 | private var reservoirLevel: ReservoirLevel? 41 | private var lastUpdateDate: Date? 42 | private var reservoirLevelHighlightState = ReservoirLevelHighlightState.normal 43 | 44 | override public func tintColorDidChange() { 45 | super.tintColorDidChange() 46 | 47 | alertLabel?.backgroundColor = tintColor 48 | volumeLabel.textColor = tintColor 49 | levelMaskView.tintColor = tintColor 50 | } 51 | 52 | override public func updateColor() { 53 | switch reservoirLevelHighlightState { 54 | case .normal: 55 | tintColor = stateColors?.normal 56 | case .warning: 57 | tintColor = stateColors?.warning 58 | case .critical: 59 | tintColor = stateColors?.error 60 | } 61 | } 62 | 63 | private lazy var timeFormatter: DateFormatter = { 64 | let formatter = DateFormatter() 65 | formatter.dateStyle = .none 66 | formatter.timeStyle = .short 67 | 68 | return formatter 69 | }() 70 | 71 | private lazy var numberFormatter: NumberFormatter = { 72 | let formatter = NumberFormatter() 73 | formatter.numberStyle = .decimal 74 | formatter.maximumFractionDigits = 0 75 | 76 | return formatter 77 | }() 78 | 79 | private func updateViews() { 80 | if let reservoirLevel = reservoirLevel, let date = lastUpdateDate { 81 | 82 | let time = timeFormatter.string(from: date) 83 | caption?.text = time 84 | 85 | switch reservoirLevel { 86 | case .aboveThreshold: 87 | level = nil 88 | volumeLabel.isHidden = true 89 | if let units = numberFormatter.string(from: Pod.maximumReservoirReading) { 90 | volumeLabel.text = String(format: LocalizedString("%@+ U", comment: "Format string for reservoir volume when above maximum reading. (1: The maximum reading)"), units) 91 | accessibilityValue = String(format: LocalizedString("Greater than %1$@ units remaining at %2$@", comment: "Accessibility format string for (1: localized volume)(2: time)"), units, time) 92 | } 93 | case .valid(let value): 94 | level = reservoirLevel.percentage 95 | volumeLabel.isHidden = false 96 | 97 | if let units = numberFormatter.string(from: value) { 98 | volumeLabel.text = String(format: LocalizedString("%@U", comment: "Format string for reservoir volume. (1: The localized volume)"), units) 99 | 100 | accessibilityValue = String(format: LocalizedString("%1$@ units remaining at %2$@", comment: "Accessibility format string for (1: localized volume)(2: time)"), units, time) 101 | } 102 | } 103 | } else { 104 | level = 0 105 | volumeLabel.isHidden = true 106 | } 107 | 108 | var alertLabelAlpha: CGFloat = 1 109 | switch reservoirLevelHighlightState { 110 | case .normal: 111 | alertLabelAlpha = 0 112 | case .warning, .critical: 113 | alertLabel?.text = "!" 114 | } 115 | 116 | UIView.animate(withDuration: 0.25, animations: { 117 | self.alertLabel?.alpha = alertLabelAlpha 118 | }) 119 | } 120 | 121 | public func update(level: ReservoirLevel?, at date: Date, reservoirLevelHighlightState: ReservoirLevelHighlightState) { 122 | self.reservoirLevel = level 123 | self.lastUpdateDate = date 124 | self.reservoirLevelHighlightState = reservoirLevelHighlightState 125 | updateViews() 126 | } 127 | } 128 | 129 | 130 | -------------------------------------------------------------------------------- /DashKitUI/UIViews/PodExpirationTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodExpirationTableViewCell.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/31/19. 6 | // Copyright © 2019 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class PodExpirationTableViewCell: UITableViewCell { 12 | 13 | @IBOutlet public weak var dateLabel: UILabel! 14 | 15 | @IBOutlet public weak var timeLabel: UILabel! 16 | 17 | private lazy var dateFormatter: DateFormatter = { 18 | let formatter = DateFormatter() 19 | formatter.dateStyle = .medium 20 | formatter.timeStyle = .short 21 | 22 | return formatter 23 | }() 24 | 25 | public var expirationDate: Date? { 26 | didSet { 27 | if let date = expirationDate { 28 | let calendar = Calendar.current 29 | var dayText: String? = nil 30 | if calendar.isDateInToday(date) { 31 | dayText = LocalizedString("Today", comment: "Name for current day") 32 | } else if calendar.isDateInTomorrow(date) { 33 | dayText = LocalizedString("Tomorrow", comment: "Name for day following this day") 34 | } else if calendar.isDateInYesterday(date) { 35 | dayText = LocalizedString("Yesterday", comment: "Name for day preceeding this day") 36 | } else { 37 | if let weekday = Calendar.current.dateComponents([.weekday], from: date).weekday { 38 | dayText = dateFormatter.weekdaySymbols[weekday-1] 39 | } 40 | } 41 | dateLabel.text = dayText 42 | timeLabel.text = dateFormatter.string(from: date) 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /DashKitUI/View Models/DeliveryUncertaintyRecoveryViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeliveryUncertaintyRecoveryViewModel.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 8/25/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DashKit 11 | import LoopKit 12 | 13 | class DeliveryUncertaintyRecoveryViewModel: PumpManagerStatusObserver { 14 | 15 | let appName: String 16 | let uncertaintyStartedAt: Date 17 | 18 | var onDismiss: (() -> Void)? 19 | var didRecover: (() -> Void)? 20 | var onDeactivate: (() -> Void)? 21 | 22 | private var finished = false 23 | 24 | init(appName: String, uncertaintyStartedAt: Date) { 25 | self.appName = appName 26 | self.uncertaintyStartedAt = uncertaintyStartedAt 27 | } 28 | 29 | func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus, oldStatus: PumpManagerStatus) { 30 | if !finished { 31 | if !status.deliveryIsUncertain { 32 | didRecover?() 33 | } 34 | } 35 | } 36 | 37 | func podDeactivationChosen() { 38 | finished = true 39 | self.onDeactivate?() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DashKitUI/View Models/InsertCannulaViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsertCannulaViewModel.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/10/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import DashKit 10 | import LoopKitUI 11 | import PodSDK 12 | 13 | class InsertCannulaViewModel: ObservableObject, Identifiable { 14 | 15 | enum InsertCannulaViewModelState { 16 | case ready 17 | case startingInsertion 18 | case inserting(finishTime: CFTimeInterval) 19 | case error(PodCommError) 20 | case finished 21 | 22 | var actionButtonAccessibilityLabel: String { 23 | switch self { 24 | case .ready, .startingInsertion: 25 | return LocalizedString("Insert Cannula", comment: "Insert cannula action button accessibility label while ready to pair") 26 | case .inserting: 27 | return LocalizedString("Inserting. Please wait.", comment: "Insert cannula action button accessibility label while pairing") 28 | case .error(let error): 29 | return String(format: "%@ %@", error.errorDescription ?? "", error.recoverySuggestion ?? "") 30 | case .finished: 31 | return LocalizedString("Cannula inserted successfully. Continue.", comment: "Insert cannula action button accessibility label when cannula insertion succeeded") 32 | } 33 | } 34 | 35 | var instructionsDisabled: Bool { 36 | switch self { 37 | case .ready, .error: 38 | return false 39 | default: 40 | return true 41 | } 42 | } 43 | 44 | var nextActionButtonDescription: String { 45 | switch self { 46 | case .ready: 47 | return LocalizedString("Insert Cannula", comment: "Cannula insertion button text while ready to insert") 48 | case .error: 49 | return LocalizedString("Retry", comment: "Cannula insertion button text while showing error") 50 | case .inserting, .startingInsertion: 51 | return LocalizedString("Inserting...", comment: "Cannula insertion button text while inserting") 52 | case .finished: 53 | return LocalizedString("Continue", comment: "Cannula insertion button text when inserted") 54 | } 55 | } 56 | 57 | var nextActionButtonStyle: ActionButton.ButtonType { 58 | switch self { 59 | case .error(let error): 60 | if !error.recoverable { 61 | return .destructive 62 | } 63 | default: 64 | break 65 | } 66 | return .primary 67 | } 68 | 69 | var progressState: ProgressIndicatorState { 70 | switch self { 71 | case .ready, .error: 72 | return .hidden 73 | case .startingInsertion: 74 | return .indeterminantProgress 75 | case .inserting(let finishTime): 76 | return .timedProgress(finishTime: finishTime) 77 | case .finished: 78 | return .completed 79 | } 80 | } 81 | 82 | var showProgressDetail: Bool { 83 | switch self { 84 | case .ready: 85 | return false 86 | default: 87 | return true 88 | } 89 | } 90 | 91 | var isProcessing: Bool { 92 | switch self { 93 | case .startingInsertion, .inserting: 94 | return true 95 | default: 96 | return false 97 | } 98 | } 99 | 100 | var isFinished: Bool { 101 | if case .finished = self { 102 | return true 103 | } 104 | return false 105 | } 106 | } 107 | 108 | var error: PodCommError? { 109 | if case .error(let error) = self.state { 110 | return error 111 | } 112 | return nil 113 | } 114 | 115 | @Published var state: InsertCannulaViewModelState = .ready 116 | 117 | var didFinish: (() -> Void)? 118 | 119 | var didRequestDeactivation: (() -> Void)? 120 | 121 | var cannulaInserter: CannulaInserter 122 | 123 | init(cannulaInserter: CannulaInserter) { 124 | self.cannulaInserter = cannulaInserter 125 | } 126 | 127 | private func handleEvent(_ event: ActivationStep2Event) { 128 | switch event { 129 | case .insertingCannula: 130 | let finishTime = TimeInterval(Pod.estimatedCannulaInsertionDuration) 131 | state = .inserting(finishTime: CACurrentMediaTime() + finishTime) 132 | case .step2Completed: 133 | state = .finished 134 | default: 135 | break 136 | } 137 | } 138 | 139 | private func insertCannula() { 140 | state = .startingInsertion 141 | 142 | cannulaInserter.insertCannula { (status) in 143 | switch status { 144 | case .error(let error): 145 | self.state = .error(error) 146 | case .event(let event): 147 | self.handleEvent(event) 148 | } 149 | } 150 | } 151 | 152 | public func continueButtonTapped() { 153 | switch state { 154 | case .finished: 155 | didFinish?() 156 | case .error(let error): 157 | if error.recoverable { 158 | insertCannula() 159 | } else { 160 | didRequestDeactivation?() 161 | } 162 | default: 163 | insertCannula() 164 | } 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /DashKitUI/View Models/MockPodSettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodSettingsViewModel.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 1/7/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DashKit 11 | import PodSDK 12 | 13 | class MockPodSettingsViewModel: ObservableObject, Identifiable { 14 | public var mockPodCommManager: MockPodCommManager 15 | @Published var activeAlerts: PodAlerts 16 | 17 | @Published var nextCommsError: PodCommError? { 18 | didSet { 19 | mockPodCommManager.nextCommsError = nextCommsError 20 | } 21 | } 22 | var updatedReservoir: NSNumber? 23 | 24 | var reservoirString: String { 25 | didSet { 26 | updatedReservoir = numberFormatter.number(from: reservoirString) 27 | } 28 | } 29 | 30 | @Published var activationDate: Date 31 | 32 | var numberFormatter: NumberFormatter = { 33 | let formatter = NumberFormatter() 34 | formatter.numberStyle = .decimal 35 | formatter.maximumFractionDigits = 2 36 | return formatter 37 | }() 38 | 39 | init(mockPodCommManager: MockPodCommManager) { 40 | self.mockPodCommManager = mockPodCommManager 41 | 42 | let reservoirAmount: Double 43 | 44 | if let podStatus = mockPodCommManager.podStatus { 45 | self.activeAlerts = podStatus.activeAlerts 46 | self.activationDate = podStatus.activationDate 47 | reservoirAmount = podStatus.initialInsulinAmount - podStatus.insulinDelivered 48 | } else { 49 | self.activeAlerts = PodAlerts() 50 | self.activationDate = Date() 51 | reservoirAmount = 0 52 | } 53 | 54 | reservoirString = numberFormatter.string(from: reservoirAmount) ?? "" 55 | 56 | nextCommsError = mockPodCommManager.nextCommsError 57 | 58 | mockPodCommManager.addObserver(self, queue: DispatchQueue.main) 59 | } 60 | 61 | func issueAlert(_ alert: PodAlerts) { 62 | mockPodCommManager.issueAlerts(alert) 63 | 64 | if let podStatus = mockPodCommManager.podStatus { 65 | activeAlerts = podStatus.activeAlerts 66 | } 67 | } 68 | 69 | func clearAlert(_ alert: PodAlerts) { 70 | mockPodCommManager.clearAlerts(alert) 71 | 72 | if let podStatus = mockPodCommManager.podStatus { 73 | activeAlerts = podStatus.activeAlerts 74 | } 75 | } 76 | 77 | func triggerAlarm(_ alarm: SimulatedPodAlarm) { 78 | mockPodCommManager.triggerAlarm(alarm.alarmCode) 79 | } 80 | 81 | func triggerSystemError() { 82 | mockPodCommManager.triggerSystemError() 83 | } 84 | 85 | func applyPendingUpdates() { 86 | if let podStatus = mockPodCommManager.podStatus { 87 | if let value = numberFormatter.number(from: reservoirString) { 88 | mockPodCommManager.podStatus?.insulinDelivered = podStatus.initialInsulinAmount - Double(truncating: value) 89 | } 90 | mockPodCommManager.podStatus?.activationDate = activationDate 91 | } 92 | mockPodCommManager.dashPumpManager?.getPodStatus() { _ in } 93 | } 94 | } 95 | 96 | extension MockPodSettingsViewModel: MockPodCommManagerObserver { 97 | func mockPodCommManagerDidUpdate() { 98 | if let podStatus = mockPodCommManager.podStatus { 99 | self.activeAlerts = podStatus.activeAlerts 100 | } 101 | } 102 | } 103 | 104 | extension PodCommError { 105 | static var simulatedErrors: [PodCommError?] { 106 | return [ 107 | nil, 108 | .unacknowledgedCommandPendingRetry, 109 | .notConnected, 110 | .failToConnect, 111 | .podNotAvailable, 112 | .activationError(.activationPhase1NotCompleted), 113 | .activationError(.podIsLumpOfCoal1Hour), 114 | .activationError(.podIsLumpOfCoal2Hours), 115 | .activationError(.moreThanOnePodAvailable), 116 | .bleCommunicationError, 117 | .bluetoothOff, 118 | .bluetoothUnauthorized, 119 | .internalError(.incompatibleProductId), 120 | .invalidAlertSetting, 121 | .invalidProgram, 122 | .invalidProgramStatus(nil), 123 | .messageSigningFailed, 124 | .nackReceived(.errorPodState), 125 | .noUnacknowledgedCommandToRetry, 126 | ] 127 | } 128 | } 129 | 130 | enum SimulatedPodAlerts: String, CaseIterable { 131 | case lowReservoirAlert 132 | case suspendInProgress 133 | case podExpireImminent 134 | case podExpiring 135 | case userPodExpiration 136 | 137 | var podAlerts: PodAlerts { 138 | switch self { 139 | case .lowReservoirAlert: 140 | return PodAlerts.lowReservoir 141 | case .suspendInProgress: 142 | return PodAlerts.suspendInProgress 143 | case .podExpireImminent: 144 | return PodAlerts.podExpireImminent 145 | case .podExpiring: 146 | return PodAlerts.podExpiring 147 | case .userPodExpiration: 148 | return PodAlerts.userPodExpiration 149 | } 150 | } 151 | } 152 | 153 | enum SimulatedPodAlarm: String, CaseIterable { 154 | case podExpired 155 | case emptyReservoir 156 | case occlusion 157 | case other 158 | 159 | var alarmCode: AlarmCode { 160 | switch self { 161 | case .podExpired: 162 | return .podExpired 163 | case .emptyReservoir: 164 | return .emptyReservoir 165 | case .occlusion: 166 | return .occlusion 167 | case .other: 168 | return .other 169 | } 170 | } 171 | } 172 | 173 | -------------------------------------------------------------------------------- /DashKitUI/View Models/PodLifeState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodLifeState.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/9/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | import LoopKitUI 13 | import DashKit 14 | import PodSDK 15 | 16 | enum PodLifeState { 17 | case podActivating 18 | // Time remaining 19 | case timeRemaining(TimeInterval) 20 | // Time since expiry 21 | case expired 22 | case podDeactivating 23 | case podAlarm(PodAlarm?, TimeInterval?) 24 | case systemError(SystemError, TimeInterval?) 25 | case noPod 26 | 27 | var progress: Double { 28 | switch self { 29 | case .timeRemaining(let timeRemaining): 30 | return max(0, min(1, (1 - (timeRemaining / Pod.lifetime)))) 31 | case .expired: 32 | return 1 33 | case .podAlarm(let alarm, let timestampOfAlarm): 34 | switch alarm?.alarmCode { 35 | case .podExpired: 36 | return 1 37 | default: 38 | return max(0, min(1, (timestampOfAlarm ?? Pod.lifetime) / Pod.lifetime)) 39 | } 40 | case .systemError(_, let timestampOfError): 41 | return max(0, min(1, (timestampOfError ?? Pod.lifetime) / Pod.lifetime)) 42 | case .podDeactivating: 43 | return 1 44 | case .noPod, .podActivating: 45 | return 0 46 | } 47 | } 48 | 49 | func progressColor(insulinTintColor: Color, guidanceColors: GuidanceColors) -> Color { 50 | switch self { 51 | case .expired: 52 | return guidanceColors.critical 53 | case .podAlarm(let alarm, _): 54 | switch alarm?.alarmCode { 55 | case .podExpired: 56 | return guidanceColors.critical 57 | default: 58 | return Color.secondary 59 | } 60 | case .timeRemaining(let timeRemaining): 61 | return timeRemaining <= Pod.timeRemainingWarningThreshold ? guidanceColors.warning : insulinTintColor 62 | default: 63 | return Color.secondary 64 | } 65 | } 66 | 67 | func labelColor(using guidanceColors: GuidanceColors) -> Color { 68 | switch self { 69 | case .podAlarm, .expired: 70 | return guidanceColors.critical 71 | default: 72 | return .secondary 73 | } 74 | } 75 | 76 | 77 | var localizedLabelText: String { 78 | switch self { 79 | case .podActivating: 80 | return LocalizedString("Unfinished Activation", comment: "Label for pod life state when pod not fully activated") 81 | case .timeRemaining: 82 | return LocalizedString("Pod expires in", comment: "Label for pod life state when time remaining") 83 | case .expired: 84 | return LocalizedString("Pod expired", comment: "Label for pod life state when within pod expiration window") 85 | case .podDeactivating: 86 | return LocalizedString("Unfinished deactivation", comment: "Label for pod life state when pod not fully deactivated") 87 | case .podAlarm(let alarm, _): 88 | if let alarm = alarm { 89 | return alarm.alarmDescription 90 | } else { 91 | return LocalizedString("Pod alarm", comment: "Label for pod life state when pod is in alarm state") 92 | } 93 | case .systemError: 94 | return LocalizedString("Pod system error", comment: "Label for pod life state when pod is in system error state") 95 | case .noPod: 96 | return LocalizedString("No Pod", comment: "Label for pod life state when no pod paired") 97 | } 98 | } 99 | 100 | var nextPodLifecycleAction: DashUIScreen { 101 | switch self { 102 | case .podActivating, .noPod: 103 | return .pairPod 104 | default: 105 | return .deactivate 106 | } 107 | } 108 | 109 | var nextPodLifecycleActionDescription: String { 110 | switch self { 111 | case .podActivating, .noPod: 112 | return LocalizedString("Pair Pod", comment: "Settings page link description when next lifecycle action is to pair new pod") 113 | case .podDeactivating: 114 | return LocalizedString("Finish deactivation", comment: "Settings page link description when next lifecycle action is to finish deactivation") 115 | default: 116 | return LocalizedString("Replace Pod", comment: "Settings page link description when next lifecycle action is to replace pod") 117 | } 118 | } 119 | 120 | var nextPodLifecycleActionColor: Color { 121 | switch self { 122 | case .podActivating, .noPod: 123 | return .accentColor 124 | default: 125 | return .red 126 | } 127 | } 128 | 129 | var isActive: Bool { 130 | switch self { 131 | case .expired, .timeRemaining: 132 | return true 133 | default: 134 | return false 135 | } 136 | } 137 | 138 | var allowsPumpManagerRemoval: Bool { 139 | if case .noPod = self { 140 | return true 141 | } 142 | return false 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /DashKitUI/View Models/RegistrationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegistrationViewModel.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/10/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import PodSDK 11 | import os.log 12 | import DashKit 13 | import SwiftUI 14 | import LoopKitUI 15 | import Combine 16 | 17 | public struct RegistrationError: Error, LocalizedError { 18 | let registrationStatus: RegistrationStatus 19 | 20 | /// A localized message describing what error occurred. 21 | public var errorDescription: String? { 22 | switch registrationStatus { 23 | case .alreadyRegistered: 24 | return LocalizedString("Already registered.", comment: "Description of registration error when already registered.") 25 | case .connectionTimeout: 26 | return LocalizedString("Connection timeout.", comment: "Description of registration error for connection timeout.") 27 | case .noDataConnection: 28 | return LocalizedString("No data connection.", comment: "Description of registration error for connection timeout.") 29 | default: 30 | return LocalizedString("Unknown Error.", comment: "Description of registration error when error is unknown.") 31 | } 32 | } 33 | 34 | // /// A localized message describing the reason for the failure. 35 | // var failureReason: String? { 36 | // } 37 | // 38 | // /// A localized message describing how one might recover from the failure. 39 | // var recoverySuggestion: String? { get } 40 | // 41 | // /// A localized message providing "help" text if the user requests help. 42 | // var helpAnchor: String? { get } 43 | } 44 | 45 | 46 | class RegistrationViewModel: ObservableObject, Identifiable { 47 | @Published var error: RegistrationError? 48 | 49 | @Published var progressState: ProgressIndicatorState 50 | 51 | @Published var isRegistered: Bool 52 | 53 | @Published var isRegistering: Bool 54 | 55 | private var registrationManager: PDMRegistrator 56 | 57 | private let log = OSLog(category: "RegistrationViewModel") 58 | 59 | var completion: (() -> Void)? 60 | 61 | init(registrationManager: PDMRegistrator) { 62 | self.registrationManager = registrationManager 63 | isRegistered = registrationManager.isRegistered() 64 | isRegistering = false 65 | progressState = .hidden 66 | } 67 | 68 | func registerTapped() { 69 | if isRegistered { 70 | completion?() 71 | } 72 | 73 | isRegistering = false 74 | error = nil 75 | self.progressState = .indeterminantProgress 76 | registrationManager.startRegistration { (status) in 77 | self.isRegistering = false 78 | switch status { 79 | case .registered, .alreadyRegistered: 80 | self.isRegistered = true 81 | self.progressState = .completed 82 | default: 83 | self.error = RegistrationError(registrationStatus: status) 84 | self.progressState = .hidden 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /DashKitUI/Views/AttachPodView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AttachPodView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/23/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct AttachPodView: View { 13 | 14 | enum Modal: Int, Identifiable { 15 | var id: Int { rawValue } 16 | 17 | case attachConfirmationModal 18 | case cancelModal 19 | } 20 | 21 | @Environment(\.verticalSizeClass) var verticalSizeClass 22 | 23 | var didConfirmAttachment: () -> Void 24 | var didRequestDeactivation: () -> Void 25 | 26 | @State private var activeModal: Modal? 27 | 28 | var body: some View { 29 | GuidePage(content: { 30 | VStack { 31 | LeadingImage("Pod") 32 | 33 | HStack { 34 | InstructionList(instructions: [ 35 | LocalizedString("Prepare site.", comment: "Label text for step one of attach pod instructions"), 36 | LocalizedString("Remove blue Pod needle cap and check cannula. Then remove paper backing.", comment: "Label text for step two of attach pod instructions"), 37 | LocalizedString("Check Pod, apply to site, then confirm pod attachment.", comment: "Label text for step three of attach pod instructions") 38 | ]) 39 | } 40 | .padding(.bottom, 8) 41 | } 42 | .accessibility(sortPriority: 1) 43 | }) { 44 | Button(action: { 45 | activeModal = .attachConfirmationModal 46 | }) { 47 | FrameworkLocalText("Continue", comment: "Action button title for attach pod view") 48 | .accessibility(identifier: "button_next_action") 49 | .actionButtonStyle(.primary) 50 | } 51 | .animation(nil) 52 | .padding() 53 | .background(Color(UIColor.systemBackground)) 54 | .zIndex(1) 55 | } 56 | .animation(.default) 57 | .alert(item: $activeModal, content: self.alert(for:)) 58 | .navigationBarTitle("Attach Pod", displayMode: .automatic) 59 | .navigationBarItems(trailing: cancelButton) 60 | .navigationBarBackButtonHidden(true) 61 | } 62 | 63 | var cancelButton: some View { 64 | Button(LocalizedString("Cancel", comment: "Cancel button text in navigation bar on pair pod UI")) { 65 | activeModal = .cancelModal 66 | } 67 | .accessibility(identifier: "button_cancel") 68 | } 69 | 70 | private func alert(for modal: Modal) -> Alert { 71 | switch modal { 72 | case .attachConfirmationModal: 73 | return confirmationModal 74 | case .cancelModal: 75 | return cancelPairingModal 76 | } 77 | } 78 | 79 | var confirmationModal: Alert { 80 | return Alert( 81 | title: FrameworkLocalText("Confirm Pod Attachment", comment: "Alert title for confirm pod attachment"), 82 | message: FrameworkLocalText("Please confirm that the Pod is securely attached to your body.\n\nThe cannula can be inserted only once with each Pod. Tap “Confirm” when Pod is attached.", comment: "Alert message body for confirm pod attachment"), 83 | primaryButton: .default(FrameworkLocalText("Confirm", comment: "Button title for confirm attachment option"), action: didConfirmAttachment), 84 | secondaryButton: .cancel() 85 | ) 86 | } 87 | 88 | var cancelPairingModal: Alert { 89 | return Alert( 90 | title: FrameworkLocalText("Are you sure you want to cancel Pod setup?", comment: "Alert title for cancel pairing modal"), 91 | message: FrameworkLocalText("If you cancel Pod setup, the current Pod will be deactivated and will be unusable.", comment: "Alert message body for confirm pod attachment"), 92 | primaryButton: .destructive(FrameworkLocalText("Yes, Deactivate Pod", comment: "Button title for confirm deactivation option"), action: didRequestDeactivation), 93 | secondaryButton: .default(FrameworkLocalText("No, Continue With Pod", comment: "Continue pairing button title of in pairing cancel modal")) 94 | ) 95 | } 96 | } 97 | 98 | struct AttachPodView_Previews: PreviewProvider { 99 | static var previews: some View { 100 | NavigationView { 101 | ZStack { 102 | Color(UIColor.secondarySystemBackground).edgesIgnoringSafeArea(.all) 103 | AttachPodView(didConfirmAttachment: {}, didRequestDeactivation: {}) 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /DashKitUI/Views/BasalStateView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasalStateView.swift 3 | // Naterade 4 | // 5 | // Created by Nathan Racklyeft on 5/12/16. 6 | // Copyright © 2016 Nathan Racklyeft. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | public struct BasalStateSwiftUIView: UIViewRepresentable { 13 | 14 | var netBasalPercent: Double 15 | 16 | public init(netBasalPercent: Double) { 17 | self.netBasalPercent = netBasalPercent 18 | } 19 | 20 | public func makeUIView(context: UIViewRepresentableContext) -> BasalStateView { 21 | let view = BasalStateView() 22 | view.netBasalPercent = netBasalPercent 23 | return view 24 | } 25 | 26 | public func updateUIView(_ uiView: BasalStateView, context: UIViewRepresentableContext) { 27 | uiView.netBasalPercent = netBasalPercent 28 | } 29 | } 30 | 31 | 32 | public final class BasalStateView: UIView { 33 | 34 | var netBasalPercent: Double = 0 { 35 | didSet { 36 | animateToPath(drawPath()) 37 | } 38 | } 39 | 40 | override public class var layerClass : AnyClass { 41 | return CAShapeLayer.self 42 | } 43 | 44 | private var shapeLayer: CAShapeLayer { 45 | return layer as! CAShapeLayer 46 | } 47 | 48 | override init(frame: CGRect) { 49 | super.init(frame: frame) 50 | 51 | shapeLayer.lineWidth = 2 52 | updateTintColor() 53 | } 54 | 55 | required public init?(coder aDecoder: NSCoder) { 56 | super.init(coder: aDecoder) 57 | 58 | shapeLayer.lineWidth = 2 59 | updateTintColor() 60 | } 61 | 62 | override public func layoutSubviews() { 63 | super.layoutSubviews() 64 | animateToPath(drawPath()) 65 | } 66 | 67 | public override func tintColorDidChange() { 68 | super.tintColorDidChange() 69 | updateTintColor() 70 | } 71 | 72 | private func updateTintColor() { 73 | shapeLayer.fillColor = tintColor.withAlphaComponent(0.5).cgColor 74 | shapeLayer.strokeColor = tintColor.cgColor 75 | } 76 | 77 | private func drawPath() -> CGPath { 78 | let startX = bounds.minX 79 | let endX = bounds.maxX 80 | let midY = bounds.midY 81 | 82 | let path = UIBezierPath() 83 | path.move(to: CGPoint(x: startX, y: midY)) 84 | 85 | let leftAnchor = startX + 1/6 * bounds.size.width 86 | let rightAnchor = startX + 5/6 * bounds.size.width 87 | 88 | let yAnchor = bounds.midY - CGFloat(netBasalPercent) * (bounds.size.height - shapeLayer.lineWidth) / 2 89 | 90 | path.addLine(to: CGPoint(x: leftAnchor, y: midY)) 91 | path.addLine(to: CGPoint(x: leftAnchor, y: yAnchor)) 92 | path.addLine(to: CGPoint(x: rightAnchor, y: yAnchor)) 93 | path.addLine(to: CGPoint(x: rightAnchor, y: midY)) 94 | path.addLine(to: CGPoint(x: endX, y: midY)) 95 | 96 | return path.cgPath 97 | } 98 | 99 | private static let animationKey = "com.loudnate.Naterade.shapePathAnimation" 100 | 101 | private func animateToPath(_ path: CGPath) { 102 | // Do not animate first draw 103 | if shapeLayer.path != nil { 104 | let animation = CABasicAnimation(keyPath: "path") 105 | animation.fromValue = shapeLayer.path ?? drawPath() 106 | animation.toValue = path 107 | animation.duration = 1 108 | animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) 109 | 110 | shapeLayer.add(animation, forKey: type(of: self).animationKey) 111 | } 112 | 113 | // Do not draw when size is zero 114 | if bounds != .zero { 115 | shapeLayer.path = path 116 | } 117 | } 118 | } 119 | 120 | struct BasalStateSwiftUIViewPreviewWrapper: View { 121 | @State private var percent: Double = 1 122 | 123 | var body: some View { 124 | VStack(spacing: 20) { 125 | BasalStateSwiftUIView(netBasalPercent: percent).frame(width: 100, height: 100, alignment: .center) 126 | Button(action: { 127 | self.percent = self.percent * -1 128 | }) { 129 | Text("Toggle sign") 130 | } 131 | Text("Percent = \(percent)") 132 | } 133 | } 134 | } 135 | struct BasalStateSwiftUIViewPreview: PreviewProvider { 136 | static var previews: some View { 137 | BasalStateSwiftUIViewPreviewWrapper() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /DashKitUI/Views/CheckInsertedCannulaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckInsertedCannulaView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/3/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct CheckInsertedCannulaView: View { 13 | 14 | 15 | @State private var cancelModalIsPresented: Bool = false 16 | 17 | private var didRequestDeactivation: () -> Void 18 | private var wasInsertedProperly: () -> Void 19 | 20 | init(didRequestDeactivation: @escaping () -> Void, wasInsertedProperly: @escaping () -> Void) { 21 | self.didRequestDeactivation = didRequestDeactivation 22 | self.wasInsertedProperly = wasInsertedProperly 23 | } 24 | 25 | var body: some View { 26 | GuidePage(content: { 27 | VStack { 28 | LeadingImage("Cannula Inserted") 29 | 30 | HStack { 31 | FrameworkLocalText("Is the cannula inserted properly?", comment: "Question to confirm the cannula is inserted properly").bold() 32 | Spacer() 33 | } 34 | HStack { 35 | FrameworkLocalText("The window on the top of the Pod should be colored pink when the cannula is properly inserted into the skin.", comment: "Description of proper cannula insertion") 36 | Spacer() 37 | }.padding(.vertical) 38 | } 39 | 40 | }) { 41 | VStack(spacing: 10) { 42 | Button(action: { 43 | self.wasInsertedProperly() 44 | }) { 45 | Text(LocalizedString("Yes", comment: "Button label for user to answer cannula was properly inserted")) 46 | .actionButtonStyle(.primary) 47 | } 48 | Button(action: { 49 | self.didRequestDeactivation() 50 | }) { 51 | Text(LocalizedString("No", comment: "Button label for user to answer cannula was not properly inserted")) 52 | .actionButtonStyle(.destructive) 53 | } 54 | }.padding() 55 | } 56 | .animation(.default) 57 | .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal } 58 | .navigationBarTitle("Check Cannula", displayMode: .automatic) 59 | .navigationBarItems(trailing: cancelButton) 60 | .navigationBarBackButtonHidden(true) 61 | } 62 | 63 | var cancelButton: some View { 64 | Button(LocalizedString("Cancel", comment: "Cancel button text in navigation bar on insert cannula screen")) { 65 | cancelModalIsPresented = true 66 | } 67 | .accessibility(identifier: "button_cancel") 68 | } 69 | 70 | var cancelPairingModal: Alert { 71 | return Alert( 72 | title: FrameworkLocalText("Are you sure you want to cancel Pod setup?", comment: "Alert title for cancel pairing modal"), 73 | message: FrameworkLocalText("If you cancel Pod setup, the current Pod will be deactivated and will be unusable.", comment: "Alert message body for confirm pod attachment"), 74 | primaryButton: .destructive(FrameworkLocalText("Yes, Deactivate Pod", comment: "Button title for confirm deactivation option"), action: { didRequestDeactivation() } ), 75 | secondaryButton: .default(FrameworkLocalText("No, Continue With Pod", comment: "Continue pairing button title of in pairing cancel modal")) 76 | ) 77 | } 78 | 79 | } 80 | 81 | struct CheckInsertedCannulaView_Previews: PreviewProvider { 82 | static var previews: some View { 83 | CheckInsertedCannulaView(didRequestDeactivation: {}, wasInsertedProperly: {} ) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /DashKitUI/Views/DeactivatePodView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeactivatePodView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/9/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct DeactivatePodView: View { 13 | 14 | @ObservedObject var viewModel: DeactivatePodViewModel 15 | 16 | @Environment(\.verticalSizeClass) var verticalSizeClass 17 | @Environment(\.guidanceColors) var guidanceColors 18 | 19 | @State private var removePodModalIsPresented: Bool = false 20 | 21 | var body: some View { 22 | GuidePage(content: { 23 | VStack { 24 | LeadingImage("Pod") 25 | 26 | HStack { 27 | Text(viewModel.instructionText) 28 | .fixedSize(horizontal: false, vertical: true) 29 | Spacer() 30 | } 31 | } 32 | .padding(.bottom, 8) 33 | }) { 34 | VStack { 35 | if viewModel.state.showProgressDetail { 36 | VStack { 37 | viewModel.error.map {ErrorView($0).accessibility(sortPriority: 0)} 38 | 39 | if viewModel.error == nil { 40 | VStack { 41 | ProgressIndicatorView(state: viewModel.state.progressState) 42 | if self.viewModel.state.isFinished { 43 | FrameworkLocalText("Deactivated", comment: "Label text showing pod is deactivated") 44 | .bold() 45 | .padding(.top) 46 | } 47 | } 48 | .padding(.bottom, 8) 49 | } 50 | 51 | } 52 | .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom))) 53 | } 54 | if viewModel.error != nil { 55 | Button(action: { 56 | if viewModel.podAttachedToBody { 57 | removePodModalIsPresented = true 58 | } else { 59 | viewModel.discardPod() 60 | } 61 | }) { 62 | FrameworkLocalText("Discard Pod", comment: "Text for discard pod button") 63 | .accessibility(identifier: "button_discard_pod_action") 64 | .actionButtonStyle(.destructive) 65 | } 66 | .disabled(viewModel.state.isProcessing) 67 | } 68 | Button(action: { 69 | viewModel.continueButtonTapped() 70 | }) { 71 | Text(viewModel.state.actionButtonDescription) 72 | .accessibility(identifier: "button_next_action") 73 | .accessibility(label: Text(viewModel.state.actionButtonAccessibilityLabel)) 74 | .actionButtonStyle(viewModel.state.actionButtonStyle) 75 | } 76 | .disabled(viewModel.state.isProcessing) 77 | } 78 | .padding() 79 | } 80 | .alert(isPresented: $removePodModalIsPresented) { removePodModal } 81 | .navigationBarTitle("Deactivate Pod", displayMode: .automatic) 82 | .navigationBarItems(trailing: 83 | Button("Cancel") { 84 | viewModel.didCancel?() 85 | } 86 | ) 87 | } 88 | 89 | var removePodModal: Alert { 90 | return Alert( 91 | title: FrameworkLocalText("Remove Pod from Body", comment: "Title for remove pod modal"), 92 | message: FrameworkLocalText("Your Pod may still be delivering Insulin.\nRemove it from your body, then tap “Continue.“", comment: "Alert message body for confirm pod attachment"), 93 | primaryButton: .cancel(), 94 | secondaryButton: .default(FrameworkLocalText("Continue", comment: "Title of button to continue discard"), action: { viewModel.discardPod() }) 95 | ) 96 | } 97 | } 98 | 99 | struct DeactivatePodView_Previews: PreviewProvider { 100 | static var previews: some View { 101 | DeactivatePodView(viewModel: DeactivatePodViewModel(podDeactivator: MockPodDeactivater(), podAttachedToBody: false)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /DashKitUI/Views/DeliveryUncertaintyRecoveryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeliveryUncertaintyRecoveryView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 8/17/20. 6 | // Copyright © 2020 LoopKit Authors. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | import DashKit 12 | 13 | struct DeliveryUncertaintyRecoveryView: View { 14 | 15 | let model: DeliveryUncertaintyRecoveryViewModel 16 | 17 | init(model: DeliveryUncertaintyRecoveryViewModel) { 18 | self.model = model 19 | } 20 | 21 | var body: some View { 22 | GuidePage(content: { 23 | Text(String(format: LocalizedString("%1$@ has been unable to communicate with the pod on your body since %2$@.\n\nWithout communication with the pod, the app cannot continue to send commands for insulin delivery or display accurate, recent information about your active insulin or the insulin being delivered by the Pod.\n\nMonitor your glucose closely for the next 6 or more hours, as there may or may not be insulin actively working in your body that %3$@ cannot display.", comment: "Format string for main text of delivery uncertainty recovery page. (1: app name)(2: date of command)(3: app name)"), self.model.appName, self.uncertaintyDateLocalizedString, self.model.appName)) 24 | .padding([.top, .bottom]) 25 | }) { 26 | VStack { 27 | Text(LocalizedString("Attemping to re-establish communication", comment: "Description string above progress indicator while attempting to re-establish communication from an unacknowledged command")).padding(.top) 28 | ProgressIndicatorView(state: .indeterminantProgress) 29 | Button(action: { 30 | self.model.podDeactivationChosen() 31 | }) { 32 | Text(LocalizedString("Deactivate Pod", comment: "Button title to deactive pod on uncertain program")) 33 | .actionButtonStyle() 34 | .padding() 35 | } 36 | } 37 | } 38 | .navigationBarTitle(Text(LocalizedString("Unable to Reach Pod", comment: "Title of delivery uncertainty recovery page")), displayMode: .large) 39 | .navigationBarItems(leading: backButton) 40 | } 41 | 42 | private var uncertaintyDateLocalizedString: String { 43 | DateFormatter.localizedString(from: model.uncertaintyStartedAt, dateStyle: .none, timeStyle: .short) 44 | } 45 | 46 | private var backButton: some View { 47 | Button(LocalizedString("Back", comment: "Back button text on DeliveryUncertaintyRecoveryView"), action: { 48 | self.model.onDismiss?() 49 | }) 50 | } 51 | } 52 | 53 | struct DeliveryUncertaintyRecoveryView_Previews: PreviewProvider { 54 | static var previews: some View { 55 | let model = DeliveryUncertaintyRecoveryViewModel(appName: "Test App", uncertaintyStartedAt: Date()) 56 | return DeliveryUncertaintyRecoveryView(model: model) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DashKitUI/Views/DesignElements/ErrorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ErrorView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/12/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct ErrorView: View { 13 | var error: LocalizedError 14 | 15 | var criticality: ErrorCriticality 16 | 17 | @Environment(\.guidanceColors) var guidanceColors 18 | 19 | public enum ErrorCriticality { 20 | case critical 21 | case normal 22 | 23 | func symbolColor(using guidanceColors: GuidanceColors) -> Color { 24 | switch self { 25 | case .critical: 26 | return guidanceColors.critical 27 | case .normal: 28 | return guidanceColors.warning 29 | } 30 | } 31 | } 32 | 33 | init(_ error: LocalizedError, errorClass: ErrorCriticality = .normal) { 34 | self.error = error 35 | self.criticality = errorClass 36 | } 37 | 38 | var body: some View { 39 | VStack(alignment: .leading, spacing: 15) { 40 | HStack { 41 | Image(systemName: "exclamationmark.triangle.fill") 42 | .foregroundColor(self.criticality.symbolColor(using: guidanceColors)) 43 | Text(self.error.errorDescription ?? "") 44 | .bold() 45 | .accessibility(identifier: "label_error_description") 46 | .fixedSize(horizontal: false, vertical: true) 47 | } 48 | .accessibilityElement(children: .ignore) 49 | .accessibility(label: FrameworkLocalText("Error", comment: "Accessibility label indicating an error occurred")) 50 | 51 | Text(self.error.recoverySuggestion ?? "") 52 | .foregroundColor(.secondary) 53 | .font(.footnote) 54 | .accessibility(identifier: "label_recovery_suggestion") 55 | .fixedSize(horizontal: false, vertical: true) 56 | } 57 | .padding(.bottom) 58 | .accessibilityElement(children: .combine) 59 | } 60 | } 61 | 62 | struct ErrorView_Previews: PreviewProvider { 63 | enum ErrorViewPreviewError: LocalizedError { 64 | case someError 65 | 66 | var localizedDescription: String { "It didn't work" } 67 | var recoverySuggestion: String { "Maybe try turning it on and off." } 68 | } 69 | 70 | static var previews: some View { 71 | ErrorView(ErrorViewPreviewError.someError) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /DashKitUI/Views/DesignElements/LeadingImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LeadingImage.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/12/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct LeadingImage: View { 12 | 13 | var name: String 14 | 15 | static let compactScreenImageHeight: CGFloat = 70 16 | static let regularScreenImageHeight: CGFloat = 150 17 | 18 | @Environment(\.verticalSizeClass) var verticalSizeClass 19 | 20 | init(_ name: String) { 21 | self.name = name 22 | } 23 | 24 | var body: some View { 25 | Image(frameworkImage: self.name, decorative: true) 26 | .resizable() 27 | .aspectRatio(contentMode: ContentMode.fit) 28 | .frame(height: self.verticalSizeClass == .compact ? LeadingImage.compactScreenImageHeight : LeadingImage.regularScreenImageHeight) 29 | .padding(.vertical, self.verticalSizeClass == .compact ? 0 : nil) 30 | } 31 | } 32 | 33 | struct LeadingImage_Previews: PreviewProvider { 34 | static var previews: some View { 35 | LeadingImage("Pod") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DashKitUI/Views/DesignElements/RoundedCard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedCard.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/9/21. 6 | // 7 | import SwiftUI 8 | 9 | fileprivate let inset: CGFloat = 16 10 | 11 | struct RoundedCardTitle: View { 12 | var title: String 13 | 14 | init(_ title: String) { 15 | self.title = title 16 | } 17 | 18 | var body: some View { 19 | Text(title) 20 | .font(.headline) 21 | .foregroundColor(.primary) 22 | } 23 | } 24 | 25 | struct RoundedCardFooter: View { 26 | var text: String 27 | 28 | init(_ text: String) { 29 | self.text = text 30 | } 31 | 32 | var body: some View { 33 | Text(text) 34 | .font(.caption) 35 | .fixedSize(horizontal: false, vertical: true) 36 | .foregroundColor(.secondary) 37 | } 38 | } 39 | 40 | public struct RoundedCardValueRow: View { 41 | var label: String 42 | var value: String 43 | var highlightValue: Bool 44 | var disclosure: Bool 45 | 46 | public init(label: String, value: String, highlightValue: Bool = false, disclosure: Bool = false) { 47 | self.label = label 48 | self.value = value 49 | self.highlightValue = highlightValue 50 | self.disclosure = disclosure 51 | } 52 | 53 | public var body: some View { 54 | HStack { 55 | Text(label) 56 | .fixedSize(horizontal: false, vertical: true) 57 | .foregroundColor(.primary) 58 | Spacer() 59 | Text(value) 60 | .fixedSize(horizontal: true, vertical: true) 61 | .foregroundColor(highlightValue ? .accentColor : .secondary) 62 | if disclosure { 63 | Image(systemName: "chevron.right") 64 | .imageScale(.small) 65 | .font(.headline) 66 | .foregroundColor(.secondary) 67 | .opacity(0.5) 68 | } 69 | } 70 | } 71 | } 72 | 73 | struct RoundedCard: View { 74 | var content: () -> Content? 75 | var alignment: HorizontalAlignment 76 | var title: String? 77 | var footer: String? 78 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 79 | 80 | init(title: String? = nil, footer: String? = nil, alignment: HorizontalAlignment = .leading, @ViewBuilder content: @escaping () -> Content? = { nil }) { 81 | self.content = content 82 | self.alignment = alignment 83 | self.title = title 84 | self.footer = footer 85 | } 86 | 87 | var body: some View { 88 | VStack(spacing: 10) { 89 | if let title = title { 90 | RoundedCardTitle(title) 91 | .frame(maxWidth: .infinity, alignment: Alignment(horizontal: .leading, vertical: .center)) 92 | .padding(.leading, titleInset) 93 | } 94 | 95 | if content() != nil { 96 | if isCompact { 97 | VStack(spacing: 0) { 98 | borderLine 99 | VStack(alignment: alignment, content: content) 100 | .frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .center)) 101 | .padding(inset) 102 | .background(Color(.secondarySystemGroupedBackground)) 103 | borderLine 104 | } 105 | } else { 106 | VStack(alignment: alignment, content: content) 107 | .frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .center)) 108 | .padding(.horizontal, inset) 109 | .padding(.vertical, 10) 110 | .background(Color(.secondarySystemGroupedBackground)) 111 | .clipShape(RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)) 112 | } 113 | } 114 | 115 | if let footer = footer { 116 | RoundedCardFooter(footer) 117 | .frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .center)) 118 | .padding(.horizontal, inset) 119 | } 120 | } 121 | } 122 | 123 | var borderLine: some View { 124 | Rectangle().fill(Color(.quaternaryLabel)) 125 | .frame(height: 0.5) 126 | } 127 | 128 | private var isCompact: Bool { 129 | return self.horizontalSizeClass == .compact 130 | } 131 | 132 | private var titleInset: CGFloat { 133 | return isCompact ? inset : 0 134 | } 135 | 136 | private var padding: CGFloat { 137 | return isCompact ? 0 : inset 138 | } 139 | 140 | private var cornerRadius: CGFloat { 141 | return isCompact ? 0 : 8 142 | } 143 | 144 | } 145 | 146 | struct RoundedCardScrollView: View { 147 | var content: () -> Content 148 | var title: String? 149 | @Environment(\.horizontalSizeClass) var horizontalSizeClass 150 | 151 | init(title: String? = nil, @ViewBuilder content: @escaping () -> Content) { 152 | self.title = title 153 | self.content = content 154 | } 155 | 156 | var body: some View { 157 | ScrollView { 158 | if let title = title { 159 | HStack { 160 | Text(title) 161 | .font(Font.largeTitle.weight(.bold)) 162 | .padding(.top) 163 | Spacer() 164 | } 165 | .padding([.leading, .trailing]) 166 | } 167 | VStack(alignment: .leading, spacing: 25, content: content) 168 | .padding(padding) 169 | } 170 | .background(Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all)) 171 | } 172 | 173 | private var padding: CGFloat { 174 | return self.horizontalSizeClass == .regular ? inset : 0 175 | } 176 | 177 | } 178 | -------------------------------------------------------------------------------- /DashKitUI/Views/ExpirationReminderPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpirationReminderPickerView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/17/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKit 11 | import LoopKitUI 12 | import HealthKit 13 | 14 | struct ExpirationReminderPickerView: View { 15 | 16 | static let expirationReminderHoursAllowed = 1...24 17 | 18 | var expirationReminderDefault: Binding 19 | 20 | var collapsible: Bool = true 21 | 22 | @State var showingHourPicker: Bool = false 23 | 24 | var expirationDefaultFormatter = QuantityFormatter(for: .hour()) 25 | 26 | var expirationDefaultString: String { 27 | return expirationValueString(expirationReminderDefault.wrappedValue) 28 | } 29 | 30 | func expirationValueString(_ value: Int) -> String { 31 | return expirationDefaultFormatter.string(from: HKQuantity(unit: .hour(), doubleValue: Double(value)), for: .hour())! 32 | } 33 | 34 | var body: some View { 35 | VStack { 36 | HStack { 37 | Text(LocalizedString("Expiration Reminder Default", comment: "Label text for expiration reminder default row")) 38 | Spacer() 39 | if collapsible { 40 | Button(expirationDefaultString) { 41 | withAnimation { 42 | showingHourPicker.toggle() 43 | } 44 | } 45 | } else { 46 | Text(expirationDefaultString) 47 | } 48 | } 49 | if showingHourPicker { 50 | Picker("", selection: expirationReminderDefault) { 51 | ForEach(Self.expirationReminderHoursAllowed, id: \.self) { value in 52 | Text(expirationValueString(value)) 53 | } 54 | } 55 | .pickerStyle(WheelPickerStyle()) 56 | .frame(width: 100) 57 | .clipped() 58 | } 59 | } 60 | } 61 | } 62 | 63 | struct ExpirationReminderPickerView_Previews: PreviewProvider { 64 | static var previews: some View { 65 | ExpirationReminderPickerView(expirationReminderDefault: .constant(2)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DashKitUI/Views/ExpirationReminderSetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ExpirationReminderSetupView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/17/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct ExpirationReminderSetupView: View { 13 | @State var expirationReminderDefault: Int = 2 14 | 15 | public var valueChanged: ((_ value: Int) -> Void)? 16 | public var continueButtonTapped: (() -> Void)? 17 | 18 | var body: some View { 19 | GuidePage(content: { 20 | VStack(alignment: .leading, spacing: 15) { 21 | Text(LocalizedString("The App notifies you in advance of Pod expiration.\n\nScroll to set the number of hours advance notice you would like to have.", comment: "Description text on ExpirationReminderSetupView")) 22 | Divider() 23 | ExpirationReminderPickerView(expirationReminderDefault: $expirationReminderDefault, collapsible: false, showingHourPicker: true) 24 | .onChange(of: expirationReminderDefault) { value in 25 | valueChanged?(value) 26 | } 27 | } 28 | .padding(.vertical, 8) 29 | }) { 30 | VStack { 31 | Button(action: { 32 | continueButtonTapped?() 33 | }) { 34 | Text(LocalizedString("Next", comment: "Text of continue button on ExpirationReminderSetupView")) 35 | .actionButtonStyle(.primary) 36 | } 37 | } 38 | .padding() 39 | } 40 | .navigationBarTitle("Expiration Reminder", displayMode: .automatic) 41 | } 42 | } 43 | 44 | struct ExpirationReminderSetupView_Previews: PreviewProvider { 45 | static var previews: some View { 46 | ExpirationReminderSetupView() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DashKitUI/Views/InsertCannulaView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsertCannulaView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/5/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct InsertCannulaView: View { 13 | 14 | @ObservedObject var viewModel: InsertCannulaViewModel 15 | 16 | @Environment(\.verticalSizeClass) var verticalSizeClass 17 | 18 | @State private var cancelModalIsPresented: Bool = false 19 | 20 | var body: some View { 21 | GuidePage(content: { 22 | VStack { 23 | LeadingImage("Pod") 24 | 25 | HStack { 26 | InstructionList(instructions: [ 27 | LocalizedString("Tap below to start cannula insertion.", comment: "Label text for step one of insert cannula instructions"), 28 | LocalizedString("Wait until insertion is completed.", comment: "Label text for step two of insert cannula instructions"), 29 | ]) 30 | .disabled(viewModel.state.instructionsDisabled) 31 | 32 | } 33 | .padding(.bottom, 8) 34 | } 35 | .accessibility(sortPriority: 1) 36 | }) { 37 | VStack { 38 | if self.viewModel.state.showProgressDetail { 39 | self.viewModel.error.map { 40 | ErrorView($0, errorClass: $0.recoverable ? .normal : .critical) 41 | .accessibility(sortPriority: 0) 42 | } 43 | 44 | if self.viewModel.error == nil { 45 | VStack { 46 | ProgressIndicatorView(state: self.viewModel.state.progressState) 47 | if self.viewModel.state.isFinished { 48 | FrameworkLocalText("Inserted", comment: "Label text indicating insertion finished.") 49 | .bold() 50 | .padding(.top) 51 | } 52 | } 53 | .padding(.bottom, 8) 54 | } 55 | } 56 | if self.viewModel.error != nil { 57 | Button(action: { 58 | self.viewModel.didRequestDeactivation?() 59 | }) { 60 | Text(LocalizedString("Deactivate Pod", comment: "Button text for deactivate pod button")) 61 | .accessibility(identifier: "button_deactivate_pod") 62 | .actionButtonStyle(.destructive) 63 | } 64 | .disabled(self.viewModel.state.isProcessing) 65 | } 66 | 67 | if (self.viewModel.error == nil || self.viewModel.error?.recoverable == true) { 68 | Button(action: { 69 | self.viewModel.continueButtonTapped() 70 | }) { 71 | Text(self.viewModel.state.nextActionButtonDescription) 72 | .accessibility(identifier: "button_next_action") 73 | .accessibility(label: Text(self.viewModel.state.actionButtonAccessibilityLabel)) 74 | .actionButtonStyle(.primary) 75 | } 76 | .disabled(self.viewModel.state.isProcessing) 77 | .animation(nil) 78 | .zIndex(1) 79 | } 80 | } 81 | .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom))) 82 | .padding() 83 | } 84 | .animation(.default) 85 | .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal } 86 | .navigationBarTitle("Insert Cannula", displayMode: .automatic) 87 | .navigationBarBackButtonHidden(true) 88 | .navigationBarItems(trailing: cancelButton) 89 | } 90 | 91 | var cancelButton: some View { 92 | Button(LocalizedString("Cancel", comment: "Cancel button text in navigation bar on insert cannula screen")) { 93 | cancelModalIsPresented = true 94 | } 95 | .accessibility(identifier: "button_cancel") 96 | } 97 | 98 | var cancelPairingModal: Alert { 99 | return Alert( 100 | title: FrameworkLocalText("Are you sure you want to cancel Pod setup?", comment: "Alert title for cancel pairing modal"), 101 | message: FrameworkLocalText("If you cancel Pod setup, the current Pod will be deactivated and will be unusable.", comment: "Alert message body for confirm pod attachment"), 102 | primaryButton: .destructive(FrameworkLocalText("Yes, Deactivate Pod", comment: "Button title for confirm deactivation option"), action: { viewModel.didRequestDeactivation?() } ), 103 | secondaryButton: .default(FrameworkLocalText("No, Continue With Pod", comment: "Continue pairing button title of in pairing cancel modal")) 104 | ) 105 | } 106 | 107 | } 108 | 109 | struct InsertCannulaView_Previews: PreviewProvider { 110 | static var previews: some View { 111 | NavigationView { 112 | ZStack { 113 | Color(UIColor.secondarySystemBackground).edgesIgnoringSafeArea(.all) 114 | InsertCannulaView(viewModel: InsertCannulaViewModel(cannulaInserter: MockCannulaInserter())) 115 | } 116 | } 117 | //.environment(\.colorScheme, .dark) 118 | //.environment(\.sizeCategory, .accessibilityLarge) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /DashKitUI/Views/LowReservoirReminderSetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LowReservoirReminderSetupView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/17/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | import LoopKit 12 | import HealthKit 13 | import DashKit 14 | 15 | struct LowReservoirReminderSetupView: View { 16 | 17 | @State var lowReservoirReminderValue: Int 18 | 19 | public var valueChanged: ((_ value: Int) -> Void)? 20 | public var continueButtonTapped: (() -> Void)? 21 | 22 | var insulinQuantityFormatter = QuantityFormatter(for: .internationalUnit()) 23 | 24 | func formatValue(_ value: Int) -> String { 25 | return insulinQuantityFormatter.string(from: HKQuantity(unit: .internationalUnit(), doubleValue: Double(value)), for: .internationalUnit()) ?? "" 26 | } 27 | 28 | var body: some View { 29 | GuidePage(content: { 30 | VStack(alignment: .leading, spacing: 15) { 31 | Text(LocalizedString("The App notifies you when the amount of insulin in the Pod reaches this level (50-10 U).\n\nScroll to set the number of units at which you would like to be reminded.", comment: "Description text on LowReservoirReminderSetupView")) 32 | Divider() 33 | HStack { 34 | Text(LocalizedString("Low Reservoir", comment: "Label text for low reservoir value row")) 35 | Spacer() 36 | Text(formatValue(lowReservoirReminderValue)) 37 | } 38 | picker 39 | } 40 | .padding(.vertical, 8) 41 | }) { 42 | VStack { 43 | Button(action: { 44 | continueButtonTapped?() 45 | }) { 46 | Text(LocalizedString("Next", comment: "Text of continue button on ExpirationReminderSetupView")) 47 | .actionButtonStyle(.primary) 48 | } 49 | } 50 | .padding() 51 | } 52 | .navigationBarTitle("Low Reservoir", displayMode: .automatic) 53 | } 54 | 55 | private var picker: some View { 56 | Picker("", selection: $lowReservoirReminderValue) { 57 | ForEach(Pod.allowedLowReservoirReminderValues, id: \.self) { value in 58 | Text(formatValue(value)) 59 | } 60 | }.pickerStyle(WheelPickerStyle()) 61 | .onChange(of: lowReservoirReminderValue) { value in 62 | valueChanged?(value) 63 | } 64 | 65 | } 66 | 67 | } 68 | struct LowReservoirReminderSetupView_Previews: PreviewProvider { 69 | static var previews: some View { 70 | LowReservoirReminderSetupView(lowReservoirReminderValue: 10) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DashKitUI/Views/PairPodView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PairPodView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/5/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct PairPodView: View { 13 | 14 | @ObservedObject var viewModel: PairPodViewModel 15 | 16 | @State private var cancelModalIsPresented: Bool = false 17 | 18 | var body: some View { 19 | GuidePage(content: { 20 | VStack { 21 | LeadingImage("Pod") 22 | 23 | HStack { 24 | InstructionList(instructions: [ 25 | LocalizedString("Fill a new pod with U-100 Insulin (leave blue Pod needle cap on).", comment: "Label text for step 1 of pair pod instructions"), 26 | LocalizedString("Listen for 2 beeps.", comment: "Label text for step 2 of pair pod instructions") 27 | ]) 28 | .disabled(viewModel.state.instructionsDisabled) 29 | } 30 | .padding(.bottom, 8) 31 | } 32 | .accessibility(sortPriority: 1) 33 | }) { 34 | VStack { 35 | if self.viewModel.state.showProgressDetail { 36 | self.viewModel.error.map { 37 | ErrorView($0, errorClass: $0.recoverable ? .normal : .critical) 38 | .accessibility(sortPriority: 0) 39 | } 40 | 41 | if self.viewModel.error == nil { 42 | VStack { 43 | ProgressIndicatorView(state: self.viewModel.state.progressState) 44 | if self.viewModel.state.isFinished { 45 | FrameworkLocalText("Paired", comment: "Label text indicating pairing finished.") 46 | .bold() 47 | .padding(.top) 48 | } 49 | } 50 | .padding(.bottom, 8) 51 | } 52 | } 53 | if self.viewModel.error != nil && self.viewModel.podIsActivated { 54 | Button(action: { 55 | self.viewModel.didRequestDeactivation?() 56 | }) { 57 | Text(LocalizedString("Deactivate Pod", comment: "Button text for deactivate pod button")) 58 | .accessibility(identifier: "button_deactivate_pod") 59 | .actionButtonStyle(.destructive) 60 | } 61 | .disabled(self.viewModel.state.isProcessing) 62 | } 63 | 64 | if (self.viewModel.error == nil || self.viewModel.error?.recoverable == true) { 65 | Button(action: { 66 | self.viewModel.continueButtonTapped() 67 | }) { 68 | Text(self.viewModel.state.nextActionButtonDescription) 69 | .accessibility(identifier: "button_next_action") 70 | .accessibility(label: Text(self.viewModel.state.actionButtonAccessibilityLabel)) 71 | .actionButtonStyle(.primary) 72 | } 73 | .disabled(self.viewModel.state.isProcessing) 74 | .animation(nil) 75 | .zIndex(1) 76 | } 77 | } 78 | .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom))) 79 | .padding() 80 | } 81 | .animation(.default) 82 | .alert(isPresented: $cancelModalIsPresented) { cancelPairingModal } 83 | .navigationBarTitle("Pair Pod", displayMode: .automatic) 84 | .navigationBarBackButtonHidden(self.viewModel.backButtonHidden) 85 | .navigationBarItems(trailing: self.viewModel.state.navBarVisible ? cancelButton : nil) 86 | } 87 | 88 | var cancelButton: some View { 89 | Button(LocalizedString("Cancel", comment: "Cancel button text in navigation bar on pair pod UI")) { 90 | if viewModel.podIsActivated { 91 | cancelModalIsPresented = true 92 | } else { 93 | viewModel.didCancelSetup?() 94 | } 95 | } 96 | .accessibility(identifier: "button_cancel") 97 | .disabled(self.viewModel.state.isProcessing) 98 | } 99 | 100 | var cancelPairingModal: Alert { 101 | return Alert( 102 | title: FrameworkLocalText("Are you sure you want to cancel Pod setup?", comment: "Alert title for cancel pairing modal"), 103 | message: FrameworkLocalText("If you cancel Pod setup, the current Pod will be deactivated and will be unusable.", comment: "Alert message body for confirm pod attachment"), 104 | primaryButton: .destructive(FrameworkLocalText("Yes, Deactivate Pod", comment: "Button title for confirm deactivation option"), action: { viewModel.didRequestDeactivation?() }), 105 | secondaryButton: .default(FrameworkLocalText("No, Continue With Pod", comment: "Continue pairing button title of in pairing cancel modal")) 106 | ) 107 | } 108 | 109 | } 110 | 111 | struct PairPodView_Previews: PreviewProvider { 112 | 113 | static var previews: some View { 114 | NavigationView { 115 | PairPodView(viewModel: PairPodViewModel(podPairer: MockPodPairer(), navigator: MockNavigator())) 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /DashKitUI/Views/PodDetailsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodDetailsView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 4/14/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | import DashKit 12 | 13 | struct PodDetailsView: View { 14 | 15 | var podVersion: PodVersionProtocol 16 | 17 | private func row(_ label: String, value: String) -> some View { 18 | HStack { 19 | Text(label) 20 | Spacer() 21 | Text(value) 22 | } 23 | } 24 | 25 | var body: some View { 26 | List { 27 | row(LocalizedString("Lot Number", comment: "description label for lot number pod details row"), value: String(describing: podVersion.lotNumber)) 28 | row(LocalizedString("Sequence Number", comment: "description label for sequence number pod details row"), value: String(describing: podVersion.sequenceNumber)) 29 | row(LocalizedString("Firmware Version", comment: "description label for firmware version pod details row"), value: podVersion.firmwareVersion) 30 | } 31 | .navigationBarTitle(Text(LocalizedString("Device Details", comment: "title for device details page")), displayMode: .automatic) 32 | } 33 | } 34 | 35 | struct PodDetailsView_Previews: PreviewProvider { 36 | static var previews: some View { 37 | PodDetailsView(podVersion: MockPodVersion(lotNumber: 1, sequenceNumber: 1, majorVersion: 1, minorVersion: 1, interimVersion: 1, bleMajorVersion: 1, bleMinorVersion: 1, bleInterimVersion: 1)) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DashKitUI/Views/PodSetupView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PodSetupView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/17/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct PodSetupView: View { 13 | @Environment(\.dismissAction) private var dismiss 14 | 15 | private struct AlertIdentifier: Identifiable { 16 | enum Choice { 17 | case skipOnboarding 18 | } 19 | var id: Choice 20 | } 21 | @State private var alertIdentifier: AlertIdentifier? 22 | 23 | let nextAction: () -> Void 24 | let allowDebugFeatures: Bool 25 | let skipOnboarding: () -> Void 26 | 27 | var body: some View { 28 | VStack(alignment: .leading) { 29 | close 30 | ScrollView { 31 | content 32 | } 33 | Spacer() 34 | continueButton 35 | .padding(.bottom) 36 | } 37 | .padding(.horizontal) 38 | .navigationBarHidden(true) 39 | .alert(item: $alertIdentifier) { alert in 40 | switch alert.id { 41 | case .skipOnboarding: 42 | return skipOnboardingAlert 43 | } 44 | } 45 | } 46 | 47 | @ViewBuilder 48 | private var close: some View { 49 | HStack { 50 | Spacer() 51 | closeButton 52 | } 53 | .padding(.top) 54 | } 55 | 56 | @ViewBuilder 57 | private var content: some View { 58 | VStack(alignment: .leading, spacing: 2) { 59 | title 60 | .padding(.top, 5) 61 | .onLongPressGesture(minimumDuration: 2) { 62 | didLongPressOnTitle() 63 | } 64 | Divider() 65 | bodyText 66 | .foregroundColor(.secondary) 67 | .padding(.top) 68 | } 69 | } 70 | 71 | @ViewBuilder 72 | private var title: some View { 73 | Text(LocalizedString("Pod Setup", comment: "Title for PodSetupView")) 74 | .font(.largeTitle) 75 | .bold() 76 | .padding(.vertical) 77 | } 78 | 79 | @ViewBuilder 80 | private var bodyText: some View { 81 | Text(LocalizedString("You will now begin the process of configuring your reminders, filling your Pod with insulin, pairing to your device and placing it on your body.", comment: "bodyText for PodSetupView")) 82 | } 83 | 84 | private var closeButton: some View { 85 | Button(LocalizedString("Close", comment: "Close button title"), action: { 86 | self.dismiss() 87 | }) 88 | } 89 | 90 | private var continueButton: some View { 91 | Button(LocalizedString("Continue", comment: "Text for continue button on PodSetupView"), action: nextAction) 92 | .buttonStyle(ActionButtonStyle()) 93 | } 94 | 95 | private var skipOnboardingAlert: Alert { 96 | Alert(title: Text("Skip Omnipod Onboarding?"), 97 | message: Text("Are you sure you want to skip Omnipod Onboarding?"), 98 | primaryButton: .cancel(), 99 | secondaryButton: .destructive(Text("Yes"), action: skipOnboarding)) 100 | } 101 | 102 | private func didLongPressOnTitle() { 103 | if allowDebugFeatures { 104 | alertIdentifier = AlertIdentifier(id: .skipOnboarding) 105 | } 106 | } 107 | 108 | } 109 | 110 | struct PodSetupView_Previews: PreviewProvider { 111 | static var previews: some View { 112 | PodSetupView(nextAction: {}, allowDebugFeatures: true, skipOnboarding: {}) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /DashKitUI/Views/RegisterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegisterView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/7/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct RegisterView: View { 13 | @ObservedObject var viewModel: RegistrationViewModel 14 | 15 | @Environment(\.verticalSizeClass) var verticalSizeClass 16 | 17 | init(viewModel: RegistrationViewModel) { 18 | self.viewModel = viewModel 19 | } 20 | 21 | var body: some View { 22 | GuidePage(content: { 23 | LeadingImage("No Pod") 24 | 25 | VStack(alignment: .leading, spacing: 8) { 26 | FrameworkLocalText("This device must be registered as a PDM. Registration requires internet connectivity and is only required once per device.", comment: "Label text when phone not registered as PDM.") 27 | .fixedSize(horizontal: false, vertical: true) 28 | } 29 | 30 | self.viewModel.error.map {ErrorView($0).accessibility(sortPriority: 0)} 31 | 32 | ProgressIndicatorView(state: self.viewModel.progressState) 33 | }) { 34 | Button(action: { 35 | self.viewModel.registerTapped() 36 | }) { 37 | Text(self.viewModel.isRegistered ? "Continue" : "Register") 38 | .actionButtonStyle() 39 | } 40 | .padding() 41 | .disabled(self.viewModel.isRegistering) 42 | } 43 | .padding() 44 | .animation(.default) 45 | .navigationBarTitle("Register Device", displayMode: .automatic) 46 | } 47 | } 48 | 49 | struct RegisterView_Previews: PreviewProvider { 50 | 51 | static let manager = MockRegistrationManager() 52 | 53 | static var previews: some View { 54 | NavigationView { 55 | RegisterView(viewModel: RegistrationViewModel(registrationManager: manager)) 56 | } 57 | //.environment(\.colorScheme, .dark) 58 | //.environment(\.sizeCategory, .accessibilityLarge) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DashKitUI/Views/SetupCompleteView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupCompleteView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 3/2/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | import PodSDK 12 | import DashKit 13 | 14 | struct SetupCompleteView: View { 15 | 16 | @Environment(\.verticalSizeClass) var verticalSizeClass 17 | @Environment(\.appName) private var appName 18 | 19 | 20 | private var onSaveScheduledExpirationReminder: ((_ selectedDate: Date, _ completion: @escaping (_ error: Error?) -> Void) -> Void)? 21 | private var didFinish: () -> Void 22 | private var didRequestDeactivation: () -> Void 23 | private var dateFormatter: DateFormatter 24 | 25 | @State private var scheduledReminderDate: Date 26 | 27 | @State private var scheduleReminderDateEditViewIsShown: Bool = false 28 | 29 | var allowedDates: [Date] 30 | 31 | init(scheduledReminderDate: Date, dateFormatter: DateFormatter, allowedDates: [Date], onSaveScheduledExpirationReminder: ((_ selectedDate: Date, _ completion: @escaping (_ error: Error?) -> Void) -> Void)?, didFinish: @escaping () -> Void, didRequestDeactivation: @escaping () -> Void) 32 | { 33 | self._scheduledReminderDate = State(initialValue: scheduledReminderDate) 34 | self.dateFormatter = dateFormatter 35 | self.allowedDates = allowedDates 36 | self.onSaveScheduledExpirationReminder = onSaveScheduledExpirationReminder 37 | self.didFinish = didFinish 38 | self.didRequestDeactivation = didRequestDeactivation 39 | } 40 | 41 | var body: some View { 42 | GuidePage(content: { 43 | VStack { 44 | LeadingImage("Pod") 45 | Text(String(format: LocalizedString("Your Pod is ready for use.\n\n%1$@ will remind you to change your pod before it expires. You can change this to a time convenient for you.", comment: "Format string for instructions for setup complete view. (1: app name)"), appName)) 46 | .fixedSize(horizontal: false, vertical: true) 47 | Divider() 48 | VStack(alignment: .leading) { 49 | Text("Scheduled Reminder") 50 | Divider() 51 | NavigationLink( 52 | destination: ScheduledExpirationReminderEditView( 53 | scheduledExpirationReminderDate: scheduledReminderDate, 54 | allowedDates: allowedDates, 55 | dateFormatter: dateFormatter, 56 | onSave: { (newDate, completion) in 57 | onSaveScheduledExpirationReminder?(newDate) { (error) in 58 | if error == nil { 59 | scheduledReminderDate = newDate 60 | } 61 | completion(error) 62 | } 63 | }, 64 | onFinish: { scheduleReminderDateEditViewIsShown = false }), 65 | isActive: $scheduleReminderDateEditViewIsShown) 66 | { 67 | RoundedCardValueRow( 68 | label: LocalizedString("Time", comment: "Label for expiration reminder row"), 69 | value: dateFormatter.string(from: scheduledReminderDate), 70 | highlightValue: false 71 | ) 72 | } 73 | } 74 | } 75 | .padding(.bottom, 8) 76 | .accessibility(sortPriority: 1) 77 | }) { 78 | Button(action: { 79 | didFinish() 80 | }) { 81 | Text(LocalizedString("Finish Setup", comment: "Action button title to continue at Setup Complete")) 82 | .actionButtonStyle(.primary) 83 | } 84 | .padding() 85 | .background(Color(UIColor.systemBackground)) 86 | .zIndex(1) 87 | } 88 | .animation(.default) 89 | .navigationBarTitle("Setup Complete", displayMode: .automatic) 90 | } 91 | } 92 | struct SetupCompleteView_Previews: PreviewProvider { 93 | static var previews: some View { 94 | SetupCompleteView( 95 | scheduledReminderDate: Date(), 96 | dateFormatter: DateFormatter(), 97 | allowedDates: [Date()], 98 | onSaveScheduledExpirationReminder: { (date, completion) in 99 | }, 100 | didFinish: { 101 | }, 102 | didRequestDeactivation: { 103 | } 104 | ) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /DashKitUI/Views/SetupGuideView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupGuideView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 2/7/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct SetupGuideView: View { 12 | var body: some View { 13 | NavigationView { 14 | ZStack { 15 | Color(UIColor.secondarySystemBackground).edgesIgnoringSafeArea(.all) 16 | PairPodSetupView() 17 | } 18 | .navigationBarTitle("Insert Cannula", displayMode: .automatic) 19 | } 20 | } 21 | } 22 | 23 | struct SetupGuideView_Previews: PreviewProvider { 24 | static var previews: some View { 25 | SetupGuideView() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DashKitUI/Views/TimeView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 5/10/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct TimeView: View { 12 | 13 | let timeZone: TimeZone 14 | 15 | private let shortTimeFormatter: DateFormatter = { 16 | let formatter = DateFormatter() 17 | formatter.dateStyle = .none 18 | formatter.timeStyle = .short 19 | return formatter 20 | }() 21 | 22 | @State var currentDate = Date() 23 | let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() 24 | 25 | var timeZoneString: String { 26 | shortTimeFormatter.timeZone = timeZone 27 | return shortTimeFormatter.string(from: currentDate) 28 | } 29 | 30 | var body: some View { 31 | Text(timeZoneString).onReceive(timer) { input in 32 | currentDate = input 33 | } 34 | } 35 | } 36 | 37 | struct TimeView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | TimeView(timeZone: .current) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DashKitUI/Views/UncertaintyRecoveredView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UncertaintyRecoveredView.swift 3 | // DashKitUI 4 | // 5 | // Created by Pete Schwamb on 8/25/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import LoopKitUI 11 | 12 | struct UncertaintyRecoveredView: View { 13 | var appName: String 14 | 15 | var didFinish: (() -> Void)? 16 | 17 | var body: some View { 18 | GuidePage(content: { 19 | Text("\(self.appName) has recovered communication with the pod on your body.\n\nInsulin delivery records have been updated and should match what has actually been delivered.\n\nYou may continue to use \(self.appName) normally now.") 20 | .padding([.top, .bottom]) 21 | }) { 22 | VStack { 23 | Button(action: { 24 | self.didFinish?() 25 | }) { 26 | Text(LocalizedString("Continue", comment: "Button title to continue")) 27 | .actionButtonStyle() 28 | .padding() 29 | } 30 | } 31 | } 32 | .navigationBarTitle(Text("Comms Recovered"), displayMode: .large) 33 | .navigationBarBackButtonHidden(true) 34 | } 35 | } 36 | 37 | struct UncertaintyRecoveredView_Previews: PreviewProvider { 38 | static var previews: some View { 39 | UncertaintyRecoveredView(appName: "Test App") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DashKitUITests/DashKitUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DashKitUITests.swift 3 | // DashKitUITests 4 | // 5 | // Created by Pete Schwamb on 3/30/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class DashKitUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDown() { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() { 27 | // This is an example of a performance test case. 28 | measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /DashKitUITests/DeactivatePodViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeactivatePodViewModelTests.swift 3 | // DashKitUITests 4 | // 5 | // Created by Pete Schwamb on 4/2/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DashKit 11 | import PodSDK 12 | @testable import DashKitUI 13 | 14 | class DeactivatePodViewModelTests: XCTestCase { 15 | 16 | var deactivationExpectation: XCTestExpectation? 17 | var discardPodExpectation: XCTestExpectation? 18 | 19 | var lastNavigation: DashUIScreen? 20 | var didNavigateExpectation: XCTestExpectation? 21 | 22 | var deactivationError: PodCommError? 23 | var discardError: PodCommError? 24 | 25 | 26 | override func setUp() { 27 | // Put setup code here. This method is called before the invocation of each test method in the class. 28 | } 29 | 30 | override func tearDown() { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | deactivationError = nil 33 | } 34 | 35 | func testContinueShouldAttemptDeactivation() { 36 | let viewModel = DeactivatePodViewModel(podDeactivator: self, podAttachedToBody: true) 37 | 38 | deactivationExpectation = expectation(description: "Deactivate Pod") 39 | 40 | viewModel.continueButtonTapped() 41 | 42 | waitForExpectations(timeout: 0.3, handler: nil) 43 | } 44 | 45 | func testContinueAfterRecoverableErrorShouldRetry() { 46 | let viewModel = DeactivatePodViewModel(podDeactivator: self, podAttachedToBody: true) 47 | 48 | deactivationError = .bleCommunicationError 49 | 50 | deactivationExpectation = expectation(description: "Deactivate Pod") 51 | viewModel.continueButtonTapped() 52 | 53 | waitForExpectations(timeout: 0.3, handler: nil) 54 | 55 | deactivationExpectation = expectation(description: "Deactivate Pod Retry") 56 | viewModel.continueButtonTapped() 57 | 58 | waitForExpectations(timeout: 0.3, handler: nil) 59 | XCTAssertNil(lastNavigation) 60 | } 61 | 62 | func testContinueAfterSuccessfulDeactivationShouldCallDidFinish() { 63 | let viewModel = DeactivatePodViewModel(podDeactivator: self, podAttachedToBody: true) 64 | 65 | deactivationExpectation = expectation(description: "Deactivate Pod") 66 | viewModel.continueButtonTapped() 67 | 68 | waitForExpectations(timeout: 0.3, handler: nil) 69 | 70 | let didFinishExpectation = expectation(description: "Pod did deactivate") 71 | 72 | viewModel.didFinish = { 73 | didFinishExpectation.fulfill() 74 | } 75 | 76 | viewModel.continueButtonTapped() 77 | 78 | waitForExpectations(timeout: 0.3, handler: nil) 79 | } 80 | 81 | func testTappingDiscardShouldDiscardPodAndFinish() { 82 | let viewModel = DeactivatePodViewModel(podDeactivator: self, podAttachedToBody: true) 83 | 84 | discardPodExpectation = expectation(description: "Discard Pod") 85 | viewModel.discardPod() 86 | 87 | waitForExpectations(timeout: 0.3, handler: nil) 88 | 89 | let didFinishExpectation = expectation(description: "Pod did deactivate") 90 | 91 | viewModel.didFinish = { 92 | didFinishExpectation.fulfill() 93 | } 94 | 95 | viewModel.continueButtonTapped() 96 | 97 | waitForExpectations(timeout: 0.3, handler: nil) 98 | } 99 | 100 | func testTappingDiscardAfterErrorClearsShouldDiscardPodAndFinish() { 101 | let viewModel = DeactivatePodViewModel(podDeactivator: self, podAttachedToBody: true) 102 | 103 | discardError = .bleCommunicationError 104 | 105 | discardPodExpectation = expectation(description: "Discard Pod") 106 | viewModel.discardPod() 107 | 108 | waitForExpectations(timeout: 0.3, handler: nil) 109 | 110 | discardPodExpectation = expectation(description: "Discard Pod Retry ") 111 | 112 | let didFinishExpectation = expectation(description: "Interface did finish") 113 | 114 | viewModel.didFinish = { 115 | didFinishExpectation.fulfill() 116 | } 117 | 118 | discardError = nil 119 | 120 | viewModel.discardPod() 121 | 122 | waitForExpectations(timeout: 0.3, handler: nil) 123 | } 124 | 125 | 126 | } 127 | 128 | extension DeactivatePodViewModelTests: DashUINavigator { 129 | func navigateTo(_ screen: DashUIScreen) { 130 | lastNavigation = screen 131 | didNavigateExpectation?.fulfill() 132 | } 133 | } 134 | 135 | extension DeactivatePodViewModelTests: PodDeactivater { 136 | func deactivatePod(completion: @escaping (PodCommResult) -> ()) { 137 | if let deactivationError = deactivationError { 138 | completion(.failure(deactivationError)) 139 | } else { 140 | completion(.success(MockPodStatus.normal)) 141 | } 142 | deactivationExpectation?.fulfill() 143 | } 144 | 145 | func discardPod(completion: @escaping (PodCommResult) -> ()) { 146 | if let discardError = discardError { 147 | completion(.failure(discardError)) 148 | } else { 149 | completion(.success(true)) 150 | } 151 | discardPodExpectation?.fulfill() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /DashKitUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /DashKitUITests/InsertCannulaViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InsertCannulaViewModelTests.swift 3 | // DashKitUITests 4 | // 5 | // Created by Pete Schwamb on 3/31/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DashKit 11 | import PodSDK 12 | @testable import DashKitUI 13 | 14 | class InsertCannulaViewModelTests: XCTestCase { 15 | 16 | var insertCannulaExpectation: XCTestExpectation? 17 | 18 | var insertionError: PodCommError? 19 | 20 | 21 | override func setUp() { 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | } 28 | 29 | func testContinueShouldStartCannulaInsertion() { 30 | let viewModel = InsertCannulaViewModel(cannulaInserter: self) 31 | viewModel.didFinish = { 32 | XCTFail("Unexpected finish") 33 | } 34 | viewModel.didRequestDeactivation = { 35 | XCTFail("Unexpected request for deactivation") 36 | } 37 | 38 | insertCannulaExpectation = expectation(description: "Cannula Insertion") 39 | 40 | viewModel.continueButtonTapped() 41 | 42 | waitForExpectations(timeout: 0.3, handler: nil) 43 | } 44 | 45 | func testContinueAfterUnrecoverableErrorShouldRequestDeactivation() { 46 | let viewModel = InsertCannulaViewModel(cannulaInserter: self) 47 | viewModel.didFinish = { 48 | XCTFail("Unexpected finish") 49 | } 50 | viewModel.didRequestDeactivation = { 51 | XCTFail("Unexpected request for deactivation") 52 | } 53 | 54 | insertionError = .podIsInAlarm(MockPodAlarm()) 55 | 56 | insertCannulaExpectation = expectation(description: "Cannula Insertion") 57 | viewModel.continueButtonTapped() 58 | 59 | let didRequestDeactivationExpectation = expectation(description: "Request Deactivation") 60 | viewModel.didRequestDeactivation = { 61 | didRequestDeactivationExpectation.fulfill() 62 | } 63 | 64 | viewModel.continueButtonTapped() 65 | 66 | waitForExpectations(timeout: 0.3, handler: nil) 67 | } 68 | 69 | func testContinueAfterRecoverableErrorShouldRetry() { 70 | let viewModel = InsertCannulaViewModel(cannulaInserter: self) 71 | viewModel.didFinish = { 72 | XCTFail("Unexpected finish") 73 | } 74 | viewModel.didRequestDeactivation = { 75 | XCTFail("Unexpected request for deactivation") 76 | } 77 | 78 | insertionError = .bleCommunicationError 79 | 80 | insertCannulaExpectation = expectation(description: "Cannula Insertion") 81 | viewModel.continueButtonTapped() 82 | 83 | waitForExpectations(timeout: 0.3, handler: nil) 84 | 85 | insertCannulaExpectation = expectation(description: "Cannula Insertion Retry") 86 | viewModel.continueButtonTapped() 87 | 88 | waitForExpectations(timeout: 0.3, handler: nil) 89 | } 90 | 91 | func testContinueAfterSuccessfulInsertionShouldCallDidFinish() { 92 | let viewModel = InsertCannulaViewModel(cannulaInserter: self) 93 | viewModel.didFinish = { 94 | XCTFail("Unexpected finish") 95 | } 96 | viewModel.didRequestDeactivation = { 97 | XCTFail("Unexpected request for deactivation") 98 | } 99 | 100 | insertCannulaExpectation = expectation(description: "Cannula Insertion") 101 | viewModel.continueButtonTapped() 102 | 103 | waitForExpectations(timeout: 0.3, handler: nil) 104 | 105 | let didFinishExpectation = expectation(description: "Cannula Insertion did finish") 106 | 107 | viewModel.didFinish = { 108 | didFinishExpectation.fulfill() 109 | } 110 | 111 | viewModel.continueButtonTapped() 112 | 113 | waitForExpectations(timeout: 0.3, handler: nil) 114 | } 115 | 116 | } 117 | 118 | extension InsertCannulaViewModelTests: CannulaInserter { 119 | func insertCannula(eventListener: @escaping (ActivationStatus) -> ()) { 120 | if let insertionError = insertionError { 121 | eventListener(.error(insertionError)) 122 | } else { 123 | eventListener(.event(.insertingCannula)) 124 | eventListener(.event(.step2Completed)) 125 | } 126 | insertCannulaExpectation?.fulfill() 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /DashKitUITests/PairPodViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PairPodViewModelTests.swift 3 | // DashKitUITests 4 | // 5 | // Created by Pete Schwamb on 3/30/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import DashKit 11 | import PodSDK 12 | @testable import DashKitUI 13 | 14 | class PairPodViewModelTests: XCTestCase { 15 | 16 | var _podCommState: PodCommState = .active 17 | 18 | var pairingError: PodCommError? 19 | 20 | var lastNavigation: DashUIScreen? 21 | var didNavigateExpectation: XCTestExpectation? 22 | 23 | var didPairExpectation: XCTestExpectation? 24 | 25 | var discardPodExpectation: XCTestExpectation? 26 | 27 | override func setUp() { 28 | // Put setup code here. This method is called before the invocation of each test method in the class. 29 | didNavigateExpectation = nil 30 | didPairExpectation = nil 31 | discardPodExpectation = nil 32 | lastNavigation = nil 33 | pairingError = nil 34 | } 35 | 36 | override func tearDown() { 37 | // Put teardown code here. This method is called after the invocation of each test method in the class. 38 | } 39 | 40 | func testContinueShouldStartPairing() { 41 | let viewModel = PairPodViewModel(podPairer: self, navigator: self) 42 | 43 | didPairExpectation = expectation(description: "Pair Attempt") 44 | 45 | viewModel.continueButtonTapped() 46 | 47 | waitForExpectations(timeout: 0.3, handler: nil) 48 | } 49 | 50 | func testContinueAfterUnrecoverableErrorShouldNavigateToDeactivate() { 51 | let viewModel = PairPodViewModel(podPairer: self, navigator: self) 52 | 53 | pairingError = .podIsInAlarm(MockPodAlarm()) 54 | 55 | didPairExpectation = expectation(description: "Pair Attempt") 56 | viewModel.continueButtonTapped() 57 | 58 | waitForExpectations(timeout: 0.3, handler: nil) 59 | 60 | didNavigateExpectation = expectation(description: "Navigate to deactivate") 61 | viewModel.continueButtonTapped() 62 | 63 | waitForExpectations(timeout: 0.3, handler: nil) 64 | XCTAssertEqual(.deactivate, lastNavigation) 65 | } 66 | 67 | func testContinueAfterRecoverableErrorShouldRetry() { 68 | let viewModel = PairPodViewModel(podPairer: self, navigator: self) 69 | 70 | pairingError = .bleCommunicationError 71 | 72 | didPairExpectation = expectation(description: "Pair Attempt") 73 | viewModel.continueButtonTapped() 74 | 75 | waitForExpectations(timeout: 0.3, handler: nil) 76 | 77 | didPairExpectation = expectation(description: "Pair Retry") 78 | viewModel.continueButtonTapped() 79 | 80 | waitForExpectations(timeout: 0.3, handler: nil) 81 | XCTAssertNil(lastNavigation) 82 | } 83 | 84 | func testContinueAfterSuccessfulPairShouldCallDidFinish() { 85 | let viewModel = PairPodViewModel(podPairer: self, navigator: self) 86 | 87 | didPairExpectation = expectation(description: "Pair Attempt") 88 | viewModel.continueButtonTapped() 89 | 90 | waitForExpectations(timeout: 0.3, handler: nil) 91 | 92 | let didFinishExpectation = expectation(description: "Pairing did finish") 93 | 94 | viewModel.didFinish = { 95 | didFinishExpectation.fulfill() 96 | } 97 | 98 | viewModel.continueButtonTapped() 99 | 100 | waitForExpectations(timeout: 0.3, handler: nil) 101 | } 102 | 103 | } 104 | 105 | extension PairPodViewModelTests: DashUINavigator { 106 | func navigateTo(_ screen: DashUIScreen) { 107 | lastNavigation = screen 108 | didNavigateExpectation?.fulfill() 109 | } 110 | } 111 | 112 | extension PairPodViewModelTests: PodPairer { 113 | var podCommState: PodCommState { 114 | return _podCommState 115 | } 116 | 117 | func pair(eventListener: @escaping (ActivationStatus) -> ()) { 118 | if let pairingError = pairingError { 119 | eventListener(.error(pairingError)) 120 | } else { 121 | eventListener(.event(.primingPod)) 122 | eventListener(.event(.step1Completed)) 123 | } 124 | didPairExpectation?.fulfill() 125 | } 126 | 127 | func discardPod(completion: @escaping (PodCommResult) -> ()) { 128 | discardPodExpectation?.fulfill() 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Tidepool Project 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or other 12 | materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 17 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 18 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 19 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 20 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 21 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 22 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 23 | POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /MockPodPlugin/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | NSHumanReadableCopyright 22 | Copyright © 2020 Tidepool. All rights reserved. 23 | NSPrincipalClass 24 | MockPodPlugin.MockPodPlugin 25 | com.loopkit.Loop.PumpManagerDisplayName 26 | Omnipod (Demo) 27 | com.loopkit.Loop.PumpManagerIdentifier 28 | OmnipodDemo 29 | com.loopkit.Loop.PluginIsSimulator 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /MockPodPlugin/MockPodPlugin.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodPlugin.swift 3 | // MockPodPlugin 4 | // 5 | // Created by Pete Schwamb on 12/11/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import os.log 10 | import LoopKitUI 11 | import DashKit 12 | import DashKitUI 13 | 14 | class MockPodPlugin: NSObject, PumpManagerUIPlugin { 15 | private let log = OSLog(category: "MockPodPlugin") 16 | 17 | public var pumpManagerType: PumpManagerUI.Type? { 18 | return MockPodPumpManager.self 19 | } 20 | 21 | override init() { 22 | super.init() 23 | log.default("Instantiated") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /MockPodPlugin/MockPodPumpManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockPodPumpManager.swift 3 | // MockPodPlugin 4 | // 5 | // Created by Pete Schwamb on 12/11/20. 6 | // Copyright © 2020 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import DashKit 11 | import DashKitUI 12 | import LoopKit 13 | 14 | class MockPodPumpManager: DashPumpManager { 15 | 16 | let mockPodCommManager: MockPodCommManager 17 | 18 | public override var managerIdentifier: String { 19 | return "OmnipodDemo" 20 | } 21 | 22 | public override var registrationManager: PDMRegistrator { 23 | return MockRegistrationManager(isRegistered: true) 24 | } 25 | 26 | 27 | public required init(podStatus: MockPodStatus? = nil, state: DashPumpManagerState, dateGenerator: @escaping () -> Date = Date.init) { 28 | 29 | mockPodCommManager = MockPodCommManager(podStatus: podStatus) 30 | 31 | super.init(state: state, podCommManager: mockPodCommManager, dateGenerator: dateGenerator) 32 | 33 | mockPodCommManager.dashPumpManager = self 34 | 35 | mockPodCommManager.addObserver(self, queue: DispatchQueue.main) 36 | } 37 | 38 | public convenience required init?(rawState: PumpManager.RawStateValue) { 39 | 40 | guard let rawPumpManagerState = rawState["pumpManagerState"] as? PumpManager.RawStateValue, 41 | let pumpManagerState = DashPumpManagerState(rawValue: rawPumpManagerState) 42 | else { 43 | return nil 44 | } 45 | 46 | let mockPodStatus: MockPodStatus? 47 | 48 | if let rawMockPodStatus = rawState["mockPodStatus"] as? MockPodStatus.RawValue { 49 | mockPodStatus = MockPodStatus(rawValue: rawMockPodStatus) 50 | } else { 51 | mockPodStatus = nil 52 | } 53 | 54 | self.init(podStatus: mockPodStatus, state: pumpManagerState) 55 | } 56 | 57 | required convenience init(state: DashPumpManagerState, dateGenerator: @escaping () -> Date = Date.init) { 58 | self.init(podStatus: nil, state: state, dateGenerator: dateGenerator) 59 | } 60 | 61 | public override var rawState: PumpManager.RawStateValue { 62 | var value: PumpManager.RawStateValue = [ 63 | "pumpManagerState": super.rawState 64 | ] 65 | 66 | if let podStatus = mockPodCommManager.podStatus { 67 | value["mockPodStatus"] = podStatus.rawValue 68 | } 69 | 70 | return value 71 | } 72 | } 73 | 74 | extension MockPodPumpManager: MockPodCommManagerObserver { 75 | func mockPodCommManagerDidUpdate() { 76 | notifyDelegateOfStateUpdate() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /PodUIDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // OmnipodPluginHost 4 | // 5 | // Created by Pete Schwamb on 3/2/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 24 | // Called when a new scene session is being created. 25 | // Use this method to select a configuration to create the new scene with. 26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 27 | } 28 | 29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 30 | // Called when the user discards a scene session. 31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 33 | } 34 | 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /PodUIDemo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /PodUIDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /PodUIDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PodUIDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PodUIDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | PodUIDemo 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSBluetoothAlwaysUsageDescription 26 | Use Bluetooth 27 | UIApplicationSceneManifest 28 | 29 | UIApplicationSupportsMultipleScenes 30 | 31 | UISceneConfigurations 32 | 33 | UIWindowSceneSessionRoleApplication 34 | 35 | 36 | UISceneConfigurationName 37 | Default Configuration 38 | UISceneDelegateClassName 39 | $(PRODUCT_MODULE_NAME).SceneDelegate 40 | 41 | 42 | 43 | 44 | UIApplicationSupportsIndirectInputEvents 45 | 46 | UIBackgroundModes 47 | 48 | bluetooth-central 49 | 50 | UILaunchStoryboardName 51 | LaunchScreen 52 | UIRequiredDeviceCapabilities 53 | 54 | armv7 55 | 56 | UISupportedInterfaceOrientations 57 | 58 | UIInterfaceOrientationPortrait 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UISupportedInterfaceOrientations~ipad 63 | 64 | UIInterfaceOrientationPortrait 65 | UIInterfaceOrientationPortraitUpsideDown 66 | UIInterfaceOrientationLandscapeLeft 67 | UIInterfaceOrientationLandscapeRight 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /PodUIDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // OmnipodPluginHost 4 | // 5 | // Created by Pete Schwamb on 3/2/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let windowScene = (scene as? UIWindowScene) else { return } 18 | 19 | window = UIWindow(windowScene: windowScene) 20 | let rootVC = ViewController() 21 | window?.rootViewController = rootVC 22 | window?.makeKeyAndVisible() 23 | } 24 | 25 | func sceneDidDisconnect(_ scene: UIScene) { 26 | // Called as the scene is being released by the system. 27 | // This occurs shortly after the scene enters the background, or when its session is discarded. 28 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 29 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 30 | } 31 | 32 | func sceneDidBecomeActive(_ scene: UIScene) { 33 | // Called when the scene has moved from an inactive state to an active state. 34 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 35 | } 36 | 37 | func sceneWillResignActive(_ scene: UIScene) { 38 | // Called when the scene will move from an active state to an inactive state. 39 | // This may occur due to temporary interruptions (ex. an incoming phone call). 40 | } 41 | 42 | func sceneWillEnterForeground(_ scene: UIScene) { 43 | // Called as the scene transitions from the background to the foreground. 44 | // Use this method to undo the changes made on entering the background. 45 | } 46 | 47 | func sceneDidEnterBackground(_ scene: UIScene) { 48 | // Called as the scene transitions from the foreground to the background. 49 | // Use this method to save data, release shared resources, and store enough scene-specific state information 50 | // to restore the scene back to its current state. 51 | } 52 | 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DashKit 2 | 3 | This is a Loop PumpManager implemention to provide support for the Omnipod DASH™ Pod. 4 | 5 | While this implementation is open source, it depends on a private framework that is not openly available. Tidepool is providing the implementation as open source in the interests of transparency, even though this PumpManager cannot be built without the private framework. 6 | 7 | -------------------------------------------------------------------------------- /ReservoirLevelHighlightState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ReservoirLevelHighlightState.swift 3 | // DashKit 4 | // 5 | // Created by Pete Schwamb on 2/19/21. 6 | // Copyright © 2021 Tidepool. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ReservoirLevelHighlightState: String, Equatable { 12 | case normal 13 | case warning 14 | case critical 15 | } 16 | --------------------------------------------------------------------------------