diff --git a/LoopKit/Locked.swift b/Common/Locked.swift similarity index 100% rename from LoopKit/Locked.swift rename to Common/Locked.swift diff --git a/Extensions/Comparable.swift b/Extensions/Comparable.swift index 7a2cafc65..8ffc4dcaa 100644 --- a/Extensions/Comparable.swift +++ b/Extensions/Comparable.swift @@ -1,13 +1,11 @@ // // Comparable.swift -// LoopKit Example +// LoopKit // -// Created by Pete Schwamb on 2/17/19. -// Copyright © 2019 LoopKit Authors. All rights reserved. +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. // -import Foundation - extension Comparable { func clamped(to range: ClosedRange) -> Self { if self < range.lowerBound { @@ -18,4 +16,8 @@ extension Comparable { return self } } + + mutating func clamp(to range: ClosedRange) { + self = clamped(to: range) + } } diff --git a/Extensions/UIColor.swift b/Extensions/UIColor.swift new file mode 100644 index 000000000..0095c6b71 --- /dev/null +++ b/Extensions/UIColor.swift @@ -0,0 +1,22 @@ +// +// UIColor.swift +// LoopKitUI +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit + + +extension UIColor { + static let delete = UIColor.higRed() +} + + +// MARK: - HIG colors +// See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/ +extension UIColor { + private static func higRed() -> UIColor { + return UIColor(red: 1, green: 59 / 255, blue: 48 / 255, alpha: 1) + } +} diff --git a/LoopKit.xcodeproj/project.pbxproj b/LoopKit.xcodeproj/project.pbxproj index 22a8b5c71..9f4294f90 100644 --- a/LoopKit.xcodeproj/project.pbxproj +++ b/LoopKit.xcodeproj/project.pbxproj @@ -299,7 +299,6 @@ 43F5035721059A8A009FA89A /* ServiceCredential.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F5035521059A8A009FA89A /* ServiceCredential.swift */; }; 43F5035A21059AF7009FA89A /* AuthenticationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F5035821059AF7009FA89A /* AuthenticationTableViewCell.swift */; }; 43F5035B21059AF7009FA89A /* AuthenticationTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 43F5035921059AF7009FA89A /* AuthenticationTableViewCell.xib */; }; - 43F5035D21059B56009FA89A /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F5035C21059B56009FA89A /* UIColor.swift */; }; 43F503632106C761009FA89A /* ServiceAuthenticationUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F503622106C761009FA89A /* ServiceAuthenticationUI.swift */; }; 43F503642106C78C009FA89A /* ServiceAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F5035421059A8A009FA89A /* ServiceAuthentication.swift */; }; 43FB60E320DCB9E0002B996B /* PumpManagerUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43FB60E220DCB9E0002B996B /* PumpManagerUI.swift */; }; @@ -311,6 +310,59 @@ 7D68A9AE1FE0A3D000522C49 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D68A9B01FE0A3D000522C49 /* Localizable.strings */; }; 7D68A9B81FE0A3D100522C49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D68A9BA1FE0A3D100522C49 /* InfoPlist.strings */; }; 7D68A9C21FE0A3D200522C49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7D68A9C41FE0A3D200522C49 /* InfoPlist.strings */; }; + 8907E35921A9D0EC00335852 /* GlucoseEntryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8907E35821A9D0EC00335852 /* GlucoseEntryTableViewController.swift */; }; + 892A5D28222EF567008961AB /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C145BF992219F10400A977CB /* Comparable.swift */; }; + 892A5D2E222EF69A008961AB /* MockHUDProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D2D222EF69A008961AB /* MockHUDProvider.swift */; }; + 892A5D3D222F03CB008961AB /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 892A5D34222F03CB008961AB /* LoopTestingKit.framework */; }; + 892A5D44222F03CB008961AB /* LoopTestingKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D43222F03CB008961AB /* LoopTestingKitTests.swift */; }; + 892A5D46222F03CB008961AB /* LoopTestingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 892A5D36222F03CB008961AB /* LoopTestingKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 892A5D49222F03CC008961AB /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 892A5D34222F03CB008961AB /* LoopTestingKit.framework */; }; + 892A5D4A222F03CC008961AB /* LoopTestingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 892A5D34222F03CB008961AB /* LoopTestingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 892A5D52222F03DB008961AB /* TestingDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D51222F03DB008961AB /* TestingDeviceManager.swift */; }; + 892A5D54222F03F9008961AB /* TestingPumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D53222F03F9008961AB /* TestingPumpManager.swift */; }; + 892A5D56222F0414008961AB /* TestingCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D55222F0414008961AB /* TestingCGMManager.swift */; }; + 892A5D57222F04E2008961AB /* LoopTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 892A5D34222F03CB008961AB /* LoopTestingKit.framework */; }; + 892A5D5C222F1210008961AB /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D8FDCB1C728FDF0073BE78 /* LoopKit.framework */; }; + 892A5D61222F6AF4008961AB /* BasalScheduleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D60222F6AF3008961AB /* BasalScheduleTableViewController.swift */; }; + 892A5D64222F6B13008961AB /* BasalScheduleEntryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 892A5D62222F6B13008961AB /* BasalScheduleEntryTableViewCell.xib */; }; + 892A5D65222F6B13008961AB /* BasalScheduleEntryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892A5D63222F6B13008961AB /* BasalScheduleEntryTableViewCell.swift */; }; + 895695F621AA413B00828067 /* DateAndDurationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 895695F521AA413B00828067 /* DateAndDurationTableViewController.swift */; }; + 8992426521EC138000EA512B /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8992426421EC138000EA512B /* UIColor.swift */; }; + 89CCD4FA21A911510068C3FB /* PercentageTextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CCD4F921A911510068C3FB /* PercentageTextFieldTableViewController.swift */; }; + 89D2047B21CC7BD8001238CC /* MockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D2047221CC7BD7001238CC /* MockKit.framework */; }; + 89D2048021CC7BD8001238CC /* MockKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89D2047F21CC7BD8001238CC /* MockKitTests.swift */; }; + 89D2048221CC7BD8001238CC /* MockKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 89D2047421CC7BD7001238CC /* MockKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 89D2048921CC7BF7001238CC /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4301582C1C7ECD7A00B64B63 /* HealthKit.framework */; }; + 89D2049F21CC7C13001238CC /* MockKitUI.h in Headers */ = {isa = PBXBuildFile; fileRef = 89D2049121CC7C13001238CC /* MockKitUI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 89D204A621CC7C55001238CC /* LoopKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D8FDCB1C728FDF0073BE78 /* LoopKit.framework */; }; + 89D204A721CC7C5C001238CC /* LoopKitUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43BA7154201E484D0058961E /* LoopKitUI.framework */; }; + 89D204A821CC7C60001238CC /* MockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 89D2047221CC7BD7001238CC /* MockKit.framework */; }; + 89D204A921CC7C8F001238CC /* MockPumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9EC621A4774500351324 /* MockPumpManager.swift */; }; + 89D204AA21CC7C8F001238CC /* MockGlucoseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CCD4F721A8D5500068C3FB /* MockGlucoseProvider.swift */; }; + 89D204AB21CC7C8F001238CC /* MockCGMDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CCD4F121A87D340068C3FB /* MockCGMDataSource.swift */; }; + 89D204AC21CC7C8F001238CC /* MockCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9EC821A4BC2400351324 /* MockCGMManager.swift */; }; + 89D204B221CC7D93001238CC /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89DC540C21B75AE7005A1CE0 /* Collection.swift */; }; + 89D204B321CC7DC7001238CC /* Locked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4353D179203E7840007B4ECD /* Locked.swift */; }; + 89D204B421CC7E74001238CC /* MockCGMManager+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CCD4F321A8A2B30068C3FB /* MockCGMManager+UI.swift */; }; + 89D204B521CC7E74001238CC /* MockPumpManager+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9ECA21A4C36200351324 /* MockPumpManager+UI.swift */; }; + 89D204B721CC7F34001238CC /* MockPumpManagerSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9ECF21A4D2E500351324 /* MockPumpManagerSetupViewController.swift */; }; + 89D204B821CC7F34001238CC /* MockPumpManagerSettingsSetupViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9ED121A4D74000351324 /* MockPumpManagerSettingsSetupViewController.swift */; }; + 89D204B921CC7F34001238CC /* MockPumpManager.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 89AB9ED321A4D8F000351324 /* MockPumpManager.storyboard */; }; + 89D204BA21CC7F34001238CC /* MockPumpManagerSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89AB9ED521A4DE5F00351324 /* MockPumpManagerSettingsViewController.swift */; }; + 89D204BB21CC7F34001238CC /* MockCGMManagerSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CCD4F521A8A6A60068C3FB /* MockCGMManagerSettingsViewController.swift */; }; + 89D204BC21CC7F34001238CC /* SineCurveParametersTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8907E35A21A9D1B200335852 /* SineCurveParametersTableViewController.swift */; }; + 89D204BD21CC7F34001238CC /* RandomOutlierTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 892F481A21AB2964004D313D /* RandomOutlierTableViewController.swift */; }; + 89D204BE21CC7F34001238CC /* GlucoseTrendTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89D2046B21C83C3F001238CC /* GlucoseTrendTableViewController.swift */; }; + 89D204BF21CC7FFB001238CC /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D8FDF21C7290350073BE78 /* NSTimeInterval.swift */; }; + 89D204C121CC8005001238CC /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43177D0D1D3737420006E908 /* NibLoadable.swift */; }; + 89D204C221CC8008001238CC /* IdentifiableClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1DF1CF269D8000DB779 /* IdentifiableClass.swift */; }; + 89D204C421CC803C001238CC /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D8FDEE1C7290350073BE78 /* HKUnit.swift */; }; + 89D204C521CC815E001238CC /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D8FDF21C7290350073BE78 /* NSTimeInterval.swift */; }; + 89D204C621CC8165001238CC /* UITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434FF1E31CF26A1E000DB779 /* UITableViewCell.swift */; }; + 89D204CB21CC8228001238CC /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434C5F9D209938CD00B2FD1A /* NumberFormatter.swift */; }; + 89D204CC21CC8236001238CC /* LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5DAB1C2118C95700048054 /* LocalizedString.swift */; }; + 89D204D221CC837A001238CC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 89D204D121CC837A001238CC /* Assets.xcassets */; }; + 89E72DE021BDDD6C00F0985C /* SwitchTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 89E72DDF21BDDD6C00F0985C /* SwitchTableViewCell.xib */; }; C11166B02180FA5C000EEAAB /* LoadingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C11166AF2180FA5C000EEAAB /* LoadingTableViewCell.swift */; }; C145BF9B2219F1CB00A977CB /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C145BF992219F10400A977CB /* Comparable.swift */; }; C145BF9C2219F1CC00A977CB /* Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C145BF992219F10400A977CB /* Comparable.swift */; }; @@ -320,9 +372,7 @@ C1E31F1222008AA300E88C00 /* SettingsNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E31F1122008AA300E88C00 /* SettingsNavigationViewController.swift */; }; C1E31F142200E7D500E88C00 /* HUDProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E31F132200E7D500E88C00 /* HUDProvider.swift */; }; C1E31F162200E85F00E88C00 /* CompletionNotifying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E31F152200E85F00E88C00 /* CompletionNotifying.swift */; }; - C1EE8F422221DB34001B12A9 /* BasalScheduleEntryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EE8F412221DB34001B12A9 /* BasalScheduleEntryTableViewCell.swift */; }; - C1EE8F442221DE11001B12A9 /* BasalScheduleEntryTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1EE8F432221DE11001B12A9 /* BasalScheduleEntryTableViewCell.xib */; }; - C1EE8F462221E761001B12A9 /* BasalScheduleTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EE8F452221E761001B12A9 /* BasalScheduleTableViewController.swift */; }; + C1EE3C20221F4E1A0081AA37 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EE3C1F221F4E1A0081AA37 /* UIColor.swift */; }; C1FB427721754EBB00FAB378 /* ReservoirVolumeHUDView.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1FB427621754EBB00FAB378 /* ReservoirVolumeHUDView.xib */; }; C1FB427921754FC900FAB378 /* ReservoirVolumeHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB427821754FC800FAB378 /* ReservoirVolumeHUDView.swift */; }; C1FB427B2175503B00FAB378 /* LevelHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB427A2175503A00FAB378 /* LevelHUDView.swift */; }; @@ -363,6 +413,34 @@ remoteGlobalIDString = 43D8FDCA1C728FDF0073BE78; remoteInfo = LoopKit; }; + 892A5D3E222F03CB008961AB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 43D8FDC21C728FDF0073BE78 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 892A5D33222F03CB008961AB; + remoteInfo = LoopTestingKit; + }; + 892A5D40222F03CB008961AB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 43D8FDC21C728FDF0073BE78 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 430157F61C7EC03B00B64B63; + remoteInfo = "LoopKit Example"; + }; + 892A5D47222F03CB008961AB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 43D8FDC21C728FDF0073BE78 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 892A5D33222F03CB008961AB; + remoteInfo = LoopTestingKit; + }; + 89D2047C21CC7BD8001238CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 43D8FDC21C728FDF0073BE78 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 89D2047121CC7BD7001238CC; + remoteInfo = MockKit; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -372,6 +450,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + 892A5D4A222F03CC008961AB /* LoopTestingKit.framework in Embed Frameworks */, 4301581A1C7ECB5E00B64B63 /* LoopKit.framework in Embed Frameworks */, 43BA715C201E484D0058961E /* LoopKitUI.framework in Embed Frameworks */, ); @@ -745,7 +824,6 @@ 43F5035521059A8A009FA89A /* ServiceCredential.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceCredential.swift; sourceTree = ""; }; 43F5035821059AF7009FA89A /* AuthenticationTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationTableViewCell.swift; sourceTree = ""; }; 43F5035921059AF7009FA89A /* AuthenticationTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AuthenticationTableViewCell.xib; sourceTree = ""; }; - 43F5035C21059B56009FA89A /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 43F503622106C761009FA89A /* ServiceAuthenticationUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceAuthenticationUI.swift; sourceTree = ""; }; 43FADDFA1C89679200DDE013 /* HKQuantitySample+GlucoseKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "HKQuantitySample+GlucoseKit.swift"; sourceTree = ""; }; 43FB60E220DCB9E0002B996B /* PumpManagerUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpManagerUI.swift; sourceTree = ""; }; @@ -768,6 +846,49 @@ 7D68AABE1FE31BE700522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 7D68AAC01FE31BE800522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; 7D68AAC61FE31BE900522C49 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + 8907E35821A9D0EC00335852 /* GlucoseEntryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseEntryTableViewController.swift; sourceTree = ""; }; + 8907E35A21A9D1B200335852 /* SineCurveParametersTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SineCurveParametersTableViewController.swift; sourceTree = ""; }; + 892A5D2D222EF69A008961AB /* MockHUDProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockHUDProvider.swift; sourceTree = ""; }; + 892A5D34222F03CB008961AB /* LoopTestingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LoopTestingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 892A5D36222F03CB008961AB /* LoopTestingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LoopTestingKit.h; sourceTree = ""; }; + 892A5D37222F03CB008961AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 892A5D3C222F03CB008961AB /* LoopTestingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LoopTestingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 892A5D43222F03CB008961AB /* LoopTestingKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopTestingKitTests.swift; sourceTree = ""; }; + 892A5D45222F03CB008961AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 892A5D51222F03DB008961AB /* TestingDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingDeviceManager.swift; sourceTree = ""; }; + 892A5D53222F03F9008961AB /* TestingPumpManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingPumpManager.swift; sourceTree = ""; }; + 892A5D55222F0414008961AB /* TestingCGMManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingCGMManager.swift; sourceTree = ""; }; + 892A5D60222F6AF3008961AB /* BasalScheduleTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalScheduleTableViewController.swift; sourceTree = ""; }; + 892A5D62222F6B13008961AB /* BasalScheduleEntryTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BasalScheduleEntryTableViewCell.xib; sourceTree = ""; }; + 892A5D63222F6B13008961AB /* BasalScheduleEntryTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalScheduleEntryTableViewCell.swift; sourceTree = ""; }; + 892F481A21AB2964004D313D /* RandomOutlierTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomOutlierTableViewController.swift; sourceTree = ""; }; + 895695F521AA413B00828067 /* DateAndDurationTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateAndDurationTableViewController.swift; sourceTree = ""; }; + 8992426421EC138000EA512B /* UIColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; + 89AB9EC621A4774500351324 /* MockPumpManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpManager.swift; sourceTree = ""; }; + 89AB9EC821A4BC2400351324 /* MockCGMManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCGMManager.swift; sourceTree = ""; }; + 89AB9ECA21A4C36200351324 /* MockPumpManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockPumpManager+UI.swift"; sourceTree = ""; }; + 89AB9ECF21A4D2E500351324 /* MockPumpManagerSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpManagerSetupViewController.swift; sourceTree = ""; }; + 89AB9ED121A4D74000351324 /* MockPumpManagerSettingsSetupViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpManagerSettingsSetupViewController.swift; sourceTree = ""; }; + 89AB9ED321A4D8F000351324 /* MockPumpManager.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = MockPumpManager.storyboard; sourceTree = ""; }; + 89AB9ED521A4DE5F00351324 /* MockPumpManagerSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPumpManagerSettingsViewController.swift; sourceTree = ""; }; + 89CCD4F121A87D340068C3FB /* MockCGMDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCGMDataSource.swift; sourceTree = ""; }; + 89CCD4F321A8A2B30068C3FB /* MockCGMManager+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockCGMManager+UI.swift"; sourceTree = ""; }; + 89CCD4F521A8A6A60068C3FB /* MockCGMManagerSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCGMManagerSettingsViewController.swift; sourceTree = ""; }; + 89CCD4F721A8D5500068C3FB /* MockGlucoseProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGlucoseProvider.swift; sourceTree = ""; }; + 89CCD4F921A911510068C3FB /* PercentageTextFieldTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PercentageTextFieldTableViewController.swift; sourceTree = ""; }; + 89D2046B21C83C3F001238CC /* GlucoseTrendTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseTrendTableViewController.swift; sourceTree = ""; }; + 89D2047221CC7BD7001238CC /* MockKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MockKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 89D2047421CC7BD7001238CC /* MockKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockKit.h; sourceTree = ""; }; + 89D2047521CC7BD7001238CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 89D2047A21CC7BD8001238CC /* MockKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MockKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 89D2047F21CC7BD8001238CC /* MockKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockKitTests.swift; sourceTree = ""; }; + 89D2048121CC7BD8001238CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 89D2048F21CC7C12001238CC /* MockKitUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MockKitUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 89D2049121CC7C13001238CC /* MockKitUI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MockKitUI.h; sourceTree = ""; }; + 89D2049221CC7C13001238CC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 89D204D121CC837A001238CC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 89DC540C21B75AE7005A1CE0 /* Collection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; + 89E72DDF21BDDD6C00F0985C /* SwitchTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SwitchTableViewCell.xib; sourceTree = ""; }; C1110E981EE98CF5009BB852 /* ice_35_min_input.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ice_35_min_input.json; sourceTree = ""; }; C11166AF2180FA5C000EEAAB /* LoadingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableViewCell.swift; sourceTree = ""; }; C12EE16B1F2964B3007DB9F1 /* InsulinModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinModel.swift; sourceTree = ""; }; @@ -785,9 +906,7 @@ C1E31F1122008AA300E88C00 /* SettingsNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationViewController.swift; sourceTree = ""; }; C1E31F132200E7D500E88C00 /* HUDProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDProvider.swift; sourceTree = ""; }; C1E31F152200E85F00E88C00 /* CompletionNotifying.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionNotifying.swift; sourceTree = ""; }; - C1EE8F412221DB34001B12A9 /* BasalScheduleEntryTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalScheduleEntryTableViewCell.swift; sourceTree = ""; }; - C1EE8F432221DE11001B12A9 /* BasalScheduleEntryTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = BasalScheduleEntryTableViewCell.xib; sourceTree = ""; }; - C1EE8F452221E761001B12A9 /* BasalScheduleTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalScheduleTableViewController.swift; sourceTree = ""; }; + C1EE3C1F221F4E1A0081AA37 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; C1FB427621754EBB00FAB378 /* ReservoirVolumeHUDView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReservoirVolumeHUDView.xib; sourceTree = ""; }; C1FB427821754FC800FAB378 /* ReservoirVolumeHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReservoirVolumeHUDView.swift; sourceTree = ""; }; C1FB427A2175503A00FAB378 /* LevelHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LevelHUDView.swift; sourceTree = ""; }; @@ -804,6 +923,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 892A5D49222F03CC008961AB /* LoopTestingKit.framework in Frameworks */, 4301582D1C7ECD7A00B64B63 /* HealthKit.framework in Frameworks */, 430158191C7ECB5E00B64B63 /* LoopKit.framework in Frameworks */, 43BA715B201E484D0058961E /* LoopKitUI.framework in Frameworks */, @@ -843,6 +963,49 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 892A5D31222F03CB008961AB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D5C222F1210008961AB /* LoopKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 892A5D39222F03CB008961AB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D3D222F03CB008961AB /* LoopTestingKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2046F21CC7BD7001238CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D57222F04E2008961AB /* LoopTestingKit.framework in Frameworks */, + 89D204A621CC7C55001238CC /* LoopKit.framework in Frameworks */, + 89D2048921CC7BF7001238CC /* HealthKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2047721CC7BD8001238CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D2047B21CC7BD8001238CC /* MockKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2048C21CC7C12001238CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D204A821CC7C60001238CC /* MockKit.framework in Frameworks */, + 89D204A721CC7C5C001238CC /* LoopKitUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -850,6 +1013,7 @@ isa = PBXGroup; children = ( 1F5DAB1C2118C95700048054 /* LocalizedString.swift */, + 4353D179203E7840007B4ECD /* Locked.swift */, ); path = Common; sourceTree = ""; @@ -857,6 +1021,7 @@ 430059211CCDC7A200C861EA /* Extensions */ = { isa = PBXGroup; children = ( + C145BF992219F10400A977CB /* Comparable.swift */, 43D8FDEE1C7290350073BE78 /* HKUnit.swift */, 434FF1DF1CF269D8000DB779 /* IdentifiableClass.swift */, 43177D0D1D3737420006E908 /* NibLoadable.swift */, @@ -868,7 +1033,6 @@ 4303C4901E2D664200ADEDC8 /* TimeZone.swift */, 434FF1E31CF26A1E000DB779 /* UITableViewCell.swift */, 43260F6D21C4BF7A00DD6837 /* UUID.swift */, - C145BF992219F10400A977CB /* Comparable.swift */, ); path = Extensions; sourceTree = ""; @@ -906,7 +1070,7 @@ isa = PBXGroup; children = ( 43F5034C210599CC009FA89A /* AuthenticationViewController.swift */, - C1EE8F452221E761001B12A9 /* BasalScheduleTableViewController.swift */, + 892A5D60222F6AF3008961AB /* BasalScheduleTableViewController.swift */, 434FB6491D712158007B9C70 /* CommandResponseViewController.swift */, 43D8FE041C7290530073BE78 /* DailyQuantityScheduleTableViewController.swift */, 43D8FE051C7290530073BE78 /* DailyValueScheduleTableViewController.swift */, @@ -916,6 +1080,9 @@ 43FB60E420DCBA02002B996B /* SetupTableViewController.swift */, 43D8FE0C1C7290530073BE78 /* SingleValueScheduleTableViewController.swift */, 434FF1F31CF294A9000DB779 /* TextFieldTableViewController.swift */, + 89CCD4F921A911510068C3FB /* PercentageTextFieldTableViewController.swift */, + 8907E35821A9D0EC00335852 /* GlucoseEntryTableViewController.swift */, + 895695F521AA413B00828067 /* DateAndDurationTableViewController.swift */, C1E31F1122008AA300E88C00 /* SettingsNavigationViewController.swift */, ); path = "View Controllers"; @@ -957,8 +1124,8 @@ children = ( 43F5035821059AF7009FA89A /* AuthenticationTableViewCell.swift */, 43F5035921059AF7009FA89A /* AuthenticationTableViewCell.xib */, - C1EE8F412221DB34001B12A9 /* BasalScheduleEntryTableViewCell.swift */, - C1EE8F432221DE11001B12A9 /* BasalScheduleEntryTableViewCell.xib */, + 892A5D63222F6B13008961AB /* BasalScheduleEntryTableViewCell.swift */, + 892A5D62222F6B13008961AB /* BasalScheduleEntryTableViewCell.xib */, C1FB428221755A9A00FAB378 /* BaseHUDView.swift */, C1FB42802175572A00FAB378 /* BatteryLevelHUDView.swift */, C1FB427E2175570C00FAB378 /* BatteryLevelHUDView.xib */, @@ -979,12 +1146,13 @@ 432CF86620D76AB90066B889 /* SettingsTableViewCell.swift */, 432CF86820D76B320066B889 /* SetupButton.swift */, 432CF86A20D76B9C0066B889 /* SetupIndicatorView.swift */, - C184FECA219F2E0100CD2722 /* SuspendResumeTableViewCell.swift */, 432CF86C20D76C470066B889 /* SwitchTableViewCell.swift */, 4369F093208BA001000E3E45 /* TextButtonTableViewCell.swift */, 434FF1F01CF29451000DB779 /* TextFieldTableViewCell.swift */, 434FF1EF1CF29451000DB779 /* TextFieldTableViewCell.xib */, 43F5034E210599DF009FA89A /* ValidatingIndicatorView.swift */, + C184FECA219F2E0100CD2722 /* SuspendResumeTableViewCell.swift */, + 89E72DDF21BDDD6C00F0985C /* SwitchTableViewCell.xib */, ); path = Views; sourceTree = ""; @@ -1027,6 +1195,7 @@ 43BA7155201E484D0058961E /* LoopKitUI */ = { isa = PBXGroup; children = ( + C1EE3C1F221F4E1A0081AA37 /* UIColor.swift */, 43177D091D3732C70006E908 /* Assets.xcassets */, 43BA7160201E48910058961E /* CarbKit */, 43BA7161201E48EB0058961E /* InsulinKit */, @@ -1048,7 +1217,6 @@ 43F5035521059A8A009FA89A /* ServiceCredential.swift */, C1FB428621755B8B00FAB378 /* StateColorPalette.swift */, C1D7366321F78A4D00048CDD /* UIAlertController.swift */, - 43F5035C21059B56009FA89A /* UIColor.swift */, ); path = LoopKitUI; sourceTree = ""; @@ -1100,6 +1268,11 @@ 4301580F1C7EC03B00B64B63 /* LoopKit ExampleUITests */, 43D8FDD91C728FDF0073BE78 /* LoopKitTests */, 43BA7155201E484D0058961E /* LoopKitUI */, + 89D2047321CC7BD7001238CC /* MockKit */, + 89D2047E21CC7BD8001238CC /* MockKitTests */, + 89D2049021CC7C13001238CC /* MockKitUI */, + 892A5D35222F03CB008961AB /* LoopTestingKit */, + 892A5D42222F03CB008961AB /* LoopTestingKitTests */, 43D8FDCC1C728FDF0073BE78 /* Products */, 43BA718D202020140058961E /* Frameworks */, ); @@ -1113,6 +1286,11 @@ 430157F71C7EC03B00B64B63 /* LoopKit Example.app */, 4301580C1C7EC03B00B64B63 /* LoopKit ExampleUITests.xctest */, 43BA7154201E484D0058961E /* LoopKitUI.framework */, + 89D2047221CC7BD7001238CC /* MockKit.framework */, + 89D2047A21CC7BD8001238CC /* MockKitTests.xctest */, + 89D2048F21CC7C12001238CC /* MockKitUI.framework */, + 892A5D34222F03CB008961AB /* LoopTestingKit.framework */, + 892A5D3C222F03CB008961AB /* LoopTestingKitTests.xctest */, ); name = Products; sourceTree = ""; @@ -1120,15 +1298,8 @@ 43D8FDCD1C728FDF0073BE78 /* LoopKit */ = { isa = PBXGroup; children = ( - 43D8FE2B1C72914D0073BE78 /* CarbKit */, - 437AFF22203BE382008C4892 /* Extensions */, - 43D8FE701C7293070073BE78 /* GlucoseKit */, - 43D8FEB21C7294520073BE78 /* InsulinKit */, - 437874B4202FDC8300A3D8B9 /* Persistence */, - 7D68A9C41FE0A3D200522C49 /* InfoPlist.strings */, 43D8FDCE1C728FDF0073BE78 /* LoopKit.h */, 43D8FDD01C728FDF0073BE78 /* Info.plist */, - 1F5DAB2B2118CE9300048054 /* Localizable.strings */, 43D8FDE51C7290340073BE78 /* BasalRateSchedule.swift */, 43D8FDE61C7290350073BE78 /* CarbRatioSchedule.swift */, 4352A73B20DECF0600CAC200 /* CGMManager.swift */, @@ -1144,7 +1315,6 @@ 43D8FDED1C7290350073BE78 /* HealthKitSampleStore.swift */, 43B17C88208EEC0B00AC27E9 /* HealthStoreUnitCache.swift */, 43F5034A21051FCD009FA89A /* KeychainManager.swift */, - 4353D179203E7840007B4ECD /* Locked.swift */, 43D8FDEF1C7290350073BE78 /* LoopMath.swift */, 432CF87220D774220066B889 /* PumpManager.swift */, 43FB610620DDF19B002B996B /* PumpManagerError.swift */, @@ -1153,6 +1323,13 @@ 43D8FDF31C7290350073BE78 /* SampleValue.swift */, 43F5035421059A8A009FA89A /* ServiceAuthentication.swift */, 43FB60EA20DDC868002B996B /* SetBolusError.swift */, + 43D8FE2B1C72914D0073BE78 /* CarbKit */, + 437AFF22203BE382008C4892 /* Extensions */, + 43D8FE701C7293070073BE78 /* GlucoseKit */, + 7D68A9C41FE0A3D200522C49 /* InfoPlist.strings */, + 43D8FEB21C7294520073BE78 /* InsulinKit */, + 1F5DAB2B2118CE9300048054 /* Localizable.strings */, + 437874B4202FDC8300A3D8B9 /* Persistence */, C133FD1621A2A845009B2D20 /* WeakSet.swift */, ); path = LoopKit; @@ -1372,6 +1549,96 @@ path = Fixtures; sourceTree = ""; }; + 892A5D35222F03CB008961AB /* LoopTestingKit */ = { + isa = PBXGroup; + children = ( + 892A5D36222F03CB008961AB /* LoopTestingKit.h */, + 892A5D37222F03CB008961AB /* Info.plist */, + 892A5D51222F03DB008961AB /* TestingDeviceManager.swift */, + 892A5D53222F03F9008961AB /* TestingPumpManager.swift */, + 892A5D55222F0414008961AB /* TestingCGMManager.swift */, + ); + path = LoopTestingKit; + sourceTree = ""; + }; + 892A5D42222F03CB008961AB /* LoopTestingKitTests */ = { + isa = PBXGroup; + children = ( + 892A5D43222F03CB008961AB /* LoopTestingKitTests.swift */, + 892A5D45222F03CB008961AB /* Info.plist */, + ); + path = LoopTestingKitTests; + sourceTree = ""; + }; + 8992426321EC137900EA512B /* Extensions */ = { + isa = PBXGroup; + children = ( + 8992426421EC138000EA512B /* UIColor.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 89D2047321CC7BD7001238CC /* MockKit */ = { + isa = PBXGroup; + children = ( + 89D204AD21CC7D2B001238CC /* Extensions */, + 89CCD4F121A87D340068C3FB /* MockCGMDataSource.swift */, + 89AB9EC821A4BC2400351324 /* MockCGMManager.swift */, + 89CCD4F721A8D5500068C3FB /* MockGlucoseProvider.swift */, + 89AB9EC621A4774500351324 /* MockPumpManager.swift */, + 89D2047421CC7BD7001238CC /* MockKit.h */, + 89D2047521CC7BD7001238CC /* Info.plist */, + ); + path = MockKit; + sourceTree = ""; + }; + 89D2047E21CC7BD8001238CC /* MockKitTests */ = { + isa = PBXGroup; + children = ( + 89D2047F21CC7BD8001238CC /* MockKitTests.swift */, + 89D2048121CC7BD8001238CC /* Info.plist */, + ); + path = MockKitTests; + sourceTree = ""; + }; + 89D2049021CC7C13001238CC /* MockKitUI */ = { + isa = PBXGroup; + children = ( + 8992426321EC137900EA512B /* Extensions */, + 89D204B621CC7E77001238CC /* View Controllers */, + 89AB9ECA21A4C36200351324 /* MockPumpManager+UI.swift */, + 89CCD4F321A8A2B30068C3FB /* MockCGMManager+UI.swift */, + 892A5D2D222EF69A008961AB /* MockHUDProvider.swift */, + 89D2049121CC7C13001238CC /* MockKitUI.h */, + 89D2049221CC7C13001238CC /* Info.plist */, + 89D204D121CC837A001238CC /* Assets.xcassets */, + ); + path = MockKitUI; + sourceTree = ""; + }; + 89D204AD21CC7D2B001238CC /* Extensions */ = { + isa = PBXGroup; + children = ( + 89DC540C21B75AE7005A1CE0 /* Collection.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + 89D204B621CC7E77001238CC /* View Controllers */ = { + isa = PBXGroup; + children = ( + 89AB9ECF21A4D2E500351324 /* MockPumpManagerSetupViewController.swift */, + 89AB9ED121A4D74000351324 /* MockPumpManagerSettingsSetupViewController.swift */, + 89AB9ED321A4D8F000351324 /* MockPumpManager.storyboard */, + 89AB9ED521A4DE5F00351324 /* MockPumpManagerSettingsViewController.swift */, + 89CCD4F521A8A6A60068C3FB /* MockCGMManagerSettingsViewController.swift */, + 8907E35A21A9D1B200335852 /* SineCurveParametersTableViewController.swift */, + 892F481A21AB2964004D313D /* RandomOutlierTableViewController.swift */, + 89D2046B21C83C3F001238CC /* GlucoseTrendTableViewController.swift */, + ); + path = "View Controllers"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1391,6 +1658,30 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 892A5D2F222F03CB008961AB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D46222F03CB008961AB /* LoopTestingKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2046D21CC7BD7001238CC /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D2048221CC7BD8001238CC /* MockKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2048A21CC7C12001238CC /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D2049F21CC7C13001238CC /* MockKitUI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -1408,6 +1699,7 @@ dependencies = ( 4301581C1C7ECB5E00B64B63 /* PBXTargetDependency */, 43BA715A201E484D0058961E /* PBXTargetDependency */, + 892A5D48222F03CB008961AB /* PBXTargetDependency */, ); name = "LoopKit Example"; productName = "LoopKit Example"; @@ -1486,13 +1778,104 @@ productReference = 43D8FDD51C728FDF0073BE78 /* LoopKitTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; + 892A5D33222F03CB008961AB /* LoopTestingKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 892A5D4B222F03CC008961AB /* Build configuration list for PBXNativeTarget "LoopTestingKit" */; + buildPhases = ( + 892A5D2F222F03CB008961AB /* Headers */, + 892A5D30222F03CB008961AB /* Sources */, + 892A5D31222F03CB008961AB /* Frameworks */, + 892A5D32222F03CB008961AB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LoopTestingKit; + productName = LoopTestingKit; + productReference = 892A5D34222F03CB008961AB /* LoopTestingKit.framework */; + productType = "com.apple.product-type.framework"; + }; + 892A5D3B222F03CB008961AB /* LoopTestingKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 892A5D4E222F03CC008961AB /* Build configuration list for PBXNativeTarget "LoopTestingKitTests" */; + buildPhases = ( + 892A5D38222F03CB008961AB /* Sources */, + 892A5D39222F03CB008961AB /* Frameworks */, + 892A5D3A222F03CB008961AB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 892A5D3F222F03CB008961AB /* PBXTargetDependency */, + 892A5D41222F03CB008961AB /* PBXTargetDependency */, + ); + name = LoopTestingKitTests; + productName = LoopTestingKitTests; + productReference = 892A5D3C222F03CB008961AB /* LoopTestingKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 89D2047121CC7BD7001238CC /* MockKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 89D2048721CC7BD8001238CC /* Build configuration list for PBXNativeTarget "MockKit" */; + buildPhases = ( + 89D2046D21CC7BD7001238CC /* Headers */, + 89D2046E21CC7BD7001238CC /* Sources */, + 89D2046F21CC7BD7001238CC /* Frameworks */, + 89D2047021CC7BD7001238CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MockKit; + productName = MockKit; + productReference = 89D2047221CC7BD7001238CC /* MockKit.framework */; + productType = "com.apple.product-type.framework"; + }; + 89D2047921CC7BD8001238CC /* MockKitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 89D2048821CC7BD8001238CC /* Build configuration list for PBXNativeTarget "MockKitTests" */; + buildPhases = ( + 89D2047621CC7BD8001238CC /* Sources */, + 89D2047721CC7BD8001238CC /* Frameworks */, + 89D2047821CC7BD8001238CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 89D2047D21CC7BD8001238CC /* PBXTargetDependency */, + ); + name = MockKitTests; + productName = MockKitTests; + productReference = 89D2047A21CC7BD8001238CC /* MockKitTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 89D2048E21CC7C12001238CC /* MockKitUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 89D204A021CC7C13001238CC /* Build configuration list for PBXNativeTarget "MockKitUI" */; + buildPhases = ( + 89D2048A21CC7C12001238CC /* Headers */, + 89D2048B21CC7C12001238CC /* Sources */, + 89D2048C21CC7C12001238CC /* Frameworks */, + 89D2048D21CC7C12001238CC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MockKitUI; + productName = MockKitUI; + productReference = 89D2048F21CC7C12001238CC /* MockKitUI.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 43D8FDC21C728FDF0073BE78 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0720; + LastSwiftUpdateCheck = 1010; LastUpgradeCheck = 1000; ORGANIZATIONNAME = "LoopKit Authors"; TargetAttributes = { @@ -1523,6 +1906,28 @@ CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 1000; }; + 892A5D33222F03CB008961AB = { + CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1010; + ProvisioningStyle = Automatic; + }; + 892A5D3B222F03CB008961AB = { + CreatedOnToolsVersion = 10.1; + ProvisioningStyle = Automatic; + TestTargetID = 430157F61C7EC03B00B64B63; + }; + 89D2047121CC7BD7001238CC = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; + 89D2047921CC7BD8001238CC = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; + 89D2048E21CC7C12001238CC = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + }; }; }; buildConfigurationList = 43D8FDC51C728FDF0073BE78 /* Build configuration list for PBXProject "LoopKit" */; @@ -1551,6 +1956,11 @@ 43D8FDCA1C728FDF0073BE78 /* LoopKit */, 43D8FDD41C728FDF0073BE78 /* LoopKitTests */, 43BA7153201E484D0058961E /* LoopKitUI */, + 89D2047121CC7BD7001238CC /* MockKit */, + 89D2047921CC7BD8001238CC /* MockKitTests */, + 89D2048E21CC7C12001238CC /* MockKitUI */, + 892A5D33222F03CB008961AB /* LoopTestingKit */, + 892A5D3B222F03CB008961AB /* LoopTestingKitTests */, ); }; /* End PBXProject section */ @@ -1582,18 +1992,19 @@ buildActionMask = 2147483647; files = ( 43BA7194202039A90058961E /* GlucoseRangeTableViewCell.xib in Resources */, + 892A5D64222F6B13008961AB /* BasalScheduleEntryTableViewCell.xib in Resources */, 43BA719720203EF30058961E /* CarbKit.storyboard in Resources */, C1FB427721754EBB00FAB378 /* ReservoirVolumeHUDView.xib in Resources */, 43BA7173201E492E0058961E /* DateAndDurationTableViewCell.xib in Resources */, 43BA7164201E49130058961E /* InsulinKit.storyboard in Resources */, 43BA7195202039B00058961E /* GlucoseRangeOverrideTableViewCell.xib in Resources */, - C1EE8F442221DE11001B12A9 /* BasalScheduleEntryTableViewCell.xib in Resources */, 1FE58796211D12CE004F24ED /* Localizable.strings in Resources */, 43BA7193202039A30058961E /* TextFieldTableViewCell.xib in Resources */, C1FB427D217551F200FAB378 /* HUDAssets.xcassets in Resources */, 43BA719620203C750058961E /* Assets.xcassets in Resources */, 1F5DAB2A2118CE9300048054 /* InfoPlist.strings in Resources */, C1FB427F2175570C00FAB378 /* BatteryLevelHUDView.xib in Resources */, + 89E72DE021BDDD6C00F0985C /* SwitchTableViewCell.xib in Resources */, 43BA7192202039950058961E /* RepeatingScheduleValueTableViewCell.xib in Resources */, 43F5035B21059AF7009FA89A /* AuthenticationTableViewCell.xib in Resources */, ); @@ -1706,6 +2117,43 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 892A5D32222F03CB008961AB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 892A5D3A222F03CB008961AB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2047021CC7BD7001238CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2047821CC7BD8001238CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2048D21CC7C12001238CC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D204D221CC837A001238CC /* Assets.xcassets in Resources */, + 89D204B921CC7F34001238CC /* MockPumpManager.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1741,13 +2189,16 @@ files = ( 43FB60E720DCBC55002B996B /* RadioSelectionTableViewController.swift in Sources */, 43BA7182201EE7090058961E /* TextFieldTableViewCell.swift in Sources */, + 8907E35921A9D0EC00335852 /* GlucoseEntryTableViewController.swift in Sources */, 43F5034D210599CC009FA89A /* AuthenticationViewController.swift in Sources */, C1E31F1222008AA300E88C00 /* SettingsNavigationViewController.swift in Sources */, 43F5034F210599DF009FA89A /* ValidatingIndicatorView.swift in Sources */, C1E31F162200E85F00E88C00 /* CompletionNotifying.swift in Sources */, + 892A5D61222F6AF4008961AB /* BasalScheduleTableViewController.swift in Sources */, 43BA718C201EEE5A0058961E /* NSData.swift in Sources */, 43F5035A21059AF7009FA89A /* AuthenticationTableViewCell.swift in Sources */, 43BA7163201E490D0058961E /* InsulinDeliveryTableViewController.swift in Sources */, + C1EE3C20221F4E1A0081AA37 /* UIColor.swift in Sources */, 43BA718A201EE8CF0058961E /* NSTimeInterval.swift in Sources */, 432CF86720D76AB90066B889 /* SettingsTableViewCell.swift in Sources */, 43BA7170201E49220058961E /* FoodTypeShortcutCell.swift in Sources */, @@ -1759,7 +2210,6 @@ 1F5DAB212118C95700048054 /* LocalizedString.swift in Sources */, 43FB60E520DCBA02002B996B /* SetupTableViewController.swift in Sources */, 43BA7180201EE7090058961E /* SingleValueScheduleTableViewController.swift in Sources */, - 43F5035D21059B56009FA89A /* UIColor.swift in Sources */, 43BA717E201EE7090058961E /* CommandResponseViewController.swift in Sources */, 43BA717A201E4F1D0058961E /* IdentifiableClass.swift in Sources */, 43BA7187201EE7090058961E /* GlucoseRangeScheduleTableViewController.swift in Sources */, @@ -1779,16 +2229,16 @@ 43BA716F201E49220058961E /* FoodEmojiDataSource.swift in Sources */, C1FB427921754FC900FAB378 /* ReservoirVolumeHUDView.swift in Sources */, 432CF86D20D76C470066B889 /* SwitchTableViewCell.swift in Sources */, - C1EE8F422221DB34001B12A9 /* BasalScheduleEntryTableViewCell.swift in Sources */, 43BA7188201EE85B0058961E /* HKUnit.swift in Sources */, C11166B02180FA5C000EEAAB /* LoadingTableViewCell.swift in Sources */, - C1EE8F462221E761001B12A9 /* BasalScheduleTableViewController.swift in Sources */, + 895695F621AA413B00828067 /* DateAndDurationTableViewController.swift in Sources */, 4369F092208B0DFF000E3E45 /* DateAndDurationTableViewCell.swift in Sources */, 43BA7189201EE8980058961E /* UITableViewCell.swift in Sources */, 43BA7181201EE7090058961E /* RepeatingScheduleValueTableViewCell.swift in Sources */, 43BA717C201EE7090058961E /* GlucoseRangeSchedule+UI.swift in Sources */, 43BA716E201E49220058961E /* DecimalTextFieldTableViewCell.swift in Sources */, 43A8EC3C210CEEA500A81379 /* CGMManagerUI.swift in Sources */, + 892A5D65222F6B13008961AB /* BasalScheduleEntryTableViewCell.swift in Sources */, C1FB427B2175503B00FAB378 /* LevelHUDView.swift in Sources */, 43BA718B201EE93C0058961E /* TimeZone.swift in Sources */, 43BA717B201EE6A40058961E /* NibLoadable.swift in Sources */, @@ -1799,6 +2249,7 @@ C1E31F142200E7D500E88C00 /* HUDProvider.swift in Sources */, 43BA717D201EE7090058961E /* GlucoseRangeTableViewCell.swift in Sources */, 43BA7166201E49220058961E /* CarbAbsorptionInputCell.swift in Sources */, + 89CCD4FA21A911510068C3FB /* PercentageTextFieldTableViewController.swift in Sources */, C1FB42812175572A00FAB378 /* BatteryLevelHUDView.swift in Sources */, 43BA7184201EE7090058961E /* TextFieldTableViewController.swift in Sources */, 43BA716A201E49220058961E /* CarbEntryTableViewController.swift in Sources */, @@ -1942,6 +2393,72 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 892A5D30222F03CB008961AB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D54222F03F9008961AB /* TestingPumpManager.swift in Sources */, + 892A5D52222F03DB008961AB /* TestingDeviceManager.swift in Sources */, + 892A5D56222F0414008961AB /* TestingCGMManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 892A5D38222F03CB008961AB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D44222F03CB008961AB /* LoopTestingKitTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2046E21CC7BD7001238CC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D204BF21CC7FFB001238CC /* NSTimeInterval.swift in Sources */, + 89D204C421CC803C001238CC /* HKUnit.swift in Sources */, + 89D204B321CC7DC7001238CC /* Locked.swift in Sources */, + 89D204A921CC7C8F001238CC /* MockPumpManager.swift in Sources */, + 89D204AA21CC7C8F001238CC /* MockGlucoseProvider.swift in Sources */, + 89D204AB21CC7C8F001238CC /* MockCGMDataSource.swift in Sources */, + 89D204B221CC7D93001238CC /* Collection.swift in Sources */, + 89D204AC21CC7C8F001238CC /* MockCGMManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2047621CC7BD8001238CC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 89D2048021CC7BD8001238CC /* MockKitTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 89D2048B21CC7C12001238CC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 892A5D2E222EF69A008961AB /* MockHUDProvider.swift in Sources */, + 89D204B721CC7F34001238CC /* MockPumpManagerSetupViewController.swift in Sources */, + 89D204B421CC7E74001238CC /* MockCGMManager+UI.swift in Sources */, + 89D204B521CC7E74001238CC /* MockPumpManager+UI.swift in Sources */, + 89D204C121CC8005001238CC /* NibLoadable.swift in Sources */, + 89D204BE21CC7F34001238CC /* GlucoseTrendTableViewController.swift in Sources */, + 89D204C621CC8165001238CC /* UITableViewCell.swift in Sources */, + 8992426521EC138000EA512B /* UIColor.swift in Sources */, + 89D204BA21CC7F34001238CC /* MockPumpManagerSettingsViewController.swift in Sources */, + 89D204BC21CC7F34001238CC /* SineCurveParametersTableViewController.swift in Sources */, + 89D204BD21CC7F34001238CC /* RandomOutlierTableViewController.swift in Sources */, + 89D204B821CC7F34001238CC /* MockPumpManagerSettingsSetupViewController.swift in Sources */, + 892A5D28222EF567008961AB /* Comparable.swift in Sources */, + 89D204C221CC8008001238CC /* IdentifiableClass.swift in Sources */, + 89D204CB21CC8228001238CC /* NumberFormatter.swift in Sources */, + 89D204BB21CC7F34001238CC /* MockCGMManagerSettingsViewController.swift in Sources */, + 89D204C521CC815E001238CC /* NSTimeInterval.swift in Sources */, + 89D204CC21CC8236001238CC /* LocalizedString.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1965,6 +2482,26 @@ target = 43D8FDCA1C728FDF0073BE78 /* LoopKit */; targetProxy = 43D8FDD71C728FDF0073BE78 /* PBXContainerItemProxy */; }; + 892A5D3F222F03CB008961AB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 892A5D33222F03CB008961AB /* LoopTestingKit */; + targetProxy = 892A5D3E222F03CB008961AB /* PBXContainerItemProxy */; + }; + 892A5D41222F03CB008961AB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 430157F61C7EC03B00B64B63 /* LoopKit Example */; + targetProxy = 892A5D40222F03CB008961AB /* PBXContainerItemProxy */; + }; + 892A5D48222F03CB008961AB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 892A5D33222F03CB008961AB /* LoopTestingKit */; + targetProxy = 892A5D47222F03CB008961AB /* PBXContainerItemProxy */; + }; + 89D2047D21CC7BD8001238CC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 89D2047121CC7BD7001238CC /* MockKit */; + targetProxy = 89D2047C21CC7BD8001238CC /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -2486,6 +3023,301 @@ }; name = Release; }; + 892A5D4C222F03CC008961AB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LoopTestingKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LoopTestingKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 892A5D4D222F03CC008961AB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = ""; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LoopTestingKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LoopTestingKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 892A5D4F222F03CC008961AB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LoopTestingKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LoopTestingKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LoopKit Example.app/LoopKit Example"; + }; + name = Debug; + }; + 892A5D50222F03CC008961AB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = LoopTestingKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LoopTestingKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/LoopKit Example.app/LoopKit Example"; + }; + name = Release; + }; + 89D2048321CC7BD8001238CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 89D2048421CC7BD8001238CC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKit/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKit; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 89D2048521CC7BD8001238CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 89D2048621CC7BD8001238CC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKitTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKitTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 89D204A121CC7C13001238CC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKitUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 89D204A221CC7C13001238CC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = MockKitUI/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 11.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.MockKitUI; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -2543,6 +3375,51 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 892A5D4B222F03CC008961AB /* Build configuration list for PBXNativeTarget "LoopTestingKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 892A5D4C222F03CC008961AB /* Debug */, + 892A5D4D222F03CC008961AB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 892A5D4E222F03CC008961AB /* Build configuration list for PBXNativeTarget "LoopTestingKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 892A5D4F222F03CC008961AB /* Debug */, + 892A5D50222F03CC008961AB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 89D2048721CC7BD8001238CC /* Build configuration list for PBXNativeTarget "MockKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 89D2048321CC7BD8001238CC /* Debug */, + 89D2048421CC7BD8001238CC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 89D2048821CC7BD8001238CC /* Build configuration list for PBXNativeTarget "MockKitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 89D2048521CC7BD8001238CC /* Debug */, + 89D2048621CC7BD8001238CC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 89D204A021CC7C13001238CC /* Build configuration list for PBXNativeTarget "MockKitUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 89D204A121CC7C13001238CC /* Debug */, + 89D204A221CC7C13001238CC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCVersionGroup section */ diff --git a/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme b/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme index 72aea7cc5..3cacc37e9 100644 --- a/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme +++ b/LoopKit.xcodeproj/xcshareddata/xcschemes/LoopKit Example.xcscheme @@ -49,6 +49,16 @@ ReferencedContainer = "container:LoopKit.xcodeproj"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LoopKit.xcodeproj/xcshareddata/xcschemes/MockKitUI.xcscheme b/LoopKit.xcodeproj/xcshareddata/xcschemes/MockKitUI.xcscheme new file mode 100644 index 000000000..c0dc4e16a --- /dev/null +++ b/LoopKit.xcodeproj/xcshareddata/xcschemes/MockKitUI.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LoopKit/CGMManager.swift b/LoopKit/CGMManager.swift index c4990fc6c..02e128fd4 100644 --- a/LoopKit/CGMManager.swift +++ b/LoopKit/CGMManager.swift @@ -38,6 +38,12 @@ public protocol CGMManagerDelegate: class { /// /// - Parameter manager: The manager instance func cgmManagerWantsDeletion(_ manager: CGMManager) + + + /// Informs the delegate that the manager has updated its state and should be persisted. + /// + /// - Parameter manager: The manager instance + func cgmManagerDidUpdateState(_ manager: CGMManager) } diff --git a/LoopKit/GlucoseKit/GlucoseStore.swift b/LoopKit/GlucoseKit/GlucoseStore.swift index b30e3b407..b1064f9d0 100644 --- a/LoopKit/GlucoseKit/GlucoseStore.swift +++ b/LoopKit/GlucoseKit/GlucoseStore.swift @@ -221,6 +221,20 @@ extension GlucoseStore { } } + + /// Deletes glucose samples from both the CoreData cache and from HealthKit. + /// + /// - Parameters: + /// - cachePredicate: The predicate to use in matching CoreData glucose objects, or `nil` to match all. + /// - healthKitPredicate: The predicate to use in matching HealthKit glucose objects. + /// - completion: The completion handler for the result of the HealthKit object deletion. + public func purgeGlucoseSamples(matchingCachePredicate cachePredicate: NSPredicate?, healthKitPredicate: NSPredicate, completion: @escaping (_ success: Bool, _ count: Int, _ error: Error?) -> Void) { + dataAccessQueue.async { + self.purgeCachedGlucoseObjects(matching: cachePredicate) + self.healthStore.deleteObjects(of: self.glucoseType, predicate: healthKitPredicate, withCompletion: completion) + } + } + /** Cleans the in-memory and managed HealthKit caches. @@ -422,7 +436,7 @@ extension GlucoseStore { return Date(timeIntervalSinceNow: -cacheLength) } - private func purgeCachedGlucoseObjects(matching predicate: NSPredicate) { + private func purgeCachedGlucoseObjects(matching predicate: NSPredicate?) { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) cacheStore.managedObjectContext.performAndWait { diff --git a/LoopKit/GlucoseKit/GlucoseTrend.swift b/LoopKit/GlucoseKit/GlucoseTrend.swift index be311f16d..3630a69f2 100644 --- a/LoopKit/GlucoseKit/GlucoseTrend.swift +++ b/LoopKit/GlucoseKit/GlucoseTrend.swift @@ -9,7 +9,7 @@ import Foundation -public enum GlucoseTrend: Int { +public enum GlucoseTrend: Int, CaseIterable { case upUpUp = 1 case upUp = 2 case up = 3 diff --git a/LoopKit/Persistence/NSManagedObjectContext.swift b/LoopKit/Persistence/NSManagedObjectContext.swift index dd4168f02..ac829a545 100644 --- a/LoopKit/Persistence/NSManagedObjectContext.swift +++ b/LoopKit/Persistence/NSManagedObjectContext.swift @@ -18,7 +18,7 @@ extension NSManagedObjectContext { /// - predicate: The predicate to match /// - Returns: The number of deleted objects /// - Throws: NSBatchDeleteRequest exeuction errors - internal func purgeObjects(of type: T.Type, matching predicate: NSPredicate) throws -> Int { + internal func purgeObjects(of type: T.Type, matching predicate: NSPredicate? = nil) throws -> Int { let fetchRequest: NSFetchRequest = T.fetchRequest() fetchRequest.predicate = predicate diff --git a/LoopKit/PumpManagerStatus.swift b/LoopKit/PumpManagerStatus.swift index 3ceb9e88f..cff31e343 100644 --- a/LoopKit/PumpManagerStatus.swift +++ b/LoopKit/PumpManagerStatus.swift @@ -44,3 +44,16 @@ public struct PumpManagerStatus: Equatable { self.bolusState = bolusState } } + +extension PumpManagerStatus: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + ## PumpManagerStatus + * timeZone: \(timeZone) + * device: \(device) + * pumpBatteryChargeRemaining: \(pumpBatteryChargeRemaining as Any) + * suspendState: \(basalDeliveryState) + * bolusState: \(bolusState) + """ + } +} diff --git a/LoopKitUI/UIColor.swift b/LoopKitUI/UIColor.swift index 7a4ed96a0..0895af0a7 100644 --- a/LoopKitUI/UIColor.swift +++ b/LoopKitUI/UIColor.swift @@ -4,7 +4,6 @@ // // Copyright © 2018 LoopKit Authors. All rights reserved. // - import UIKit private class FrameworkBundle { diff --git a/LoopKitUI/View Controllers/DateAndDurationTableViewController.swift b/LoopKitUI/View Controllers/DateAndDurationTableViewController.swift new file mode 100644 index 000000000..10d2cfe7e --- /dev/null +++ b/LoopKitUI/View Controllers/DateAndDurationTableViewController.swift @@ -0,0 +1,99 @@ +// +// DateAndDurationTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/24/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit + + +public protocol DateAndDurationTableViewControllerDelegate: class { + func dateAndDurationTableViewControllerDidChangeDate(_ controller: DateAndDurationTableViewController) +} + +public class DateAndDurationTableViewController: UITableViewController { + public enum InputMode { + case date(Date, mode: UIDatePicker.Mode) + case duration(TimeInterval) + } + + public var inputMode: InputMode = .date(Date(), mode: .dateAndTime) { + didSet { + delegate?.dateAndDurationTableViewControllerDidChangeDate(self) + } + } + + public var titleText: String? + + public var contextHelp: String? + + public var indexPath: IndexPath? + + public weak var delegate: DateAndDurationTableViewControllerDelegate? + + public convenience init() { + self.init(style: .grouped) + } + + public override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(DateAndDurationTableViewCell.nib(), forCellReuseIdentifier: DateAndDurationTableViewCell.className) + } + + private var completion: ((InputMode) -> Void)? + + public func onSave(_ completion: @escaping (InputMode) -> Void) { + let saveBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(save)) + navigationItem.rightBarButtonItem = saveBarButtonItem + self.completion = completion + } + + @objc private func save() { + completion?(inputMode) + dismiss(animated: true) + } + + public override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 1 + } + + public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: DateAndDurationTableViewCell.className, for: indexPath) as! DateAndDurationTableViewCell + switch inputMode { + case .date(let date, mode: let mode): + cell.datePicker.datePickerMode = mode + cell.date = date + case .duration(let duration): + cell.datePicker.datePickerMode = .countDownTimer + cell.maximumDuration = .hours(24) + cell.duration = duration + } + cell.titleLabel.text = titleText + cell.isDatePickerHidden = false + cell.selectionStyle = .none + cell.delegate = self + return cell + } + + public override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return contextHelp + } +} + +extension DateAndDurationTableViewController: DatePickerTableViewCellDelegate { + func datePickerTableViewCellDidUpdateDate(_ cell: DatePickerTableViewCell) { + switch inputMode { + case .date(_, mode: let mode): + inputMode = .date(cell.date, mode: mode) + case .duration(_): + inputMode = .duration(cell.duration) + } + } +} diff --git a/LoopKitUI/View Controllers/GlucoseEntryTableViewController.swift b/LoopKitUI/View Controllers/GlucoseEntryTableViewController.swift new file mode 100644 index 000000000..c8a12efc3 --- /dev/null +++ b/LoopKitUI/View Controllers/GlucoseEntryTableViewController.swift @@ -0,0 +1,68 @@ +// +// GlucoseEntryTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/24/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit + + +public protocol GlucoseEntryTableViewControllerDelegate: class { + func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) +} + +public class GlucoseEntryTableViewController: TextFieldTableViewController { + + let glucoseUnit: HKUnit + + private lazy var glucoseFormatter: NumberFormatter = { + let quantityFormatter = QuantityFormatter() + quantityFormatter.setPreferredNumberFormatter(for: glucoseUnit) + return quantityFormatter.numberFormatter + }() + + public var glucose: HKQuantity? { + get { + guard let value = value, let doubleValue = Double(value) else { + return nil + } + return HKQuantity(unit: glucoseUnit, doubleValue: doubleValue) + } + set { + if let newValue = newValue { + value = glucoseFormatter.string(from: newValue.doubleValue(for: glucoseUnit)) + } else { + value = nil + } + } + } + + public weak var glucoseEntryDelegate: GlucoseEntryTableViewControllerDelegate? + + public init(glucoseUnit: HKUnit) { + self.glucoseUnit = glucoseUnit + super.init(style: .grouped) + unit = glucoseUnit.glucoseUnitDisplayString + keyboardType = .decimalPad + placeholder = "Enter glucose value" + delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +extension GlucoseEntryTableViewController: TextFieldTableViewControllerDelegate { + public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { + glucoseEntryDelegate?.glucoseEntryTableViewControllerDidChangeGlucose(self) + } + + public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { + glucoseEntryDelegate?.glucoseEntryTableViewControllerDidChangeGlucose(self) + } +} diff --git a/LoopKitUI/View Controllers/PercentageTextFieldTableViewController.swift b/LoopKitUI/View Controllers/PercentageTextFieldTableViewController.swift new file mode 100644 index 000000000..a7a2c6c99 --- /dev/null +++ b/LoopKitUI/View Controllers/PercentageTextFieldTableViewController.swift @@ -0,0 +1,67 @@ +// +// PercentageTextFieldTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/23/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit + + +public protocol PercentageTextFieldTableViewControllerDelegate: class { + func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) +} + +public class PercentageTextFieldTableViewController: TextFieldTableViewController { + + public var percentage: Double? { + get { + if let doubleValue = value.flatMap(Double.init) { + return doubleValue / 100 + } else { + return nil + } + } + set { + if let percentage = newValue { + value = percentageFormatter.string(from: percentage * 100) + } else { + value = nil + } + } + } + + public weak var percentageDelegate: PercentageTextFieldTableViewControllerDelegate? + + var maximumFractionDigits: Int = 1 { + didSet { + percentageFormatter.maximumFractionDigits = maximumFractionDigits + } + } + + private lazy var percentageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.maximumFractionDigits = maximumFractionDigits + return formatter + }() + + public convenience init() { + self.init(style: .grouped) + unit = "%" + keyboardType = .decimalPad + placeholder = "Enter percentage" + delegate = self + } +} + +extension PercentageTextFieldTableViewController: TextFieldTableViewControllerDelegate { + public func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { + percentageDelegate?.percentageTextFieldTableViewControllerDidChangePercentage(self) + } + + public func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { + percentageDelegate?.percentageTextFieldTableViewControllerDidChangePercentage(self) + } +} diff --git a/LoopKitUI/Views/SwitchTableViewCell.swift b/LoopKitUI/Views/SwitchTableViewCell.swift index 2b2e3cafc..eab8fadbf 100644 --- a/LoopKitUI/Views/SwitchTableViewCell.swift +++ b/LoopKitUI/Views/SwitchTableViewCell.swift @@ -17,11 +17,15 @@ public final class SwitchTableViewCell: UITableViewCell { public var `switch`: UISwitch? + public var onToggle: ((_ isOn: Bool) -> Void)? + override public func awakeFromNib() { super.awakeFromNib() `switch` = UISwitch(frame: .zero) accessoryView = `switch` + + `switch`?.addTarget(self, action: #selector(respondToToggle), for: .valueChanged) } override public func layoutSubviews() { @@ -34,7 +38,14 @@ public final class SwitchTableViewCell: UITableViewCell { override public func prepareForReuse() { super.prepareForReuse() + onToggle = nil self.switch?.removeTarget(nil, action: nil, for: .valueChanged) + `switch`?.addTarget(self, action: #selector(respondToToggle), for: .valueChanged) } + @objc private func respondToToggle() { + if let `switch` = `switch`, let onToggle = onToggle { + onToggle(`switch`.isOn) + } + } } diff --git a/LoopKitUI/Views/SwitchTableViewCell.xib b/LoopKitUI/Views/SwitchTableViewCell.xib new file mode 100644 index 000000000..a589abed9 --- /dev/null +++ b/LoopKitUI/Views/SwitchTableViewCell.xib @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LoopKitUI/Views/TextButtonTableViewCell.swift b/LoopKitUI/Views/TextButtonTableViewCell.swift index 3e4c6e543..9f5bc28e6 100644 --- a/LoopKitUI/Views/TextButtonTableViewCell.swift +++ b/LoopKitUI/Views/TextButtonTableViewCell.swift @@ -38,4 +38,12 @@ open class TextButtonTableViewCell: LoadingTableViewCell { textLabel?.textColor = tintColor } + + open override func prepareForReuse() { + super.prepareForReuse() + + textLabel?.textAlignment = .natural + tintColor = nil + isEnabled = true + } } diff --git a/LoopTestingKit/Info.plist b/LoopTestingKit/Info.plist new file mode 100644 index 000000000..e1fe4cfb7 --- /dev/null +++ b/LoopTestingKit/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/LoopTestingKit/LoopTestingKit.h b/LoopTestingKit/LoopTestingKit.h new file mode 100644 index 000000000..bf81a5849 --- /dev/null +++ b/LoopTestingKit/LoopTestingKit.h @@ -0,0 +1,19 @@ +// +// LoopTestingKit.h +// LoopTestingKit +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +#import + +//! Project version number for LoopTestingKit. +FOUNDATION_EXPORT double LoopTestingKitVersionNumber; + +//! Project version string for LoopTestingKit. +FOUNDATION_EXPORT const unsigned char LoopTestingKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/LoopTestingKit/TestingCGMManager.swift b/LoopTestingKit/TestingCGMManager.swift new file mode 100644 index 000000000..6cc6fcba2 --- /dev/null +++ b/LoopTestingKit/TestingCGMManager.swift @@ -0,0 +1,12 @@ +// +// TestingCGMManager.swift +// LoopTestingKit +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import LoopKit + + +public protocol TestingCGMManager: CGMManager, TestingDeviceManager {} diff --git a/LoopTestingKit/TestingDeviceManager.swift b/LoopTestingKit/TestingDeviceManager.swift new file mode 100644 index 000000000..7f659b143 --- /dev/null +++ b/LoopTestingKit/TestingDeviceManager.swift @@ -0,0 +1,15 @@ +// +// TestingDeviceManager.swift +// LoopTestingKit +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import LoopKit +import HealthKit + + +public protocol TestingDeviceManager: DeviceManager { + var testingDevice: HKDevice { get } +} diff --git a/LoopTestingKit/TestingPumpManager.swift b/LoopTestingKit/TestingPumpManager.swift new file mode 100644 index 000000000..49258bfb5 --- /dev/null +++ b/LoopTestingKit/TestingPumpManager.swift @@ -0,0 +1,12 @@ +// +// TestingPumpManager.swift +// LoopTestingKit +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import LoopKit + + +public protocol TestingPumpManager: PumpManager, TestingDeviceManager {} diff --git a/LoopTestingKitTests/Info.plist b/LoopTestingKitTests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/LoopTestingKitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/LoopTestingKitTests/LoopTestingKitTests.swift b/LoopTestingKitTests/LoopTestingKitTests.swift new file mode 100644 index 000000000..87885427a --- /dev/null +++ b/LoopTestingKitTests/LoopTestingKitTests.swift @@ -0,0 +1,34 @@ +// +// LoopTestingKitTests.swift +// LoopTestingKitTests +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import LoopTestingKit + +class LoopTestingKitTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/MockKit/Extensions/Collection.swift b/MockKit/Extensions/Collection.swift new file mode 100644 index 000000000..37202c207 --- /dev/null +++ b/MockKit/Extensions/Collection.swift @@ -0,0 +1,37 @@ +// +// Collection.swift +// LoopKit +// +// Created by Michael Pangburn on 12/4/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Dispatch + + +extension Collection { + func asyncMap( + _ asyncTransform: ( + _ element: Element, + _ completion: @escaping (NewElement) -> Void + ) -> Void, + notifyingOn queue: DispatchQueue = .global(), + completion: @escaping ([NewElement]) -> Void + ) { + let result = Locked(Array(repeating: nil, count: count)) + let group = DispatchGroup() + + for (resultIndex, element) in enumerated() { + group.enter() + asyncTransform(element) { newElement in + result.value[resultIndex] = newElement + group.leave() + } + } + + group.notify(queue: queue) { + let transformed = result.value.map { $0! } + completion(transformed) + } + } +} diff --git a/MockKit/Info.plist b/MockKit/Info.plist new file mode 100644 index 000000000..4fab88222 --- /dev/null +++ b/MockKit/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + PumpManager + MockPumpManager + CGMManager + MockCGMManager + + diff --git a/MockKit/MockCGMDataSource.swift b/MockKit/MockCGMDataSource.swift new file mode 100644 index 000000000..973d240c6 --- /dev/null +++ b/MockKit/MockCGMDataSource.swift @@ -0,0 +1,275 @@ +// +// MockCGMDataSource.swift +// LoopKit +// +// Created by Michael Pangburn on 11/23/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit + + +public struct MockCGMDataSource { + public enum Model { + public typealias SineCurveParameters = (baseGlucose: HKQuantity, amplitude: HKQuantity, period: TimeInterval, referenceDate: Date) + + case constant(_ glucose: HKQuantity) + case sineCurve(parameters: SineCurveParameters) + case noData + } + + public struct Effects { + public typealias RandomOutlier = (chance: Double, delta: HKQuantity) + + public var glucoseNoise: HKQuantity? + public var randomLowOutlier: RandomOutlier? + public var randomHighOutlier: RandomOutlier? + public var randomErrorChance: Double? + + public init( + glucoseNoise: HKQuantity? = nil, + randomLowOutlier: RandomOutlier? = nil, + randomHighOutlier: RandomOutlier? = nil, + randomErrorChance: Double? = nil + ) { + self.glucoseNoise = glucoseNoise + self.randomLowOutlier = randomLowOutlier + self.randomHighOutlier = randomHighOutlier + self.randomErrorChance = randomErrorChance + } + } + + static let device = HKDevice( + name: MockCGMManager.managerIdentifier, + manufacturer: nil, + model: nil, + hardwareVersion: nil, + firmwareVersion: nil, + softwareVersion: String(LoopKitVersionNumber), + localIdentifier: nil, + udiDeviceIdentifier: nil + ) + + public var model: Model { + didSet { + glucoseProvider = MockGlucoseProvider(model: model, effects: effects) + } + } + + public var effects: Effects { + didSet { + glucoseProvider = MockGlucoseProvider(model: model, effects: effects) + } + } + + private var glucoseProvider: MockGlucoseProvider + + private var lastFetchedData = Locked(Date.distantPast) + + let dataPointFrequency: TimeInterval + + public init( + model: Model, + effects: Effects = .init(), + dataPointFrequency: TimeInterval = /* minutes */ 5 * 60 + ) { + self.model = model + self.effects = effects + self.glucoseProvider = MockGlucoseProvider(model: model, effects: effects) + self.dataPointFrequency = dataPointFrequency + } + + func fetchNewData(_ completion: @escaping (CGMResult) -> Void) { + let now = Date() + // Give 5% wiggle room for producing data points + let bufferedFrequency = dataPointFrequency - 0.05 * dataPointFrequency + if now.timeIntervalSince(lastFetchedData.value) < bufferedFrequency { + completion(.noData) + return + } + + lastFetchedData.value = now + glucoseProvider.fetchData(at: now, completion: completion) + } + + func backfillData(from interval: DateInterval, completion: @escaping (CGMResult) -> Void) { + lastFetchedData.value = interval.end + let request = MockGlucoseProvider.BackfillRequest(datingBack: interval.duration, dataPointFrequency: dataPointFrequency) + glucoseProvider.backfill(request, endingAt: interval.end, completion: completion) + } +} + +extension MockCGMDataSource: RawRepresentable { + public typealias RawValue = [String: Any] + + public init?(rawValue: RawValue) { + guard + let model = (rawValue["model"] as? Model.RawValue).flatMap(Model.init(rawValue:)), + let effects = (rawValue["effects"] as? Effects.RawValue).flatMap(Effects.init(rawValue:)) + else { + return nil + } + + self.init(model: model, effects: effects) + } + + public var rawValue: RawValue { + return [ + "model": model.rawValue, + "effects": effects.rawValue + ] + } +} + +extension MockCGMDataSource.Model: RawRepresentable { + public typealias RawValue = [String: Any] + + private enum Kind: String { + case constant = "constant" + case sineCurve = "sineCurve" + case noData = "noData" + } + + private static let unit = HKUnit.milligramsPerDeciliter + + public init?(rawValue: RawValue) { + guard + let kindRawValue = rawValue["kind"] as? Kind.RawValue, + let kind = Kind(rawValue: kindRawValue) + else { + return nil + } + + let unit = MockCGMDataSource.Model.unit + func glucose(forKey key: String) -> HKQuantity? { + guard let doubleValue = rawValue[key] as? Double else { + return nil + } + return HKQuantity(unit: unit, doubleValue: doubleValue) + } + + switch kind { + case .constant: + guard let quantity = glucose(forKey: "quantity") else { + return nil + } + self = .constant(quantity) + case .sineCurve: + guard + let baseGlucose = glucose(forKey: "baseGlucose"), + let amplitude = glucose(forKey: "amplitude"), + let period = rawValue["period"] as? TimeInterval, + let referenceDateSeconds = rawValue["referenceDate"] as? TimeInterval + else { + return nil + } + + let referenceDate = Date(timeIntervalSince1970: referenceDateSeconds) + self = .sineCurve(parameters: (baseGlucose: baseGlucose, amplitude: amplitude, period: period, referenceDate: referenceDate)) + case .noData: + self = .noData + } + } + + public var rawValue: RawValue { + var rawValue: RawValue = ["kind": kind.rawValue] + + let unit = MockCGMDataSource.Model.unit + switch self { + case .constant(let quantity): + rawValue["quantity"] = quantity.doubleValue(for: unit) + case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: let period, referenceDate: let referenceDate)): + rawValue["baseGlucose"] = baseGlucose.doubleValue(for: unit) + rawValue["amplitude"] = amplitude.doubleValue(for: unit) + rawValue["period"] = period + rawValue["referenceDate"] = referenceDate.timeIntervalSince1970 + case .noData: + break + } + + return rawValue + } + + private var kind: Kind { + switch self { + case .constant: + return .constant + case .sineCurve: + return .sineCurve + case .noData: + return .noData + } + } +} + +extension MockCGMDataSource.Effects: RawRepresentable { + public typealias RawValue = [String: Any] + + private static let unit = HKUnit.milligramsPerDeciliter + + public init?(rawValue: RawValue) { + self.init() + + let unit = MockCGMDataSource.Effects.unit + func randomOutlier(forKey key: String) -> RandomOutlier? { + guard + let outlier = rawValue[key] as? [String: Double], + let chance = outlier["chance"], + let delta = outlier["delta"] + else { + return nil + } + + return (chance: chance, delta: HKQuantity(unit: unit, doubleValue: delta)) + } + + if let glucoseNoise = rawValue["glucoseNoise"] as? Double { + self.glucoseNoise = HKQuantity(unit: unit, doubleValue: glucoseNoise) + } + + self.randomLowOutlier = randomOutlier(forKey: "randomLowOutlier") + self.randomHighOutlier = randomOutlier(forKey: "randomHighOutlier") + self.randomErrorChance = rawValue["randomErrorChance"] as? Double + } + + public var rawValue: RawValue { + var rawValue: RawValue = [:] + + let unit = MockCGMDataSource.Effects.unit + func insertOutlier(_ outlier: RandomOutlier, forKey key: String) { + rawValue[key] = [ + "chance": outlier.chance, + "delta": outlier.delta.doubleValue(for: unit) + ] + } + + if let glucoseNoise = glucoseNoise { + rawValue["glucoseNoise"] = glucoseNoise.doubleValue(for: unit) + } + + if let randomLowOutlier = randomLowOutlier { + insertOutlier(randomLowOutlier, forKey: "randomLowOutlier") + } + + if let randomHighOutlier = randomHighOutlier { + insertOutlier(randomHighOutlier, forKey: "randomHighOutlier") + } + + if let randomErrorChance = randomErrorChance { + rawValue["randomErrorChance"] = randomErrorChance + } + + return rawValue + } +} + +extension MockCGMDataSource: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + ## MockCGMDataSource + * model: \(model) + * effects: \(effects) + """ + } +} diff --git a/MockKit/MockCGMManager.swift b/MockKit/MockCGMManager.swift new file mode 100644 index 000000000..c6fae0431 --- /dev/null +++ b/MockKit/MockCGMManager.swift @@ -0,0 +1,160 @@ +// +// MockCGMManager.swift +// LoopKit +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit +import LoopTestingKit + + +public struct MockCGMState: SensorDisplayable { + public var isStateValid: Bool + + public var trendType: GlucoseTrend? + + public var isLocal: Bool { + return true + } +} + +public final class MockCGMManager: TestingCGMManager { + public static let managerIdentifier = "MockCGMManager" + public static let localizedTitle = "Simulator" + + public var mockSensorState: MockCGMState { + didSet { + cgmManagerDelegate?.cgmManagerDidUpdateState(self) + } + } + + public var sensorState: SensorDisplayable? { + return mockSensorState + } + + public var testingDevice: HKDevice { + return MockCGMDataSource.device + } + + public var device: HKDevice? { + return testingDevice + } + + public var cgmManagerDelegate: CGMManagerDelegate? + + public var dataSource: MockCGMDataSource { + didSet { + cgmManagerDelegate?.cgmManagerDidUpdateState(self) + } + } + + private var glucoseUpdateTimer: Timer? + + public init?(rawState: RawStateValue) { + if let mockSensorStateRawValue = rawState["mockSensorState"] as? MockCGMState.RawValue, + let mockSensorState = MockCGMState(rawValue: mockSensorStateRawValue) { + self.mockSensorState = mockSensorState + } else { + self.mockSensorState = MockCGMState(isStateValid: true, trendType: nil) + } + + if let dataSourceRawValue = rawState["dataSource"] as? MockCGMDataSource.RawValue, + let dataSource = MockCGMDataSource(rawValue: dataSourceRawValue) { + self.dataSource = dataSource + } else { + self.dataSource = MockCGMDataSource(model: .noData) + } + + setupGlucoseUpdateTimer() + } + + deinit { + glucoseUpdateTimer?.invalidate() + } + + public var rawState: RawStateValue { + return [ + "mockSensorState": mockSensorState.rawValue, + "dataSource": dataSource.rawValue + ] + } + + public let appURL: URL? = nil + + public let providesBLEHeartbeat = false + + public let managedDataInterval: TimeInterval? = nil + + public let shouldSyncToRemoteService = false + + public func fetchNewDataIfNeeded(_ completion: @escaping (CGMResult) -> Void) { + dataSource.fetchNewData(completion) + } + + public func backfillData(datingBack duration: TimeInterval) { + let now = Date() + dataSource.backfillData(from: DateInterval(start: now.addingTimeInterval(-duration), end: now)) { result in + self.cgmManagerDelegate?.cgmManager(self, didUpdateWith: result) + } + } + + private func setupGlucoseUpdateTimer() { + glucoseUpdateTimer = Timer.scheduledTimer(withTimeInterval: dataSource.dataPointFrequency, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.dataSource.fetchNewData { result in + self.cgmManagerDelegate?.cgmManager(self, didUpdateWith: result) + } + } + } +} + +extension MockCGMManager { + public var debugDescription: String { + return """ + ## MockCGMManager + state: \(mockSensorState) + dataSource: \(dataSource) + """ + } +} + +extension MockCGMState: RawRepresentable { + public typealias RawValue = [String: Any] + + public init?(rawValue: RawValue) { + guard let isStateValid = rawValue["isStateValid"] as? Bool else { + return nil + } + + self.isStateValid = isStateValid + + if let trendTypeRawValue = rawValue["trendType"] as? GlucoseTrend.RawValue { + self.trendType = GlucoseTrend(rawValue: trendTypeRawValue) + } + } + + public var rawValue: RawValue { + var rawValue: RawValue = [ + "isStateValid": isStateValid, + ] + + if let trendType = trendType { + rawValue["trendType"] = trendType.rawValue + } + + return rawValue + } +} + +extension MockCGMState: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + ## MockCGMState + * isStateValid: \(isStateValid) + * trendType: \(trendType as Any) + """ + } +} diff --git a/MockKit/MockGlucoseProvider.swift b/MockKit/MockGlucoseProvider.swift new file mode 100644 index 000000000..d7c046d36 --- /dev/null +++ b/MockKit/MockGlucoseProvider.swift @@ -0,0 +1,225 @@ +// +// MockGlucoseProvider.swift +// LoopKit +// +// Created by Michael Pangburn on 11/23/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit + + +/// Returns a value based on the result of a random coin flip. +/// - Parameter chanceOfHeads: The chance of flipping heads. Must be a value in the range `0...1`. Defaults to `0.5`. +/// - Parameter valueIfHeads: An autoclosure producing the value to return if the coin flips heads. +/// - Parameter valueIfTails: An autoclosure producing the value to return if the coin flips tails. +private func coinFlip( + withChanceOfHeads chanceOfHeads: Double = 0.5, + ifHeads valueIfHeads: @autoclosure () -> Output, + ifTails valueIfTails: @autoclosure () -> Output +) -> Output { + precondition((0...1).contains(chanceOfHeads)) + let isHeads = .random(in: 0..<100) < chanceOfHeads * 100 + return isHeads ? valueIfHeads() : valueIfTails() +} + + +struct MockGlucoseProvider { + struct BackfillRequest { + let duration: TimeInterval + let dataPointFrequency: TimeInterval + + var dataPointCount: Int { + return Int(duration / dataPointFrequency) + } + + init(datingBack duration: TimeInterval, dataPointFrequency: TimeInterval) { + self.duration = duration + self.dataPointFrequency = dataPointFrequency + } + } + + /// Given a date, asynchronously produce the CGMResult at that date. + private let fetchDataAt: (_ date: Date, _ completion: @escaping (CGMResult) -> Void) -> Void + + func fetchData(at date: Date, completion: @escaping (CGMResult) -> Void) { + fetchDataAt(date, completion) + } + + func backfill(_ backfill: BackfillRequest, endingAt date: Date, completion: @escaping (CGMResult) -> Void) { + let dataPointDates = (0...backfill.dataPointCount).map { offset in + return date.addingTimeInterval(-backfill.dataPointFrequency * Double(offset)) + } + dataPointDates.asyncMap(fetchDataAt) { allResults in + let allSamples = allResults.flatMap { result -> [NewGlucoseSample] in + if case .newData(let samples) = result { + return samples + } else { + return [] + } + } + let result: CGMResult = allSamples.isEmpty ? .noData : .newData(allSamples) + completion(result) + } + } +} + +extension MockGlucoseProvider { + init(model: MockCGMDataSource.Model, effects: MockCGMDataSource.Effects) { + self = effects.transformations.reduce(model.glucoseProvider) { model, transform in transform(model) } + } + + private static func glucoseSample(at date: Date, quantity: HKQuantity) -> NewGlucoseSample { + return NewGlucoseSample( + date: date, + quantity: quantity, + isDisplayOnly: false, + syncIdentifier: UUID().uuidString, + device: MockCGMDataSource.device + ) + } +} + +// MARK: - Models + +extension MockGlucoseProvider { + fileprivate static func constant(_ quantity: HKQuantity) -> MockGlucoseProvider { + return MockGlucoseProvider { date, completion in + let sample = glucoseSample(at: date, quantity: quantity) + completion(.newData([sample])) + } + } + + fileprivate static func sineCurve(parameters: MockCGMDataSource.Model.SineCurveParameters) -> MockGlucoseProvider { + let (baseGlucose, amplitude, period, referenceDate) = parameters + precondition(period > 0) + let unit = HKUnit.milligramsPerDeciliter + precondition(baseGlucose.is(compatibleWith: unit)) + precondition(amplitude.is(compatibleWith: unit)) + let baseGlucoseValue = baseGlucose.doubleValue(for: unit) + let amplitudeValue = amplitude.doubleValue(for: unit) + + return MockGlucoseProvider { date, completion in + let timeOffset = date.timeIntervalSince1970 - referenceDate.timeIntervalSince1970 + let glucoseValue = baseGlucoseValue + amplitudeValue * sin(2 * .pi / period * timeOffset) + let sample = glucoseSample(at: date, quantity: HKQuantity(unit: unit, doubleValue: glucoseValue)) + completion(.newData([sample])) + } + } + + fileprivate static var noData: MockGlucoseProvider { + return MockGlucoseProvider { _, completion in completion(.noData) } + } + + fileprivate static func error(_ error: Error) -> MockGlucoseProvider { + return MockGlucoseProvider { _, completion in completion(.error(error)) } + } +} + +// MARK: - Effects + +private struct MockGlucoseProviderError: Error { } + +extension MockGlucoseProvider { + fileprivate func withRandomNoise(upTo magnitude: HKQuantity) -> MockGlucoseProvider { + let unit = HKUnit.milligramsPerDeciliter + precondition(magnitude.is(compatibleWith: unit)) + let magnitude = magnitude.doubleValue(for: unit) + + return mapGlucoseQuantities { glucose in + let glucoseValue = glucose.doubleValue(for: unit) + .random(in: -magnitude...magnitude) + return HKQuantity(unit: unit, doubleValue: glucoseValue) + } + } + + fileprivate func randomlyProducingLowOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider { + return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: -) + } + + fileprivate func randomlyProducingHighOutlier(withChance chanceOfOutlier: Double, outlierDelta: HKQuantity) -> MockGlucoseProvider { + return randomlyProducingOutlier(withChance: chanceOfOutlier, outlierDeltaMagnitude: outlierDelta, outlierDeltaSign: +) + } + + private func randomlyProducingOutlier( + withChance chanceOfOutlier: Double, + outlierDeltaMagnitude: HKQuantity, + outlierDeltaSign: (Double) -> Double + ) -> MockGlucoseProvider { + let unit = HKUnit.milligramsPerDeciliter + precondition(outlierDeltaMagnitude.is(compatibleWith: unit)) + let outlierDelta = outlierDeltaSign(outlierDeltaMagnitude.doubleValue(for: unit)) + return mapGlucoseQuantities { glucose in + return coinFlip( + withChanceOfHeads: chanceOfOutlier, + ifHeads: HKQuantity(unit: unit, doubleValue: glucose.doubleValue(for: unit) + outlierDelta), + ifTails: glucose + ) + } + } + + fileprivate func randomlyErroringOnNewData(withChance chance: Double) -> MockGlucoseProvider { + return mapResult { result in + return coinFlip(withChanceOfHeads: chance, ifHeads: .error(MockGlucoseProviderError()), ifTails: result) + } + } + + private func mapResult(_ transform: @escaping (CGMResult) -> CGMResult) -> MockGlucoseProvider { + return MockGlucoseProvider { date, completion in + self.fetchData(at: date) { result in + completion(transform(result)) + } + } + } + + private func mapGlucoseQuantities(_ transform: @escaping (HKQuantity) -> HKQuantity) -> MockGlucoseProvider { + return mapResult { result in + return result.mapGlucoseQuantities(transform) + } + } +} + +private extension CGMResult { + func mapGlucoseQuantities(_ transform: (HKQuantity) -> HKQuantity) -> CGMResult { + guard case .newData(let samples) = self else { + return self + } + return .newData( + samples.map { sample in + return NewGlucoseSample( + date: sample.date, + quantity: transform(sample.quantity), + isDisplayOnly: sample.isDisplayOnly, + syncIdentifier: sample.syncIdentifier, + syncVersion: sample.syncVersion, + device: sample.device + ) + } + ) + } +} + +private extension MockCGMDataSource.Model { + var glucoseProvider: MockGlucoseProvider { + switch self { + case .constant(let quantity): + return .constant(quantity) + case .sineCurve(parameters: let parameters): + return .sineCurve(parameters: parameters) + case .noData: + return .noData + } + } +} + +private extension MockCGMDataSource.Effects { + var transformations: [(MockGlucoseProvider) -> MockGlucoseProvider] { + // Each effect maps to a transformation on a MockGlucoseProvider + return [ + glucoseNoise.map { maximumDeltaMagnitude in { $0.withRandomNoise(upTo: maximumDeltaMagnitude) } }, + randomLowOutlier.map { chance, delta in { $0.randomlyProducingLowOutlier(withChance: chance, outlierDelta: delta) } }, + randomHighOutlier.map { chance, delta in { $0.randomlyProducingHighOutlier(withChance: chance, outlierDelta: delta) } }, + randomErrorChance.map { chance in { $0.randomlyErroringOnNewData(withChance: chance) } } + ].compactMap { $0 } + } +} diff --git a/MockKit/MockKit.h b/MockKit/MockKit.h new file mode 100644 index 000000000..5804e5e4f --- /dev/null +++ b/MockKit/MockKit.h @@ -0,0 +1,19 @@ +// +// MockKit.h +// MockKit +// +// Created by Michael Pangburn on 12/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +#import + +//! Project version number for MockKit. +FOUNDATION_EXPORT double MockKitVersionNumber; + +//! Project version string for MockKit. +FOUNDATION_EXPORT const unsigned char MockKitVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/MockKit/MockPumpManager.swift b/MockKit/MockPumpManager.swift new file mode 100644 index 000000000..0298496b3 --- /dev/null +++ b/MockKit/MockPumpManager.swift @@ -0,0 +1,337 @@ +// +// MockPumpManager.swift +// LoopKit +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit +import LoopTestingKit + + +public protocol MockPumpManagerStateObserver { + func mockPumpManager(_ manager: MockPumpManager, didUpdateState state: MockPumpManagerState) + func mockPumpManager(_ manager: MockPumpManager, didUpdateStatus status: PumpManagerStatus) +} + +public struct MockPumpManagerState { + public var reservoirUnitsRemaining: Double + public var tempBasalEnactmentShouldError: Bool + public var bolusEnactmentShouldError: Bool + public var deliverySuspensionShouldError: Bool + public var deliveryResumptionShouldError: Bool +} + +private enum MockPumpManagerError: LocalizedError { + case pumpSuspended + case communicationFailure + + var failureReason: String? { + switch self { + case .pumpSuspended: + return "Pump is suspended" + case .communicationFailure: + return "Unable to communicate with pump" + } + } +} + +public final class MockPumpManager: TestingPumpManager { + public static let managerIdentifier = "MockPumpManager" + public static let localizedTitle = "Simulator" + private static let device = HKDevice( + name: MockPumpManager.managerIdentifier, + manufacturer: nil, + model: nil, + hardwareVersion: nil, + firmwareVersion: nil, + softwareVersion: String(LoopKitVersionNumber), + localIdentifier: nil, + udiDeviceIdentifier: nil + ) + + private static let deliveryUnitsPerMinute = 1.5 + private static let pulsesPerUnit: Double = 20 + private static let pumpReservoirCapacity: Double = 200 + + public var pumpReservoirCapacity: Double { + return MockPumpManager.pumpReservoirCapacity + } + + public var supportedBasalRates: [Double] { + return (0...700).map { Double($0) / Double(type(of: self).pulsesPerUnit) } + } + + public var maximumBasalScheduleEntryCount: Int { + return 48 + } + + public var minimumBasalScheduleEntryDuration: TimeInterval { + return .minutes(30) + } + + public var testingDevice: HKDevice { + return type(of: self).device + } + + public var status: PumpManagerStatus { + didSet { + statusObservers.forEach { $0.pumpManager(self, didUpdate: status) } + stateObservers.forEach { $0.mockPumpManager(self, didUpdateStatus: status) } + pumpManagerDelegate?.pumpManager(self, didUpdate: status) + pumpManagerDelegate?.pumpManagerDidUpdateState(self) + } + } + + public var state: MockPumpManagerState { + didSet { + stateObservers.forEach { $0.mockPumpManager(self, didUpdateState: state) } + if state.reservoirUnitsRemaining != oldValue.reservoirUnitsRemaining { + pumpManagerDelegate?.pumpManager(self, didReadReservoirValue: state.reservoirUnitsRemaining, at: Date()) { result in + // nothing to do here + } + } + pumpManagerDelegate?.pumpManagerDidUpdateState(self) + } + } + + public var maximumBasalRatePerHour: Double = 5 + public var maximumBolus: Double = 25 + + public var pumpManagerDelegate: PumpManagerDelegate? + + private var statusObservers = WeakSet() + private var stateObservers = WeakSet() + + private var pendingPumpEvents: [NewPumpEvent] = [] + + public init() { + status = PumpManagerStatus(timeZone: .current, device: MockPumpManager.device, pumpBatteryChargeRemaining: 1, basalDeliveryState: .active, bolusState: .none) + state = MockPumpManagerState(reservoirUnitsRemaining: MockPumpManager.pumpReservoirCapacity, tempBasalEnactmentShouldError: false, bolusEnactmentShouldError: false, deliverySuspensionShouldError: false, deliveryResumptionShouldError: false) + } + + public init?(rawState: RawStateValue) { + guard let state = (rawState["state"] as? MockPumpManagerState.RawValue).flatMap(MockPumpManagerState.init(rawValue:)) else { + return nil + } + let pumpBatteryChargeRemaining = rawState["pumpBatteryChargeRemaining"] as? Double ?? 1 + + self.status = PumpManagerStatus(timeZone: .current, device: MockPumpManager.device, pumpBatteryChargeRemaining: pumpBatteryChargeRemaining, basalDeliveryState: .active, bolusState: .none) + self.state = state + } + + public var rawState: RawStateValue { + var raw: RawStateValue = ["state": state.rawValue] + if let pumpBatteryChargeRemaining = status.pumpBatteryChargeRemaining { + raw["pumpBatteryChargeRemaining"] = pumpBatteryChargeRemaining + } + return raw + } + + public var pumpRecordsBasalProfileStartEvents: Bool { + return false + } + + public func addStatusObserver(_ observer: PumpManagerStatusObserver) { + statusObservers.insert(observer) + } + + public func addStateObserver(_ observer: MockPumpManagerStateObserver) { + stateObservers.insert(observer) + } + + public func removeStatusObserver(_ observer: PumpManagerStatusObserver) { + statusObservers.remove(observer) + } + + public func assertCurrentPumpData() { + pumpManagerDelegate?.pumpManager(self, didReadPumpEvents: pendingPumpEvents) { [weak self] error in + guard let self = self else { return } + self.pumpManagerDelegate?.pumpManagerRecommendsLoop(self) + } + + let totalInsulinUsage = pendingPumpEvents.reduce(into: 0 as Double) { total, event in + if let units = event.dose?.units { + total += units + } + } + + DispatchQueue.main.async { + self.state.reservoirUnitsRemaining -= totalInsulinUsage + } + + pendingPumpEvents.removeAll() + } + + public func enactTempBasal(unitsPerHour: Double, for duration: TimeInterval, completion: @escaping (PumpManagerResult) -> Void) { + if state.tempBasalEnactmentShouldError { + completion(.failure(PumpManagerError.communication(MockPumpManagerError.communicationFailure))) + } else { + let temp = NewPumpEvent.tempBasal(at: Date(), for: duration, unitsPerHour: unitsPerHour) + pendingPumpEvents.append(temp) + completion(.success(temp.dose!)) + } + } + + public func enactBolus(units: Double, at startDate: Date, willRequest: @escaping (DoseEntry) -> Void, completion: @escaping (PumpManagerResult) -> Void) { + + if state.bolusEnactmentShouldError { + completion(.failure(SetBolusError.certain(PumpManagerError.communication(MockPumpManagerError.communicationFailure)))) + } else { + guard status.basalDeliveryState != .suspended else { + completion(.failure(SetBolusError.certain(PumpManagerError.deviceState(MockPumpManagerError.pumpSuspended)))) + return + } + let bolus = NewPumpEvent.bolus(at: Date(), units: units, deliveryUnitsPerMinute: type(of: self).deliveryUnitsPerMinute) + pendingPumpEvents.append(bolus) + willRequest(bolus.dose!) + completion(.success(bolus.dose!)) + } + } + + public func roundToDeliveryIncrement(units: Double) -> Double { + return round(units * MockPumpManager.pulsesPerUnit) / MockPumpManager.pulsesPerUnit + } + + public func updateBLEHeartbeatPreference() { + // nothing to do here + } + + public func suspendDelivery(completion: @escaping (Error?) -> Void) { + let previousState = status.basalDeliveryState + status.basalDeliveryState = .suspending + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { + if self.state.deliverySuspensionShouldError { + self.status.basalDeliveryState = previousState + completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure)) + } else { + let suspend = NewPumpEvent.suspend(at: Date()) + self.pendingPumpEvents.append(suspend) + self.status.basalDeliveryState = .suspended + completion(nil) + } + } + } + + public func resumeDelivery(completion: @escaping (Error?) -> Void) { + let previousState = status.basalDeliveryState + status.basalDeliveryState = .resuming + + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { + if self.state.deliveryResumptionShouldError { + self.status.basalDeliveryState = previousState + completion(PumpManagerError.communication(MockPumpManagerError.communicationFailure)) + } else { + let resume = NewPumpEvent.resume(at: Date()) + self.pendingPumpEvents.append(resume) + self.status.basalDeliveryState = .active + completion(nil) + } + } + } +} + +extension MockPumpManager { + public var debugDescription: String { + return """ + ## MockPumpManager + status: \(status) + state: \(status) + pendingPumpEvents: \(pendingPumpEvents) + """ + } +} + +private extension NewPumpEvent { + static func bolus(at date: Date, units: Double, deliveryUnitsPerMinute: Double) -> NewPumpEvent { + let dose = DoseEntry( + type: .bolus, + startDate: date, + endDate: date.addingTimeInterval(.minutes(units / deliveryUnitsPerMinute)), + value: units, + unit: .units + ) + return NewPumpEvent(date: date, dose: dose, isMutable: false, raw: newDataIdentifier(), title: "Bolus", type: .bolus) + } + + static func tempBasal(at date: Date, for duration: TimeInterval, unitsPerHour: Double) -> NewPumpEvent { + let dose = DoseEntry( + type: .basal, + startDate: date, + endDate: date.addingTimeInterval(duration), + value: unitsPerHour, + unit: .unitsPerHour + ) + return NewPumpEvent(date: date, dose: dose, isMutable: false, raw: newDataIdentifier(), title: "Temp Basal", type: .tempBasal) + } + + static func suspend(at date: Date) -> NewPumpEvent { + let dose = DoseEntry(suspendDate: date) + return NewPumpEvent(date: date, dose: dose, isMutable: false, raw: newDataIdentifier(), title: "Suspend", type: .suspend) + } + + static func resume(at date: Date) -> NewPumpEvent { + let dose = DoseEntry(resumeDate: date) + return NewPumpEvent(date: date, dose: dose, isMutable: false, raw: newDataIdentifier(), title: "Resume", type: .resume) + } + + private static func newDataIdentifier() -> Data { + return UUID().uuidString.data(using: .utf8)! + } +} + +extension MockPumpManagerState: RawRepresentable { + public typealias RawValue = [String: Any] + + public init?(rawValue: RawValue) { + guard let reservoirUnitsRemaining = rawValue["reservoirUnitsRemaining"] as? Double else { + return nil + } + + self.reservoirUnitsRemaining = reservoirUnitsRemaining + self.tempBasalEnactmentShouldError = rawValue["tempBasalEnactmentShouldError"] as? Bool ?? false + self.bolusEnactmentShouldError = rawValue["bolusEnactmentShouldError"] as? Bool ?? false + self.deliverySuspensionShouldError = rawValue["deliverySuspensionShouldError"] as? Bool ?? false + self.deliveryResumptionShouldError = rawValue["deliveryResumptionShouldError"] as? Bool ?? false + } + + public var rawValue: RawValue { + var raw: RawValue = [ + "reservoirUnitsRemaining": reservoirUnitsRemaining + ] + + if tempBasalEnactmentShouldError { + raw["tempBasalEnactmentShouldError"] = true + } + + if bolusEnactmentShouldError { + raw["bolusEnactmentShouldError"] = true + } + + if deliverySuspensionShouldError { + raw["deliverySuspensionShouldError"] = true + } + + if deliveryResumptionShouldError { + raw["deliveryResumptionShouldError"] = true + } + + return raw + } +} + +extension MockPumpManagerState: CustomDebugStringConvertible { + public var debugDescription: String { + return """ + ## MockPumpManagerState + * reservoirUnitsRemaining: \(reservoirUnitsRemaining) + * tempBasalEnactmentShouldError: \(tempBasalEnactmentShouldError) + * bolusEnactmentShouldError: \(bolusEnactmentShouldError) + * deliverySuspensionShouldError: \(deliverySuspensionShouldError) + * deliveryResumptionShouldError: \(deliveryResumptionShouldError) + """ + } +} diff --git a/MockKitTests/Info.plist b/MockKitTests/Info.plist new file mode 100644 index 000000000..6c40a6cd0 --- /dev/null +++ b/MockKitTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/MockKitTests/MockKitTests.swift b/MockKitTests/MockKitTests.swift new file mode 100644 index 000000000..7013d97ab --- /dev/null +++ b/MockKitTests/MockKitTests.swift @@ -0,0 +1,34 @@ +// +// MockKitTests.swift +// MockKitTests +// +// Created by Michael Pangburn on 12/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import XCTest +@testable import MockKit + +class MockKitTests: XCTestCase { + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/MockKitUI/Assets.xcassets/Contents.json b/MockKitUI/Assets.xcassets/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/MockKitUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MockKitUI/Assets.xcassets/Simulator Small.imageset/7xx Small Outline.pdf b/MockKitUI/Assets.xcassets/Simulator Small.imageset/7xx Small Outline.pdf new file mode 100644 index 000000000..a5439cd58 Binary files /dev/null and b/MockKitUI/Assets.xcassets/Simulator Small.imageset/7xx Small Outline.pdf differ diff --git a/MockKitUI/Assets.xcassets/Simulator Small.imageset/Contents.json b/MockKitUI/Assets.xcassets/Simulator Small.imageset/Contents.json new file mode 100644 index 000000000..41bac9713 --- /dev/null +++ b/MockKitUI/Assets.xcassets/Simulator Small.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "7xx Small Outline.pdf", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/MockKitUI/Extensions/UIColor.swift b/MockKitUI/Extensions/UIColor.swift new file mode 100644 index 000000000..f8b88c4af --- /dev/null +++ b/MockKitUI/Extensions/UIColor.swift @@ -0,0 +1,21 @@ +// +// UIColor.swift +// LoopKitUI +// +// Copyright © 2018 LoopKit Authors. All rights reserved. +// +import UIKit + + +extension UIColor { + static let delete = UIColor.higRed() +} + + +// MARK: - HIG colors +// See: https://developer.apple.com/ios/human-interface-guidelines/visual-design/color/ +extension UIColor { + private static func higRed() -> UIColor { + return UIColor(red: 1, green: 59 / 255, blue: 48 / 255, alpha: 1) + } +} diff --git a/MockKitUI/Info.plist b/MockKitUI/Info.plist new file mode 100644 index 000000000..e07dbec16 --- /dev/null +++ b/MockKitUI/Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + PumpManagerUI + MockPumpManager + CGMManagerUI + MockCGMManager + + diff --git a/MockKitUI/MockCGMManager+UI.swift b/MockKitUI/MockCGMManager+UI.swift new file mode 100644 index 000000000..ad3e3252f --- /dev/null +++ b/MockKitUI/MockCGMManager+UI.swift @@ -0,0 +1,30 @@ +// +// MockCGMManager+UI.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/23/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +extension MockCGMManager: CGMManagerUI { + public static func setupViewController() -> (UIViewController & CGMManagerSetupViewController & CompletionNotifying)? { + return nil + } + + public func settingsViewController(for glucoseUnit: HKUnit) -> (UIViewController & CompletionNotifying) { + let settings = MockCGMManagerSettingsViewController(cgmManager: self, glucoseUnit: glucoseUnit) + let nav = SettingsNavigationViewController(rootViewController: settings) + return nav + } + + public var smallImage: UIImage? { + return nil + } +} diff --git a/MockKitUI/MockHUDProvider.swift b/MockKitUI/MockHUDProvider.swift new file mode 100644 index 000000000..f251b9e77 --- /dev/null +++ b/MockKitUI/MockHUDProvider.swift @@ -0,0 +1,105 @@ +// +// MockHUDProvider.swift +// MockKitUI +// +// Created by Michael Pangburn on 3/5/19. +// Copyright © 2019 LoopKit Authors. All rights reserved. +// + +import LoopKit +import LoopKitUI +import MockKit + + +final class MockHUDProvider: NSObject, HUDProvider { + + var managerIdentifier: String { + return MockPumpManager.managerIdentifier + } + + private var pumpManager: MockPumpManager + + private var lastPumpManagerStatus: PumpManagerStatus + + private weak var reservoirView: ReservoirVolumeHUDView? + + private weak var batteryView: BatteryLevelHUDView? + + init(pumpManager: MockPumpManager) { + self.pumpManager = pumpManager + self.lastPumpManagerStatus = pumpManager.status + super.init() + pumpManager.addStateObserver(self) + } + + var visible: Bool = false + + var hudViewsRawState: HUDViewsRawState { + var rawValue: HUDViewsRawState = [ + "pumpReservoirCapacity": pumpManager.pumpReservoirCapacity + ] + + if let pumpBatteryChargeRemaining = lastPumpManagerStatus.pumpBatteryChargeRemaining { + rawValue["pumpBatteryChargeRemaining"] = pumpBatteryChargeRemaining + } + + rawValue["reservoirUnitsRemaining"] = pumpManager.state.reservoirUnitsRemaining + + return rawValue + } + + func createHUDViews() -> [BaseHUDView] { + reservoirView = ReservoirVolumeHUDView.instantiate() + updateReservoirView() + + batteryView = BatteryLevelHUDView.instantiate() + updateBatteryView() + + return [reservoirView, batteryView].compactMap { $0 } + } + + static func createHUDViews(rawValue: HUDViewsRawState) -> [BaseHUDView] { + guard let pumpReservoirCapacity = rawValue["pumpReservoirCapacity"] as? Double else { + return [] + } + + let reservoirVolumeHUDView = ReservoirVolumeHUDView.instantiate() + if let reservoirUnitsRemaining = rawValue["reservoirUnitsRemaining"] as? Double { + let reservoirLevel = (reservoirUnitsRemaining / pumpReservoirCapacity).clamped(to: 0...1) + reservoirVolumeHUDView.level = reservoirLevel + reservoirVolumeHUDView.setReservoirVolume(volume: reservoirUnitsRemaining, at: Date()) + } + + let batteryPercentage = rawValue["pumpBatteryChargeRemaining"] as? Double + let batteryLevelHUDView = BatteryLevelHUDView.instantiate() + batteryLevelHUDView.batteryLevel = batteryPercentage + + return [reservoirVolumeHUDView, batteryLevelHUDView] + } + + func didTapOnHUDView(_ view: BaseHUDView) -> HUDTapAction? { + return nil + } + + private func updateReservoirView() { + let reservoirVolume = pumpManager.state.reservoirUnitsRemaining + let reservoirLevel = (reservoirVolume / pumpManager.pumpReservoirCapacity).clamped(to: 0...1) + reservoirView?.level = reservoirLevel + reservoirView?.setReservoirVolume(volume: reservoirVolume, at: Date()) + } + + private func updateBatteryView() { + batteryView?.batteryLevel = lastPumpManagerStatus.pumpBatteryChargeRemaining + } +} + +extension MockHUDProvider: MockPumpManagerStateObserver { + func mockPumpManager(_ manager: MockPumpManager, didUpdateState state: MockPumpManagerState) { + updateReservoirView() + } + + func mockPumpManager(_ manager: MockPumpManager, didUpdateStatus status: PumpManagerStatus) { + lastPumpManagerStatus = status + updateBatteryView() + } +} diff --git a/MockKitUI/MockKitUI.h b/MockKitUI/MockKitUI.h new file mode 100644 index 000000000..212fdcf67 --- /dev/null +++ b/MockKitUI/MockKitUI.h @@ -0,0 +1,19 @@ +// +// MockKitUI.h +// MockKitUI +// +// Created by Michael Pangburn on 12/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +#import + +//! Project version number for MockKitUI. +FOUNDATION_EXPORT double MockKitUIVersionNumber; + +//! Project version string for MockKitUI. +FOUNDATION_EXPORT const unsigned char MockKitUIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/MockKitUI/MockPumpManager+UI.swift b/MockKitUI/MockPumpManager+UI.swift new file mode 100644 index 000000000..4534cbc65 --- /dev/null +++ b/MockKitUI/MockPumpManager+UI.swift @@ -0,0 +1,75 @@ +// +// MockPumpManager+UI.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import LoopKitUI +import MockKit + + +extension MockPumpManager: PumpManagerUI { + public static func setupViewController() -> (UIViewController & CompletionNotifying & PumpManagerSetupViewController) { + return MockPumpManagerSetupViewController.instantiateFromStoryboard() + } + + public func settingsViewController() -> (UIViewController & CompletionNotifying) { + let settings = MockPumpManagerSettingsViewController(pumpManager: self) + let nav = SettingsNavigationViewController(rootViewController: settings) + return nav + } + + public var smallImage: UIImage? { + return UIImage(named: "Simulator Small", in: Bundle(for: MockPumpManagerSettingsViewController.self), compatibleWith: nil) + } + + public func hudProvider() -> HUDProvider? { + return MockHUDProvider(pumpManager: self) + } + + public static func createHUDViews(rawValue: [String : Any]) -> [BaseHUDView] { + return MockHUDProvider.createHUDViews(rawValue: rawValue) + } +} + +// MARK: - DeliveryLimitSettingsTableViewControllerSyncSource +extension MockPumpManager { + public func syncDeliveryLimitSettings(for viewController: DeliveryLimitSettingsTableViewController, completion: @escaping (DeliveryLimitSettingsResult) -> Void) { + completion(.success(maximumBasalRatePerHour: maximumBasalRatePerHour, maximumBolus: maximumBolus)) + } + + public func syncButtonTitle(for viewController: DeliveryLimitSettingsTableViewController) -> String { + return "Continue" + } + + public func syncButtonDetailText(for viewController: DeliveryLimitSettingsTableViewController) -> String? { + return nil + } + + public func deliveryLimitSettingsTableViewControllerIsReadOnly(_ viewController: DeliveryLimitSettingsTableViewController) -> Bool { + return false + } +} + +// MARK: - BasalScheduleTableViewControllerSyncSource +extension MockPumpManager { + public func syncScheduleValues(for viewController: BasalScheduleTableViewController, completion: @escaping (SyncBasalScheduleResult) -> Void) { + completion(.success(scheduleItems: [RepeatingScheduleValue(startTime: 0, value: 1.0)], timeZone: .current)) + } + + public func syncButtonTitle(for viewController: BasalScheduleTableViewController) -> String { + return "Continue" + } + + public func syncButtonDetailText(for viewController: BasalScheduleTableViewController) -> String? { + return nil + } + + public func basalScheduleTableViewControllerIsReadOnly(_ viewController: BasalScheduleTableViewController) -> Bool { + return false + } +} diff --git a/MockKitUI/View Controllers/GlucoseTrendTableViewController.swift b/MockKitUI/View Controllers/GlucoseTrendTableViewController.swift new file mode 100644 index 000000000..a3d6b7113 --- /dev/null +++ b/MockKitUI/View Controllers/GlucoseTrendTableViewController.swift @@ -0,0 +1,52 @@ +// +// GlucoseTrendTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 12/17/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import LoopKit +import LoopKitUI + + +protocol GlucoseTrendTableViewControllerDelegate: class { + func glucoseTrendTableViewControllerDidChangeTrend(_ controller: GlucoseTrendTableViewController) +} + +final class GlucoseTrendTableViewController: RadioSelectionTableViewController { + + var glucoseTrend: GlucoseTrend? { + get { + if let selectedIndex = selectedIndex { + return GlucoseTrend.allCases[selectedIndex] + } else { + return nil + } + } + set { + if let newValue = newValue { + selectedIndex = GlucoseTrend.allCases.index(of: newValue) + } else { + selectedIndex = nil + } + } + } + + weak var glucoseTrendDelegate: GlucoseTrendTableViewControllerDelegate? + + convenience init() { + self.init(style: .grouped) + options = GlucoseTrend.allCases.map { trend in + "\(trend.symbol) \(trend.localizedDescription)" + } + delegate = self + } +} + +extension GlucoseTrendTableViewController: RadioSelectionTableViewControllerDelegate { + func radioSelectionTableViewControllerDidChangeSelectedIndex(_ controller: RadioSelectionTableViewController) { + glucoseTrendDelegate?.glucoseTrendTableViewControllerDidChangeTrend(self) + } +} diff --git a/MockKitUI/View Controllers/MockCGMManagerSettingsViewController.swift b/MockKitUI/View Controllers/MockCGMManagerSettingsViewController.swift new file mode 100644 index 000000000..c29e975d7 --- /dev/null +++ b/MockKitUI/View Controllers/MockCGMManagerSettingsViewController.swift @@ -0,0 +1,432 @@ +// +// MockCGMManagerSettingsViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/23/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +final class MockCGMManagerSettingsViewController: UITableViewController { + let cgmManager: MockCGMManager + let glucoseUnit: HKUnit + + init(cgmManager: MockCGMManager, glucoseUnit: HKUnit) { + self.cgmManager = cgmManager + self.glucoseUnit = glucoseUnit + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "CGM Settings" + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 44 + + tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className) + tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className) + + let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:))) + self.navigationItem.setRightBarButton(button, animated: false) + } + + @objc func doneTapped(_ sender: Any) { + done() + } + + private func done() { + if let nav = navigationController as? SettingsNavigationViewController { + nav.notifyComplete() + } + if let nav = navigationController as? MockPumpManagerSetupViewController { + nav.finishedSettingsDisplay() + } + } + + // MARK: - Data Source + + private enum Section: Int, CaseIterable { + case model = 0 + case effects + case history + case deleteCGM + } + + private enum ModelRow: Int, CaseIterable { + case constant = 0 + case sineCurve + case noData + } + + private enum EffectsRow: Int, CaseIterable { + case noise = 0 + case lowOutlier + case highOutlier + case error + } + + private enum HistoryRow: Int, CaseIterable { + case trend = 0 + case backfill + } + + // MARK: - UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .model: + return ModelRow.allCases.count + case .effects: + return EffectsRow.allCases.count + case .history: + return HistoryRow.allCases.count + case .deleteCGM: + return 1 + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch Section(rawValue: section)! { + case .model: + return "Model" + case .effects: + return "Effects" + case .history: + return "History" + case .deleteCGM: + return " " // Use an empty string for more dramatic spacing + } + } + + private lazy var quantityFormatter = QuantityFormatter() + + private lazy var percentageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.maximumFractionDigits = 1 + return formatter + }() + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section(rawValue: indexPath.section)! { + case .model: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + switch ModelRow(rawValue: indexPath.row)! { + case .constant: + cell.textLabel?.text = "Constant" + if case .constant(let glucose) = cgmManager.dataSource.model { + cell.detailTextLabel?.text = quantityFormatter.string(from: glucose, for: glucoseUnit) + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .disclosureIndicator + } + case .sineCurve: + cell.textLabel?.text = "Sine Curve" + if case .sineCurve(parameters: (baseGlucose: let baseGlucose, amplitude: let amplitude, period: _, referenceDate: _)) = cgmManager.dataSource.model { + if let baseGlucoseText = quantityFormatter.numberFormatter.string(from: baseGlucose.doubleValue(for: glucoseUnit)), + let amplitudeText = quantityFormatter.string(from: amplitude, for: glucoseUnit) { + cell.detailTextLabel?.text = "\(baseGlucoseText) ± \(amplitudeText)" + } + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .disclosureIndicator + } + case .noData: + cell.textLabel?.text = "No Data" + if case .noData = cgmManager.dataSource.model { + cell.accessoryType = .checkmark + } + } + return cell + case .effects: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + switch EffectsRow(rawValue: indexPath.row)! { + case .noise: + cell.textLabel?.text = "Glucose Noise" + if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise { + cell.detailTextLabel?.text = quantityFormatter.string(from: maximumDeltaMagnitude, for: glucoseUnit) + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + case .lowOutlier: + cell.textLabel?.text = "Random Low Outlier" + if let chance = cgmManager.dataSource.effects.randomLowOutlier?.chance, + let percentageString = percentageFormatter.string(from: chance * 100) + { + cell.detailTextLabel?.text = "\(percentageString)% chance" + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + case .highOutlier: + cell.textLabel?.text = "Random High Outlier" + if let chance = cgmManager.dataSource.effects.randomHighOutlier?.chance, + let percentageString = percentageFormatter.string(from: chance * 100) + { + cell.detailTextLabel?.text = "\(percentageString)% chance" + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + case .error: + cell.textLabel?.text = "Random Error" + if let chance = cgmManager.dataSource.effects.randomErrorChance, + let percentageString = percentageFormatter.string(from: chance * 100) + { + cell.detailTextLabel?.text = "\(percentageString)% chance" + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + } + + cell.accessoryType = .disclosureIndicator + return cell + case .history: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + switch HistoryRow(rawValue: indexPath.row)! { + case .trend: + cell.textLabel?.text = "Trend" + cell.detailTextLabel?.text = cgmManager.mockSensorState.trendType?.symbol + case .backfill: + cell.textLabel?.text = "Backfill Glucose" + } + cell.accessoryType = .disclosureIndicator + return cell + case .deleteCGM: + let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell + cell.textLabel?.text = "Delete CGM" + cell.textLabel?.textAlignment = .center + cell.tintColor = .delete + cell.isEnabled = true + return cell + } + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sender = tableView.cellForRow(at: indexPath) + + switch Section(rawValue: indexPath.section)! { + case .model: + switch ModelRow(rawValue: indexPath.row)! { + case .constant: + let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit) + vc.title = "Constant" + vc.indexPath = indexPath + vc.contextHelp = "A constant glucose model returns a fixed glucose value regardless of context." + vc.glucoseEntryDelegate = self + show(vc, sender: sender) + case .sineCurve: + let vc = SineCurveParametersTableViewController(glucoseUnit: glucoseUnit) + if case .sineCurve(parameters: let parameters) = cgmManager.dataSource.model { + vc.parameters = parameters + } else { + vc.parameters = nil + } + vc.contextHelp = "The sine curve parameters describe a mathematical model for glucose value production." + vc.delegate = self + show(vc, sender: sender) + case .noData: + cgmManager.dataSource.model = .noData + tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic) + } + case .effects: + switch EffectsRow(rawValue: indexPath.row)! { + case .noise: + let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit) + if let maximumDeltaMagnitude = cgmManager.dataSource.effects.glucoseNoise { + vc.glucose = maximumDeltaMagnitude + } + vc.title = "Glucose Noise" + vc.contextHelp = "The magnitude of glucose noise applied to CGM values determines the maximum random amount of variation applied to each glucose value." + vc.indexPath = indexPath + vc.glucoseEntryDelegate = self + show(vc, sender: sender) + case .lowOutlier: + let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit) + vc.title = "Low Outlier" + vc.randomOutlier = cgmManager.dataSource.effects.randomLowOutlier + vc.contextHelp = "Produced glucose values will have a chance of being decreased by the delta quantity." + vc.indexPath = indexPath + vc.delegate = self + show(vc, sender: sender) + case .highOutlier: + let vc = RandomOutlierTableViewController(glucoseUnit: glucoseUnit) + vc.title = "High Outlier" + vc.randomOutlier = cgmManager.dataSource.effects.randomHighOutlier + vc.contextHelp = "Produced glucose values will have a chance of being increased by the delta quantity." + vc.indexPath = indexPath + vc.delegate = self + show(vc, sender: sender) + case .error: + let vc = PercentageTextFieldTableViewController() + if let chance = cgmManager.dataSource.effects.randomErrorChance { + vc.percentage = chance + } + vc.title = "Random Error" + vc.contextHelp = "The percentage determines the chance with which the CGM will error when a glucose value is requested." + vc.indexPath = indexPath + vc.percentageDelegate = self + show(vc, sender: sender) + } + case .history: + switch HistoryRow(rawValue: indexPath.row)! { + case .trend: + let vc = GlucoseTrendTableViewController() + vc.glucoseTrend = cgmManager.mockSensorState.trendType + vc.title = "Glucose Trend" + vc.glucoseTrendDelegate = self + show(vc, sender: sender) + case .backfill: + let vc = DateAndDurationTableViewController() + vc.inputMode = .duration(.hours(3)) + vc.title = "Backfill" + vc.contextHelp = "Performing a backfill will not delete existing prior glucose values." + vc.indexPath = indexPath + vc.onSave { inputMode in + guard case .duration(let duration) = inputMode else { + assertionFailure() + return + } + self.cgmManager.backfillData(datingBack: duration) + } + show(vc, sender: sender) + } + case .deleteCGM: + let confirmVC = UIAlertController(cgmDeletionHandler: { + self.cgmManager.cgmManagerDelegate?.cgmManagerWantsDeletion(self.cgmManager) + self.done() + }) + + present(confirmVC, animated: true) { + tableView.deselectRow(at: indexPath, animated: true) + } + } + } + + private func indexPaths( + forSection section: Section, + rows _: Row.Type + ) -> [IndexPath] where Row.RawValue == Int { + let rows = Row.allCases + return zip(rows, repeatElement(section, count: rows.count)).map { row, section in + return IndexPath(row: row.rawValue, section: section.rawValue) + } + } +} + +extension MockCGMManagerSettingsViewController: GlucoseEntryTableViewControllerDelegate { + func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) { + guard let indexPath = controller.indexPath else { + assertionFailure() + return + } + + tableView.deselectRow(at: indexPath, animated: true) + switch indexPath { + case [Section.model.rawValue, ModelRow.constant.rawValue]: + if let glucose = controller.glucose { + cgmManager.dataSource.model = .constant(glucose) + tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic) + } + case [Section.effects.rawValue, EffectsRow.noise.rawValue]: + if let glucose = controller.glucose { + cgmManager.dataSource.effects.glucoseNoise = glucose + } + tableView.reloadRows(at: [indexPath], with: .automatic) + default: + assertionFailure() + } + } +} + +extension MockCGMManagerSettingsViewController: SineCurveParametersTableViewControllerDelegate { + func sineCurveParametersTableViewControllerDidUpdateParameters(_ controller: SineCurveParametersTableViewController) { + if let parameters = controller.parameters { + cgmManager.dataSource.model = .sineCurve(parameters: parameters) + tableView.reloadRows(at: indexPaths(forSection: .model, rows: ModelRow.self), with: .automatic) + } + } +} + +extension MockCGMManagerSettingsViewController: RandomOutlierTableViewControllerDelegate { + func randomOutlierTableViewControllerDidChangeOutlier(_ controller: RandomOutlierTableViewController) { + guard let indexPath = controller.indexPath else { + assertionFailure() + return + } + + switch indexPath { + case [Section.effects.rawValue, EffectsRow.lowOutlier.rawValue]: + cgmManager.dataSource.effects.randomLowOutlier = controller.randomOutlier + case [Section.effects.rawValue, EffectsRow.highOutlier.rawValue]: + cgmManager.dataSource.effects.randomHighOutlier = controller.randomOutlier + default: + assertionFailure() + } + + tableView.reloadRows(at: [indexPath], with: .automatic) + } +} + +extension MockCGMManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate { + func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) { + guard let indexPath = controller.indexPath else { + assertionFailure() + return + } + + switch indexPath { + case [Section.effects.rawValue, EffectsRow.error.rawValue]: + if let chance = controller.percentage { + cgmManager.dataSource.effects.randomErrorChance = chance.clamped(to: 0...100) + } + tableView.reloadRows(at: [indexPath], with: .automatic) + default: + assertionFailure() + } + } +} + +extension MockCGMManagerSettingsViewController: GlucoseTrendTableViewControllerDelegate { + func glucoseTrendTableViewControllerDidChangeTrend(_ controller: GlucoseTrendTableViewController) { + cgmManager.mockSensorState.trendType = controller.glucoseTrend + tableView.reloadRows(at: [[Section.history.rawValue, HistoryRow.trend.rawValue]], with: .automatic) + } +} + +private extension UIAlertController { + convenience init(cgmDeletionHandler handler: @escaping () -> Void) { + self.init( + title: nil, + message: "Are you sure you want to delete this CGM?", + preferredStyle: .actionSheet + ) + + addAction(UIAlertAction( + title: "Delete CGM", + style: .destructive, + handler: { _ in + handler() + } + )) + + let cancel = "Cancel" + addAction(UIAlertAction(title: cancel, style: .cancel, handler: nil)) + } +} diff --git a/MockKitUI/View Controllers/MockPumpManager.storyboard b/MockKitUI/View Controllers/MockPumpManager.storyboard new file mode 100644 index 000000000..26fe0ee24 --- /dev/null +++ b/MockKitUI/View Controllers/MockPumpManager.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MockKitUI/View Controllers/MockPumpManagerSettingsSetupViewController.swift b/MockKitUI/View Controllers/MockPumpManagerSettingsSetupViewController.swift new file mode 100644 index 000000000..bc059456b --- /dev/null +++ b/MockKitUI/View Controllers/MockPumpManagerSettingsSetupViewController.swift @@ -0,0 +1,156 @@ +// +// MockPumpManagerSettingsSetupViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +final class MockPumpManagerSettingsSetupViewController: SetupTableViewController { + + var pumpManager: MockPumpManager? + + private var pumpManagerSetupViewController: MockPumpManagerSetupViewController? { + return navigationController as? MockPumpManagerSetupViewController + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className) + } + + private lazy var quantityFormatter: QuantityFormatter = { + let quantityFormatter = QuantityFormatter() + quantityFormatter.numberFormatter.minimumFractionDigits = 0 + quantityFormatter.numberFormatter.maximumFractionDigits = 3 + + return quantityFormatter + }() + + // MARK: - Table view data source + + private enum Section: Int, CaseIterable { + case configuration + } + + private enum ConfigurationRow: Int, CaseIterable { + case basalRates + case deliveryLimits + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .configuration: + return ConfigurationRow.allCases.count + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section(rawValue: indexPath.section)! { + case .configuration: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + + switch ConfigurationRow(rawValue: indexPath.row)! { + case .basalRates: + cell.textLabel?.text = "Basal Rates" + + if let basalRateSchedule = pumpManagerSetupViewController?.basalSchedule { + let unit = HKUnit.internationalUnit() + let total = HKQuantity(unit: unit, doubleValue: basalRateSchedule.total()) + cell.detailTextLabel?.text = quantityFormatter.string(from: total, for: unit) + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString + } + case .deliveryLimits: + cell.textLabel?.text = "Delivery Limits" + + if pumpManagerSetupViewController?.maxBolusUnits == nil || pumpManagerSetupViewController?.maxBasalRateUnitsPerHour == nil { + cell.detailTextLabel?.text = SettingsTableViewCell.TapToSetString + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.EnabledString + } + } + + cell.accessoryType = .disclosureIndicator + + return cell + } + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sender = tableView.cellForRow(at: indexPath) + + switch Section(rawValue: indexPath.section)! { + case .configuration: + switch ConfigurationRow(rawValue: indexPath.row)! { + case .basalRates: + guard let pumpManager = pumpManager else { + return + } + let vc = BasalScheduleTableViewController(allowedBasalRates: pumpManager.supportedBasalRates, maximumScheduleItemCount: pumpManager.maximumBasalScheduleEntryCount, minimumTimeInterval: pumpManager.minimumBasalScheduleEntryDuration) + + if let profile = pumpManagerSetupViewController?.basalSchedule { + vc.scheduleItems = profile.items + vc.timeZone = profile.timeZone + } + + vc.title = sender?.textLabel?.text + vc.delegate = self + vc.syncSource = pumpManager + + show(vc, sender: sender) + case .deliveryLimits: + let vc = DeliveryLimitSettingsTableViewController(style: .grouped) + + vc.maximumBasalRatePerHour = pumpManagerSetupViewController?.maxBasalRateUnitsPerHour + vc.maximumBolus = pumpManagerSetupViewController?.maxBolusUnits + + vc.title = sender?.textLabel?.text + vc.delegate = self + vc.syncSource = pumpManager + + show(vc, sender: sender) + } + } + } + + override func continueButtonPressed(_ sender: Any) { + pumpManagerSetupViewController?.completeSetup() + } +} + +extension MockPumpManagerSettingsSetupViewController: DailyValueScheduleTableViewControllerDelegate { + func dailyValueScheduleTableViewControllerWillFinishUpdating(_ controller: DailyValueScheduleTableViewController) { + if let controller = controller as? SingleValueScheduleTableViewController { + pumpManagerSetupViewController?.basalSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone) + } + + tableView.reloadRows(at: [[Section.configuration.rawValue, ConfigurationRow.basalRates.rawValue]], with: .none) + } +} + +extension MockPumpManagerSettingsSetupViewController: DeliveryLimitSettingsTableViewControllerDelegate { + func deliveryLimitSettingsTableViewControllerDidUpdateMaximumBasalRatePerHour(_ vc: DeliveryLimitSettingsTableViewController) { + pumpManagerSetupViewController?.maxBasalRateUnitsPerHour = vc.maximumBasalRatePerHour + + tableView.reloadRows(at: [[Section.configuration.rawValue, ConfigurationRow.deliveryLimits.rawValue]], with: .none) + } + + func deliveryLimitSettingsTableViewControllerDidUpdateMaximumBolus(_ vc: DeliveryLimitSettingsTableViewController) { + pumpManagerSetupViewController?.maxBolusUnits = vc.maximumBolus + + tableView.reloadRows(at: [[Section.configuration.rawValue, ConfigurationRow.deliveryLimits.rawValue]], with: .none) + } +} diff --git a/MockKitUI/View Controllers/MockPumpManagerSettingsViewController.swift b/MockKitUI/View Controllers/MockPumpManagerSettingsViewController.swift new file mode 100644 index 000000000..b326267d2 --- /dev/null +++ b/MockKitUI/View Controllers/MockPumpManagerSettingsViewController.swift @@ -0,0 +1,319 @@ +// +// MockPumpManagerSettingsViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +final class MockPumpManagerSettingsViewController: UITableViewController { + + let pumpManager: MockPumpManager + + init(pumpManager: MockPumpManager) { + self.pumpManager = pumpManager + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private let quantityFormatter = QuantityFormatter() + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Pump Settings" + + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = 44 + + tableView.sectionHeaderHeight = UITableView.automaticDimension + tableView.estimatedSectionHeaderHeight = 55 + + tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className) + tableView.register(SwitchTableViewCell.nib(), forCellReuseIdentifier: SwitchTableViewCell.className) + tableView.register(TextButtonTableViewCell.self, forCellReuseIdentifier: TextButtonTableViewCell.className) + tableView.register(SuspendResumeTableViewCell.self, forCellReuseIdentifier: SuspendResumeTableViewCell.className) + + pumpManager.addStatusObserver(self) + + let button = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped(_:))) + self.navigationItem.setRightBarButton(button, animated: false) + } + + @objc func doneTapped(_ sender: Any) { + done() + } + + private func done() { + if let nav = navigationController as? SettingsNavigationViewController { + nav.notifyComplete() + } + if let nav = navigationController as? MockPumpManagerSetupViewController { + nav.finishedSettingsDisplay() + } + } + + // MARK: - Data Source + + private enum Section: Int, CaseIterable { + case actions = 0 + case settings + case deletePump + } + + private enum ActionRow: Int, CaseIterable { + case suspendResume = 0 + } + + private enum SettingsRow: Int, CaseIterable { + case reservoirRemaining = 0 + case batteryRemaining + case tempBasalErrorToggle + case bolusErrorToggle + case suspendErrorToggle + case resumeErrorToggle + } + + // MARK: UITableViewDataSource + + override func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .actions: + return ActionRow.allCases.count + case .settings: + return SettingsRow.allCases.count + case .deletePump: + return 1 + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch Section(rawValue: section)! { + case .actions: + return nil + case .settings: + return "Configuration" + case .deletePump: + return " " // Use an empty string for more dramatic spacing + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section(rawValue: indexPath.section)! { + case .actions: + switch ActionRow(rawValue: indexPath.row)! { + case .suspendResume: + let cell = tableView.dequeueReusableCell(withIdentifier: SuspendResumeTableViewCell.className, for: indexPath) as! SuspendResumeTableViewCell + cell.basalDeliveryState = pumpManager.status.basalDeliveryState + return cell + } + case .settings: + switch SettingsRow(rawValue: indexPath.row)! { + case .reservoirRemaining: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = "Reservoir Remaining" + cell.detailTextLabel?.text = quantityFormatter.string(from: HKQuantity(unit: .internationalUnit(), doubleValue: pumpManager.state.reservoirUnitsRemaining), for: .internationalUnit()) + cell.accessoryType = .disclosureIndicator + return cell + case .batteryRemaining: + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) + cell.textLabel?.text = "Battery Remaining" + if let remainingCharge = pumpManager.status.pumpBatteryChargeRemaining { + cell.detailTextLabel?.text = "\(Int(round(remainingCharge * 100)))%" + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + cell.accessoryType = .disclosureIndicator + return cell + case .tempBasalErrorToggle: + return switchTableViewCell(for: indexPath, titled: "Error on Temp Basal", boundTo: \.tempBasalEnactmentShouldError) + case .bolusErrorToggle: + return switchTableViewCell(for: indexPath, titled: "Error on Bolus", boundTo: \.bolusEnactmentShouldError) + case .suspendErrorToggle: + return switchTableViewCell(for: indexPath, titled: "Error on Suspend", boundTo: \.deliverySuspensionShouldError) + case .resumeErrorToggle: + return switchTableViewCell(for: indexPath, titled: "Error on Resume", boundTo: \.deliveryResumptionShouldError) + } + case .deletePump: + let cell = tableView.dequeueReusableCell(withIdentifier: TextButtonTableViewCell.className, for: indexPath) as! TextButtonTableViewCell + cell.textLabel?.text = "Delete Pump" + cell.textLabel?.textAlignment = .center + cell.tintColor = .delete + cell.isEnabled = true + return cell + } + } + + private func switchTableViewCell(for indexPath: IndexPath, titled title: String, boundTo keyPath: WritableKeyPath) -> SwitchTableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell + cell.titleLabel?.text = title + cell.switch?.isOn = pumpManager.state[keyPath: keyPath] + cell.onToggle = { [unowned pumpManager] isOn in + pumpManager.state[keyPath: keyPath] = isOn + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sender = tableView.cellForRow(at: indexPath) + + switch Section(rawValue: indexPath.section)! { + case .actions: + switch ActionRow(rawValue: indexPath.row)! { + case .suspendResume: + if let suspendResumeCell = sender as? SuspendResumeTableViewCell { + suspendResumeCellTapped(suspendResumeCell) + } + tableView.deselectRow(at: indexPath, animated: true) + } + case .settings: + switch SettingsRow(rawValue: indexPath.row)! { + case .reservoirRemaining: + let vc = TextFieldTableViewController() + vc.value = String(format: "%.1f", pumpManager.state.reservoirUnitsRemaining) + vc.unit = "U" + vc.keyboardType = .decimalPad + vc.indexPath = indexPath + vc.delegate = self + show(vc, sender: sender) + case .batteryRemaining: + let vc = PercentageTextFieldTableViewController() + vc.percentage = pumpManager.status.pumpBatteryChargeRemaining + vc.indexPath = indexPath + vc.percentageDelegate = self + show(vc, sender: sender) + case .tempBasalErrorToggle, .bolusErrorToggle, .suspendErrorToggle, .resumeErrorToggle: + break + } + case .deletePump: + let confirmVC = UIAlertController(pumpDeletionHandler: { + self.pumpManager.pumpManagerDelegate?.pumpManagerWillDeactivate(self.pumpManager) + self.done() + }) + + present(confirmVC, animated: true) { + tableView.deselectRow(at: indexPath, animated: true) + } + } + } + + private func suspendResumeCellTapped(_ cell: SuspendResumeTableViewCell) { + switch cell.shownAction { + case .resume: + pumpManager.resumeDelivery { (error) in + if let error = error { + DispatchQueue.main.async { + let title = LocalizedString("Error Resuming", comment: "The alert title for a resume error") + self.present(UIAlertController(with: error, title: title), animated: true) + } + } + } + case .suspend: + pumpManager.suspendDelivery { (error) in + if let error = error { + DispatchQueue.main.async { + let title = LocalizedString("Error Suspending", comment: "The alert title for a suspend error") + self.present(UIAlertController(with: error, title: title), animated: true) + } + } + } + } + } +} + +extension MockPumpManagerSettingsViewController: PumpManagerStatusObserver { + public func pumpManager(_ pumpManager: PumpManager, didUpdate status: PumpManagerStatus) { + DispatchQueue.main.async { + if let suspendResumeTableViewCell = self.tableView?.cellForRow(at: IndexPath(row: ActionRow.suspendResume.rawValue, section: Section.actions.rawValue)) as? SuspendResumeTableViewCell + { + suspendResumeTableViewCell.basalDeliveryState = status.basalDeliveryState + } + } + } +} + +extension MockPumpManagerSettingsViewController: TextFieldTableViewControllerDelegate { + func textFieldTableViewControllerDidReturn(_ controller: TextFieldTableViewController) { + update(from: controller) + } + + func textFieldTableViewControllerDidEndEditing(_ controller: TextFieldTableViewController) { + update(from: controller) + } + + private func update(from controller: TextFieldTableViewController) { + guard let indexPath = controller.indexPath else { assertionFailure(); return } + assert(indexPath == [Section.settings.rawValue, SettingsRow.reservoirRemaining.rawValue]) + if let value = controller.value.flatMap(Double.init) { + pumpManager.state.reservoirUnitsRemaining = value + } + tableView.reloadRows(at: [indexPath], with: .automatic) + } +} + +extension MockPumpManagerSettingsViewController: PercentageTextFieldTableViewControllerDelegate { + func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) { + guard let indexPath = controller.indexPath else { assertionFailure(); return } + assert(indexPath == [Section.settings.rawValue, SettingsRow.batteryRemaining.rawValue]) + pumpManager.status.pumpBatteryChargeRemaining = controller.percentage.map { $0.clamped(to: 0...1) } + tableView.reloadRows(at: [indexPath], with: .automatic) + } +} + +private extension UIAlertController { + convenience init(pumpDeletionHandler handler: @escaping () -> Void) { + self.init( + title: nil, + message: "Are you sure you want to delete this pump?", + preferredStyle: .actionSheet + ) + + addAction(UIAlertAction( + title: "Delete Pump", + style: .destructive, + handler: { _ in handler() } + )) + + addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil)) + } + + convenience init(title: String, error: Error) { + + let message: String + + if let localizedError = error as? LocalizedError { + let sentenceFormat = NSLocalizedString("%@.", comment: "Appends a full-stop to a statement") + message = [localizedError.failureReason, localizedError.recoverySuggestion].compactMap({ $0 }).map({ + String(format: sentenceFormat, $0) + }).joined(separator: "\n") + } else { + message = String(describing: error) + } + + self.init( + title: title, + message: message, + preferredStyle: .alert + ) + + addAction(UIAlertAction( + title: NSLocalizedString("OK", comment: "Button title to acknowledge error"), + style: .default, + handler: nil + )) + } +} diff --git a/MockKitUI/View Controllers/MockPumpManagerSetupViewController.swift b/MockKitUI/View Controllers/MockPumpManagerSetupViewController.swift new file mode 100644 index 000000000..d384ee412 --- /dev/null +++ b/MockKitUI/View Controllers/MockPumpManagerSetupViewController.swift @@ -0,0 +1,70 @@ +// +// MockPumpManagerSetupViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/20/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import LoopKit +import LoopKitUI +import MockKit + + +final class MockPumpManagerSetupViewController: UINavigationController, PumpManagerSetupViewController, CompletionNotifying { + + static func instantiateFromStoryboard() -> MockPumpManagerSetupViewController { + return UIStoryboard(name: "MockPumpManager", bundle: Bundle(for: MockPumpManagerSetupViewController.self)).instantiateInitialViewController() as! MockPumpManagerSetupViewController + } + + var maxBasalRateUnitsPerHour: Double? + + var maxBolusUnits: Double? + + var basalSchedule: BasalRateSchedule? + + let pumpManager = MockPumpManager() + + weak var setupDelegate: PumpManagerSetupViewControllerDelegate? + + weak var completionDelegate: CompletionDelegate? + + override public func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .white + navigationBar.shadowImage = UIImage() + + delegate = self + } + + func completeSetup() { + setupDelegate?.pumpManagerSetupViewController(self, didSetUpPumpManager: pumpManager) + completionDelegate?.completionNotifyingDidComplete(self) + } + + public func finishedSettingsDisplay() { + completionDelegate?.completionNotifyingDidComplete(self) + } +} + +extension MockPumpManagerSetupViewController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) { + switch viewController { + case let vc as MockPumpManagerSettingsSetupViewController: + vc.pumpManager = pumpManager + default: + break + } + + // Adjust the appearance for the main setup view controllers only + if viewController is SetupTableViewController { + navigationBar.isTranslucent = false + navigationBar.shadowImage = UIImage() + } else { + navigationBar.isTranslucent = true + navigationBar.shadowImage = nil + } + } +} diff --git a/MockKitUI/View Controllers/RandomOutlierTableViewController.swift b/MockKitUI/View Controllers/RandomOutlierTableViewController.swift new file mode 100644 index 000000000..04aad2a06 --- /dev/null +++ b/MockKitUI/View Controllers/RandomOutlierTableViewController.swift @@ -0,0 +1,159 @@ +// +// RandomOutlierTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/25/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +protocol RandomOutlierTableViewControllerDelegate: class { + func randomOutlierTableViewControllerDidChangeOutlier(_ controller: RandomOutlierTableViewController) +} + +final class RandomOutlierTableViewController: UITableViewController { + + let glucoseUnit: HKUnit + + var randomOutlier: MockCGMDataSource.Effects.RandomOutlier? { + get { + guard let chance = chance, let delta = delta else { + return nil + } + return (chance: chance, delta: delta) + } + set { + chance = newValue?.chance + delta = newValue?.delta + } + } + + var contextHelp: String? + + var indexPath: IndexPath? + + weak var delegate: RandomOutlierTableViewControllerDelegate? + + private var chance: Double? { + didSet { + delegate?.randomOutlierTableViewControllerDidChangeOutlier(self) + } + } + + private var delta: HKQuantity? { + didSet { + delegate?.randomOutlierTableViewControllerDidChangeOutlier(self) + } + } + + private lazy var percentageFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimumIntegerDigits = 1 + formatter.maximumFractionDigits = 1 + return formatter + }() + + private lazy var glucoseFormatter = QuantityFormatter() + + init(glucoseUnit: HKUnit) { + self.glucoseUnit = glucoseUnit + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className) + } + + // MARK: - Data Source + + private enum Row: Int, CaseIterable { + case chance = 0 + case delta + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return Row.allCases.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell + + switch Row(rawValue: indexPath.row)! { + case .chance: + cell.textLabel?.text = "Chance" + if let chance = chance, + let percentageText = percentageFormatter.string(from: chance * 100) { + cell.detailTextLabel?.text = "\(percentageText)%" + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + case .delta: + cell.textLabel?.text = "Delta" + if let delta = delta { + cell.detailTextLabel?.text = glucoseFormatter.string(from: delta, for: glucoseUnit) + } else { + cell.detailTextLabel?.text = SettingsTableViewCell.NoValueString + } + } + + cell.accessoryType = .disclosureIndicator + return cell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sender = tableView.cellForRow(at: indexPath) + + switch Row(rawValue: indexPath.row)! { + case .chance: + let vc = PercentageTextFieldTableViewController() + vc.percentage = chance + vc.title = "Chance" + vc.contextHelp = "The percentage determines the chance with which the CGM will produce an outlier when a glucose value is requested." + vc.percentageDelegate = self + show(vc, sender: sender) + case .delta: + let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit) + vc.glucose = delta + vc.title = "Delta" + vc.contextHelp = "The delta determines the offset from the expected glucose value when the CGM produces an outlier." + vc.glucoseEntryDelegate = self + show(vc, sender: sender) + } + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return contextHelp + } +} + +extension RandomOutlierTableViewController: PercentageTextFieldTableViewControllerDelegate { + func percentageTextFieldTableViewControllerDidChangePercentage(_ controller: PercentageTextFieldTableViewController) { + chance = controller.percentage?.clamped(to: 0...100) + tableView.reloadRows(at: [[0, Row.chance.rawValue]], with: .automatic) + } +} + +extension RandomOutlierTableViewController: GlucoseEntryTableViewControllerDelegate { + func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) { + delta = controller.glucose + tableView.reloadRows(at: [[0, Row.delta.rawValue]], with: .automatic) + } +} diff --git a/MockKitUI/View Controllers/SineCurveParametersTableViewController.swift b/MockKitUI/View Controllers/SineCurveParametersTableViewController.swift new file mode 100644 index 000000000..add5fcc10 --- /dev/null +++ b/MockKitUI/View Controllers/SineCurveParametersTableViewController.swift @@ -0,0 +1,237 @@ +// +// SineCurveParametersTableViewController.swift +// LoopKitUI +// +// Created by Michael Pangburn on 11/24/18. +// Copyright © 2018 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit +import LoopKit +import LoopKitUI +import MockKit + + +protocol SineCurveParametersTableViewControllerDelegate: class { + func sineCurveParametersTableViewControllerDidUpdateParameters(_ controller: SineCurveParametersTableViewController) +} + +final class SineCurveParametersTableViewController: UITableViewController { + + let glucoseUnit: HKUnit + + var parameters: MockCGMDataSource.Model.SineCurveParameters? { + get { + if let baseGlucose = baseGlucose, + let amplitude = amplitude, + let period = period, + let referenceDate = referenceDate + { + return (baseGlucose: baseGlucose, amplitude: amplitude, period: period, referenceDate: referenceDate) + } else { + return nil + } + } + set { + baseGlucose = newValue?.baseGlucose + amplitude = newValue?.amplitude + period = newValue?.period ?? defaultPeriod + referenceDate = newValue?.referenceDate ?? defaultReferenceDate + } + } + + var defaultPeriod: TimeInterval = .hours(6) + var defaultReferenceDate = Date() + + private var baseGlucose: HKQuantity? { + didSet { + delegate?.sineCurveParametersTableViewControllerDidUpdateParameters(self) + } + } + + private var amplitude: HKQuantity? { + didSet { + delegate?.sineCurveParametersTableViewControllerDidUpdateParameters(self) + } + } + + private var period: TimeInterval? { + didSet { + delegate?.sineCurveParametersTableViewControllerDidUpdateParameters(self) + } + } + + private var referenceDate: Date? { + didSet { + delegate?.sineCurveParametersTableViewControllerDidUpdateParameters(self) + } + } + + var contextHelp: String? + + weak var delegate: SineCurveParametersTableViewControllerDelegate? + + private lazy var glucoseFormatter = QuantityFormatter() + + private lazy var durationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.hour, .minute] + formatter.unitsStyle = .short + return formatter + }() + + private lazy var dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + init(glucoseUnit: HKUnit) { + self.glucoseUnit = glucoseUnit + super.init(style: .grouped) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "Sine Curve" + + tableView.register(SettingsTableViewCell.self, forCellReuseIdentifier: SettingsTableViewCell.className) + } + + // MARK: - Data Source + + private enum Row: Int, CaseIterable { + case baseGlucose = 0 + case amplitude + case period + case referenceDate + } + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return Row.allCases.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: SettingsTableViewCell.className, for: indexPath) as! SettingsTableViewCell + let formatGlucose = { self.glucoseFormatter.string(from: $0, for: self.glucoseUnit) } + + switch Row(rawValue: indexPath.row)! { + case .baseGlucose: + cell.textLabel?.text = "Base Glucose" + cell.detailTextLabel?.text = baseGlucose.map(formatGlucose) ?? SettingsTableViewCell.NoValueString + case .amplitude: + cell.textLabel?.text = "Amplitude" + cell.detailTextLabel?.text = amplitude.map(formatGlucose) ?? SettingsTableViewCell.NoValueString + case .period: + cell.textLabel?.text = "Period" + cell.detailTextLabel?.text = period.flatMap(durationFormatter.string(from:)) ?? SettingsTableViewCell.NoValueString + case .referenceDate: + cell.textLabel?.text = "Reference Date" + cell.detailTextLabel?.text = referenceDate.map(dateFormatter.string(from:)) ?? SettingsTableViewCell.NoValueString + } + + cell.accessoryType = .disclosureIndicator + return cell + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sender = tableView.cellForRow(at: indexPath) + let title = sender?.textLabel?.text + + func presentGlucoseEntryViewController(for glucose: HKQuantity?, contextHelp: String?) { + let vc = GlucoseEntryTableViewController(glucoseUnit: glucoseUnit) + vc.glucose = glucose + vc.title = title + vc.contextHelp = contextHelp + vc.indexPath = indexPath + vc.glucoseEntryDelegate = self + show(vc, sender: sender) + } + + func presentDateAndDurationViewController(for inputMode: DateAndDurationTableViewController.InputMode, contextHelp: String?) { + let vc = DateAndDurationTableViewController() + vc.inputMode = inputMode + vc.title = title + vc.contextHelp = contextHelp + vc.indexPath = indexPath + vc.delegate = self + show(vc, sender: sender) + } + + switch Row(rawValue: indexPath.row)! { + case .baseGlucose: + presentGlucoseEntryViewController(for: baseGlucose, contextHelp: "The base glucose represents the zero about which the sine curve oscillates.") + case .amplitude: + presentGlucoseEntryViewController(for: amplitude, contextHelp: "The amplitude represents the magnitude of the oscillation of the glucose curve.") + case .period: + presentDateAndDurationViewController(for: .duration(period ?? defaultPeriod), contextHelp: "The period describes the duration of one complete glucose cycle.") + case .referenceDate: + presentDateAndDurationViewController(for: .date(referenceDate ?? defaultReferenceDate, mode: .dateAndTime), contextHelp: "The reference date describes the origin of the sine curve with respect to time. Changing the reference date applies a phase shift to the curve.") + } + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + return contextHelp + } +} + +extension SineCurveParametersTableViewController: GlucoseEntryTableViewControllerDelegate { + func glucoseEntryTableViewControllerDidChangeGlucose(_ controller: GlucoseEntryTableViewController) { + guard let indexPath = controller.indexPath else { + assertionFailure() + return + } + + switch Row(rawValue: indexPath.row)! { + case .baseGlucose: + baseGlucose = controller.glucose + case .amplitude: + amplitude = controller.glucose + default: + assertionFailure() + } + + tableView.reloadRows(at: [indexPath], with: .automatic) + } +} + +extension SineCurveParametersTableViewController: DateAndDurationTableViewControllerDelegate { + func dateAndDurationTableViewControllerDidChangeDate(_ controller: DateAndDurationTableViewController) { + guard let indexPath = controller.indexPath else { + assertionFailure() + return + } + + switch Row(rawValue: indexPath.row)! { + case .period: + guard case .duration(let duration) = controller.inputMode else { + assertionFailure() + return + } + period = duration + case .referenceDate: + guard case .date(let date, mode: _) = controller.inputMode else { + assertionFailure() + return + } + referenceDate = date + default: + assertionFailure() + } + + tableView.reloadRows(at: [indexPath], with: .automatic) + } +}