├── .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 |
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 |
--------------------------------------------------------------------------------