diff --git a/.gitignore b/.gitignore index 7f76a428f6..033162007f 100644 --- a/.gitignore +++ b/.gitignore @@ -70,3 +70,5 @@ RemoteSettings.plist # OS .DS_Store +# Framework development +Loop.xcworkspace diff --git a/Cartfile b/Cartfile index 752c28b902..41185c78f9 100644 --- a/Cartfile +++ b/Cartfile @@ -1,7 +1,7 @@ -github "LoopKit/LoopKit" == 1.3.0 +github "LoopKit/LoopKit" == 1.4.2 github "LoopKit/xDripG5" ~> 0.8.0 github "i-schuetz/SwiftCharts" ~> 0.6.0 -github "mddub/dexcom-share-client-swift" == 0.2.1 +github "mddub/dexcom-share-client-swift" == 0.4.0 github "mddub/G4ShareSpy" == 0.3.2 -github "ps2/rileylink_ios" == 1.1.0 +github "ps2/rileylink_ios" == 1.2.0 github "amplitude/Amplitude-iOS" ~> 3.8.5 diff --git a/Cartfile.resolved b/Cartfile.resolved index 2911b5559b..a41833fb32 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,7 +1,7 @@ -github "LoopKit/LoopKit" "v1.3.0" +github "LoopKit/LoopKit" "v1.4.2" github "LoopKit/xDripG5" "v0.8.0" github "amplitude/Amplitude-iOS" "v3.14.1" github "i-schuetz/SwiftCharts" "0.6" github "mddub/G4ShareSpy" "v0.3.2" -github "mddub/dexcom-share-client-swift" "v0.2.1" -github "ps2/rileylink_ios" "v1.1.0" +github "mddub/dexcom-share-client-swift" "v0.4.0" +github "ps2/rileylink_ios" "v1.2.0" diff --git a/Carthage/Build/.Amplitude-iOS.version b/Carthage/Build/.Amplitude-iOS.version index f887ad548e..05177f3b51 100644 --- a/Carthage/Build/.Amplitude-iOS.version +++ b/Carthage/Build/.Amplitude-iOS.version @@ -1,12 +1,11 @@ { - "commitish" : "v3.14.1", "iOS" : [ { - "hash" : "d272791b1288fa80098d66adc4a855198ebcaa6c2c50b8487921c44d9d1e9e0e", + "hash" : "10a5fca8009894bc4f368f1637fbbc7d24be2c4fbcd638c28de99b36971039ef", "name" : "Amplitude" } ], - "xcodeVersion" : "Xcode 8.3.2\nBuild version 8E2002", + "commitish" : "v3.14.1", "watchOS" : [ ], diff --git a/Carthage/Build/.G4ShareSpy.version b/Carthage/Build/.G4ShareSpy.version index b2942ea56e..93d6af9c48 100644 --- a/Carthage/Build/.G4ShareSpy.version +++ b/Carthage/Build/.G4ShareSpy.version @@ -1,12 +1,11 @@ { - "commitish" : "v0.3.2", "iOS" : [ { - "hash" : "db9044cc4f0523847cfdf9f53175f81aa10eaa34296f397561a239520c1a0f05", + "hash" : "cc156b56d33767956ff8bbd7c147dde85aedae1fcfdd448b3b086d43dcc78f51", "name" : "G4ShareSpy" } ], - "xcodeVersion" : "Xcode 8.3.2\nBuild version 8E2002", + "commitish" : "v0.3.2", "watchOS" : [ ], diff --git a/Carthage/Build/.LoopKit.version b/Carthage/Build/.LoopKit.version index d45d559238..cb6a0d5d55 100644 --- a/Carthage/Build/.LoopKit.version +++ b/Carthage/Build/.LoopKit.version @@ -1,23 +1,23 @@ { "iOS" : [ { - "hash" : "ff17afa9c1bc6adb1a9097f10358f3109a2fad7f17a7427a99e3add19f8192c2", + "hash" : "1bcbe9463f39173f48ee1e744276e2be8a655f73e2bef25e39badb1a6095ae3f", "name" : "GlucoseKit" }, { - "hash" : "064098139c38ebe785c5882068d75604c9e5f862ef932765389593f4bc56dc92", - "name" : "InsulinKit" + "hash" : "fd500f253d456ea9299163883151a651376a50d30b2bad203c0d8a5ddf5849c5", + "name" : "LoopKit" }, { - "hash" : "962b86c0793ced4741d32439a9ce836c054a2f2b58214279cfcfb634acea5757", - "name" : "LoopKit" + "hash" : "9d10ff9b6df2cf8008dbd2096d2b3277caaf76e3fee1ecc108b6b60cfb4181f3", + "name" : "InsulinKit" }, { - "hash" : "b93fe2ceaff06f8748e90e7a83d80b64d7d2b2c52a78ebe6fb59f9ea3eb270c5", + "hash" : "bdbb30c66844c54627d49009a95978fdba4f2f689a0994f835eef89b0dde5873", "name" : "CarbKit" } ], - "commitish" : "v1.3.0", + "commitish" : "v1.4.2", "watchOS" : [ ], diff --git a/Carthage/Build/.SwiftCharts.version b/Carthage/Build/.SwiftCharts.version index e333aa5887..ae869cd7e3 100644 --- a/Carthage/Build/.SwiftCharts.version +++ b/Carthage/Build/.SwiftCharts.version @@ -1,18 +1,17 @@ { - "commitish" : "0.6", "iOS" : [ { - "hash" : "326259aba63578ffb93455bb07d2a1919e68d92157e02941c1628fd7704e1efa", + "hash" : "d6ddb49e6f1c8abb58516b6c4334e9458a8d4d3e3147d0b1489a034a92ac0741", "name" : "SwiftCharts" } ], - "xcodeVersion" : "Xcode 8.3.2\nBuild version 8E2002", + "commitish" : "0.6", "watchOS" : [ ], "tvOS" : [ { - "hash" : "1449a06dc971aeecdf02272698471b91cb6988ea0ef25e474d9e3d3db1e949b3", + "hash" : "43eb1be1f543f40d6899617161952dac59688ac4ab472068d2edba8a35642cb0", "name" : "SwiftCharts" } ], diff --git a/Carthage/Build/.dexcom-share-client-swift.version b/Carthage/Build/.dexcom-share-client-swift.version index 7df924b253..d4be0e2106 100644 --- a/Carthage/Build/.dexcom-share-client-swift.version +++ b/Carthage/Build/.dexcom-share-client-swift.version @@ -1,12 +1,11 @@ { - "commitish" : "v0.2.1", "iOS" : [ { - "hash" : "7a23bd73e6e8f0daee22d4df7ff6f410a40d21ebf83998e1adb12010a7ee902f", + "hash" : "af566c1904ed3a6f35421c09c21b40aa358eac8b8ac3e22f288c10000139da11", "name" : "ShareClient" } ], - "xcodeVersion" : "Xcode 8.3.2\nBuild version 8E2002", + "commitish" : "v0.2.1", "watchOS" : [ ], diff --git a/Carthage/Build/.rileylink_ios.version b/Carthage/Build/.rileylink_ios.version index 2a8076c2d6..044c5e5e60 100644 --- a/Carthage/Build/.rileylink_ios.version +++ b/Carthage/Build/.rileylink_ios.version @@ -1,27 +1,23 @@ { "iOS" : [ { - "hash" : "001a015c6f775b65568a03398b15a7ec016d5514945a004333b3ca2d7fe2c083", - "name" : "Crypto" - }, - { - "hash" : "9ca95fb0daffb835a39d2403ac1da2ca6af12fdeaa2ddfc1a4676ca0db304896", + "hash" : "9d97688f169b331ae19bba15fd17d9a79c99c54c783a85013d7f3f5a9e158366", "name" : "RileyLinkBLEKit" }, { - "hash" : "f58e47ea57693a5e37162f8e0d9e1b2057139b7019012fd25a7dc992c3629067", + "hash" : "e2c80229794ea573fdffd0801aedc0129c931b5b416c6f65af63bd076b871c30", "name" : "NightscoutUploadKit" }, { - "hash" : "50b787e77565e49d65cbb7a7a6ddeae9a4d3c78afe524a6d3def56b73120c66b", + "hash" : "99e9ae36e543d2f09cdd9afbf93d2217d19c62809ed802a593ab3328134c1405", "name" : "RileyLinkKit" }, { - "hash" : "c9491ddea5895f23a4d15efe49bb1d44b80897145f1702b8ccdbe1b4660b21cc", + "hash" : "f7eb5e6e87317c07a66762e5b49cb7180632c7cf51e0d532a58aeaac2d5c806e", "name" : "MinimedKit" } ], - "commitish" : "v1.1.0", + "commitish" : "v1.2.0", "watchOS" : [ ], diff --git a/Carthage/Build/.xDripG5.version b/Carthage/Build/.xDripG5.version index 1dbbc164bb..0ccfb58635 100644 --- a/Carthage/Build/.xDripG5.version +++ b/Carthage/Build/.xDripG5.version @@ -1,12 +1,11 @@ { - "commitish" : "v0.8.0", "iOS" : [ { - "hash" : "8568448b3acdbfa1da26b3a87e2b08db5b11e9b6625ccc61e159179f23ea951a", + "hash" : "a093bc579bbd7d505080e8bf012bc222d54f207dc5fe3e40aaf7d9bb7c57498a", "name" : "xDripG5" } ], - "xcodeVersion" : "Xcode 8.3.2\nBuild version 8E2002", + "commitish" : "v0.8.0", "watchOS" : [ ], diff --git a/Carthage/Build/iOS/Amplitude.framework/Amplitude b/Carthage/Build/iOS/Amplitude.framework/Amplitude index c22acf809a..48f8613f86 100755 Binary files a/Carthage/Build/iOS/Amplitude.framework/Amplitude and b/Carthage/Build/iOS/Amplitude.framework/Amplitude differ diff --git a/Carthage/Build/iOS/Amplitude.framework/Info.plist b/Carthage/Build/iOS/Amplitude.framework/Info.plist index 0e584bcd79..ce6b91e787 100644 Binary files a/Carthage/Build/iOS/Amplitude.framework/Info.plist and b/Carthage/Build/iOS/Amplitude.framework/Info.plist differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Assets.car b/Carthage/Build/iOS/CarbKit.framework/Assets.car new file mode 100644 index 0000000000..ddc5a13391 Binary files /dev/null and b/Carthage/Build/iOS/CarbKit.framework/Assets.car differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit b/Carthage/Build/iOS/CarbKit.framework/CarbKit index 9195125610..17f695e12a 100755 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit and b/Carthage/Build/iOS/CarbKit.framework/CarbKit differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib new file mode 100644 index 0000000000..9eb2ad5665 Binary files /dev/null and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbAbsorptionInputController.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib index 658f53aa5f..5f38b91a8a 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryEditViewController.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib index 84f856e3c5..9a06ec1429 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/CarbEntryTableViewController.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/Info.plist b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/Info.plist index a017eca395..a6103f40b1 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/Info.plist and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/Info.plist differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib index 7cc65fd611..2406d103e6 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/LyL-9U-twn-view-9Ci-XW-6nA.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib new file mode 100644 index 0000000000..1186daf973 Binary files /dev/null and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/QAc-uE-L5K-view-ZAF-8o-e2g.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib index 18b9046ce1..99927e4517 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/UINavigationController-wgu-gT-TgV.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib index e3cac98580..0559109e45 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib and b/Carthage/Build/iOS/CarbKit.framework/CarbKit.storyboardc/rUL-yg-cFX-view-b1s-8o-0Wp.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib b/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib new file mode 100644 index 0000000000..ca4e358b8a Binary files /dev/null and b/Carthage/Build/iOS/CarbKit.framework/DatePickerTableViewCell.nib differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h b/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h index d37b460941..e91d477254 100644 --- a/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h +++ b/Carthage/Build/iOS/CarbKit.framework/Headers/CarbKit-Swift.h @@ -140,28 +140,51 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wproperty-attribute-mismatch" #pragma clang diagnostic ignored "-Wduplicate-method-arg" @class HKUnit; +@class HKQuantity; @class UITableView; @class UITableViewCell; -@class UIStoryboardSegue; @class NSBundle; @class NSCoder; SWIFT_CLASS("_TtC7CarbKit27CarbEntryEditViewController") @interface CarbEntryEditViewController : UITableViewController @property (nonatomic, strong) HKUnit * _Nonnull preferredUnit; +@property (nonatomic, strong) HKQuantity * _Nonnull maxQuantity; +/// Entry configuration values. Must be set before presenting. +@property (nonatomic) NSTimeInterval absorptionTimePickerInterval; +@property (nonatomic) NSTimeInterval maxAbsorptionTime; +@property (nonatomic) NSTimeInterval maximumDateFutureInterval; - (void)viewDidLoad; - (NSInteger)numberOfSectionsInTableView:(UITableView * _Nonnull)tableView SWIFT_WARN_UNUSED_RESULT; - (NSInteger)tableView:(UITableView * _Nonnull)tableView numberOfRowsInSection:(NSInteger)section SWIFT_WARN_UNUSED_RESULT; - (UITableViewCell * _Nonnull)tableView:(UITableView * _Nonnull)tableView cellForRowAtIndexPath:(NSIndexPath * _Nonnull)indexPath SWIFT_WARN_UNUSED_RESULT; +- (NSString * _Nullable)tableView:(UITableView * _Nonnull)tableView titleForFooterInSection:(NSInteger)section SWIFT_WARN_UNUSED_RESULT; - (NSIndexPath * _Nullable)tableView:(UITableView * _Nonnull)tableView willSelectRowAtIndexPath:(NSIndexPath * _Nonnull)indexPath SWIFT_WARN_UNUSED_RESULT; - (void)tableView:(UITableView * _Nonnull)tableView didSelectRowAtIndexPath:(NSIndexPath * _Nonnull)indexPath; -- (void)prepareForSegue:(UIStoryboardSegue * _Nonnull)segue sender:(id _Nullable)sender; +- (BOOL)shouldPerformSegueWithIdentifier:(NSString * _Nonnull)identifier sender:(id _Nullable)sender SWIFT_WARN_UNUSED_RESULT; - (nonnull instancetype)initWithStyle:(UITableViewStyle)style OBJC_DESIGNATED_INITIALIZER; - (nonnull instancetype)initWithNibName:(NSString * _Nullable)nibNameOrNil bundle:(NSBundle * _Nullable)nibBundleOrNil OBJC_DESIGNATED_INITIALIZER; - (nullable instancetype)initWithCoder:(NSCoder * _Nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER; @end +@interface CarbEntryEditViewController (SWIFT_EXTENSION(CarbKit)) +@end + + +@interface CarbEntryEditViewController (SWIFT_EXTENSION(CarbKit)) +@end + + +@interface CarbEntryEditViewController (SWIFT_EXTENSION(CarbKit)) +@end + + +@interface CarbEntryEditViewController (SWIFT_EXTENSION(CarbKit)) +@end + +@class UIStoryboardSegue; + SWIFT_CLASS("_TtC7CarbKit28CarbEntryTableViewController") @interface CarbEntryTableViewController : UITableViewController - (void)viewDidLoad; @@ -184,7 +207,7 @@ SWIFT_CLASS("_TtC7CarbKit28CarbEntryTableViewController") @interface HKQuantitySample (SWIFT_EXTENSION(CarbKit)) @property (nonatomic, readonly, copy) NSString * _Nullable foodType; @property (nonatomic, readonly) BOOL createdByCurrentApp; -@property (nonatomic, readonly, copy) NSString * _Nullable externalId; +@property (nonatomic, readonly, copy) NSString * _Nullable externalID; @end @@ -192,6 +215,11 @@ SWIFT_CLASS("_TtC7CarbKit28CarbEntryTableViewController") @end +@interface UIInputView (SWIFT_EXTENSION(CarbKit)) +@property (nonatomic, readonly) BOOL enableInputClicksWhenVisible; +@end + + @interface UITableViewCell (SWIFT_EXTENSION(CarbKit)) @end diff --git a/Carthage/Build/iOS/CarbKit.framework/Info.plist b/Carthage/Build/iOS/CarbKit.framework/Info.plist index c85ba8eb92..ef4bf98431 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Info.plist and b/Carthage/Build/iOS/CarbKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc index 53399ffd73..70eb96e649 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule index bf3f9f3923..47100adee4 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc index 6964d20c64..dd0be98dbe 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule index 10759ae65f..58200db750 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc index ad3e27a92a..30d00d5791 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule index 27a8b9b341..2506f92692 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc index 79b4ece711..812cd9b119 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule index 6f86f41f05..51b49bf1b8 100644 Binary files a/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/CarbKit.framework/Modules/CarbKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/Crypto.framework/Crypto b/Carthage/Build/iOS/Crypto.framework/Crypto index fba6354ee3..e149161ec8 100755 Binary files a/Carthage/Build/iOS/Crypto.framework/Crypto and b/Carthage/Build/iOS/Crypto.framework/Crypto differ diff --git a/Carthage/Build/iOS/Crypto.framework/Info.plist b/Carthage/Build/iOS/Crypto.framework/Info.plist index fcfa74313e..d8259475a0 100644 Binary files a/Carthage/Build/iOS/Crypto.framework/Info.plist and b/Carthage/Build/iOS/Crypto.framework/Info.plist differ diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy b/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy index 6572b6ae96..af4dff5825 100755 Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy and b/Carthage/Build/iOS/G4ShareSpy.framework/G4ShareSpy differ diff --git a/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist b/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist index 857781b29a..c440e62862 100644 Binary files a/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist and b/Carthage/Build/iOS/G4ShareSpy.framework/Info.plist differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit b/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit index 6ffb4cbda2..d66e5e268e 100755 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit and b/Carthage/Build/iOS/GlucoseKit.framework/GlucoseKit differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h b/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h index 24dd4e6376..9dfd783b55 100644 --- a/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h +++ b/Carthage/Build/iOS/GlucoseKit.framework/Headers/GlucoseKit-Swift.h @@ -139,6 +139,8 @@ typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4))); #pragma clang diagnostic ignored "-Wduplicate-method-arg" @interface HKQuantitySample (SWIFT_EXTENSION(GlucoseKit)) +@property (nonatomic, readonly) BOOL isDisplayOnly; +@property (nonatomic, readonly, copy) NSString * _Nonnull provenanceIdentifier; @end diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Info.plist b/Carthage/Build/iOS/GlucoseKit.framework/Info.plist index 98e3c9f128..6a93b2c21b 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Info.plist and b/Carthage/Build/iOS/GlucoseKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc index 95d8c3747a..276a36f458 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule index 6e32b848d3..f77ad5cbd9 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc index 433712660c..980b077935 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule index 839ed8cf47..828c1cf5ea 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc index b467b74bdc..44b4a566fc 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule index 5b6f72620e..faed0d444f 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc index 6fe4c0f89d..9f07690f0f 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule index 5fa9f5b4a2..73064e7fab 100644 Binary files a/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/GlucoseKit.framework/Modules/GlucoseKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib index ab87762627..ade8b6160e 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib and b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/UITableViewController-jGX-GA-nlH.nib differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib index 85718388c7..8b7124b9f3 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib and b/Carthage/Build/iOS/InsulinKit.framework/Base.lproj/InsulinKit.storyboardc/jGX-GA-nlH-view-ccM-3y-LQM.nib differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Info.plist b/Carthage/Build/iOS/InsulinKit.framework/Info.plist index 034613d540..6bce4cc024 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Info.plist and b/Carthage/Build/iOS/InsulinKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/InsulinKit b/Carthage/Build/iOS/InsulinKit.framework/InsulinKit index 26a0edd384..d0cd9664ff 100755 Binary files a/Carthage/Build/iOS/InsulinKit.framework/InsulinKit and b/Carthage/Build/iOS/InsulinKit.framework/InsulinKit differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc index 5956305656..92dea5cc43 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule index 102232b1ca..e2df56f0a1 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc index eb9c7e3836..650b33c6f9 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule index 39ca7bc5a2..104756627f 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc index 002bd2f3f6..d9c63906ae 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule index c9a0fb50ed..083d00c2a8 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc index 83de2a99a1..f663125138 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule index 138ae04062..f22eb8e11b 100644 Binary files a/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/InsulinKit.framework/Modules/InsulinKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Assets.car b/Carthage/Build/iOS/LoopKit.framework/Assets.car index e6222fb506..9138ce0282 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Assets.car and b/Carthage/Build/iOS/LoopKit.framework/Assets.car differ diff --git a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib index 08de1385ec..fc5c1a943b 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeOverrideTableViewCell.nib differ diff --git a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib index eef9000b34..15e29f356c 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/GlucoseRangeTableViewCell.nib differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Info.plist b/Carthage/Build/iOS/LoopKit.framework/Info.plist index 2825c4e209..a86049d5b5 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Info.plist and b/Carthage/Build/iOS/LoopKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/LoopKit.framework/LoopKit b/Carthage/Build/iOS/LoopKit.framework/LoopKit index 4c662b7eed..257e47617e 100755 Binary files a/Carthage/Build/iOS/LoopKit.framework/LoopKit and b/Carthage/Build/iOS/LoopKit.framework/LoopKit differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc index ba630f7d79..372bddde56 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule index d8476a4e87..ad97ba6f2a 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc index d4b35bfa45..4a7009cf10 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule index 515c36ff72..bbb7f3f101 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc index e7e483da51..83f940e2d9 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule index 22d846a32f..587298c4d5 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc index 4e99b57fd8..8b31259553 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule index 6610a89bd9..90810a16b4 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/LoopKit.framework/Modules/LoopKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib index 4db9214f92..b7b6cbe128 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/RepeatingScheduleValueTableViewCell.nib differ diff --git a/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib b/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib index 0cd51c1f4d..acb9cabd5d 100644 Binary files a/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib and b/Carthage/Build/iOS/LoopKit.framework/TextFieldTableViewCell.nib differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/Info.plist b/Carthage/Build/iOS/MinimedKit.framework/Info.plist index 3b532b77ad..be14e90d5e 100644 Binary files a/Carthage/Build/iOS/MinimedKit.framework/Info.plist and b/Carthage/Build/iOS/MinimedKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/MinimedKit b/Carthage/Build/iOS/MinimedKit.framework/MinimedKit index d45284abaf..de5ccc8885 100755 Binary files a/Carthage/Build/iOS/MinimedKit.framework/MinimedKit and b/Carthage/Build/iOS/MinimedKit.framework/MinimedKit differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule index 191fb22d21..f0fa1fd139 100644 Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule index 22e5270ed3..614b6fd131 100644 Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule index 7183e34cb7..0e45b51d5f 100644 Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule index ac734d5a90..1218b21051 100644 Binary files a/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/MinimedKit.framework/Modules/MinimedKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist b/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist index a39a058eac..1f24d150d7 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc index 3ae4433b0c..12d9e538d5 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule index 92159f33b1..bc55cc53ee 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc index ac2f7f7b62..8c2c4cba8c 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule index 7c3a3b70e5..7ec8cceb84 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc index f39bad9748..3a13160c81 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule index b33c37793f..1d2916ef4e 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc index 43e5d21622..4f05acc141 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule index 056315b39a..95bcb6f77a 100644 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/NightscoutUploadKit.framework/Modules/NightscoutUploadKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit b/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit index 08606c3c81..35cabd5d28 100755 Binary files a/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit and b/Carthage/Build/iOS/NightscoutUploadKit.framework/NightscoutUploadKit differ diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEDevice.h b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEDevice.h index 3c77422752..9063f61900 100644 --- a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEDevice.h +++ b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEDevice.h @@ -16,6 +16,8 @@ typedef NS_ENUM(NSUInteger, RileyLinkState) { RileyLinkStateDisconnected }; +extern NSString * _Nonnull const SubgRfspyErrorDomain; + typedef NS_ENUM(NSUInteger, SubgRfspyError) { SubgRfspyErrorRxTimeout = 0xaa, SubgRfspyErrorCmdInterrupted = 0xbb, @@ -64,9 +66,9 @@ typedef NS_ENUM(NSUInteger, SubgRfspyVersionState) { @interface RileyLinkBLEDevice : NSObject @property (nonatomic, nullable, readonly) NSString * name; -@property (nonatomic, nullable, retain) NSNumber * RSSI; +@property (nonatomic, nullable, strong) NSNumber * RSSI; @property (nonatomic, nonnull, readonly) NSString * peripheralId; -@property (nonatomic, nonnull, readonly, retain) CBPeripheral * peripheral; +@property (nonatomic, nonnull, strong) CBPeripheral * peripheral; @property (nonatomic, readonly) RileyLinkState state; @@ -99,6 +101,6 @@ typedef NS_ENUM(NSUInteger, SubgRfspyVersionState) { - (void) setCustomName:(nonnull NSString*)customName; - (void) enableIdleListeningOnChannel:(uint8_t)channel; - (void) disableIdleListening; -- (void) assertIdleListening; +- (void) assertIdleListeningForcingRestart:(BOOL)forceRestart; @end diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEManager.h b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEManager.h index 39cec82cfb..aa94fd6785 100644 --- a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEManager.h +++ b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Headers/RileyLinkBLEManager.h @@ -9,9 +9,8 @@ @import CoreBluetooth; @import Foundation; -#define RILEYLINK_EVENT_LIST_UPDATED @"RILEYLINK_EVENT_LIST_UPDATED" +#define RILEYLINK_EVENT_DEVICE_CREATED @"RILEYLINK_EVENT_DEVICE_CREATED" #define RILEYLINK_IDLE_RESPONSE_RECEIVED @"RILEYLINK_IDLE_RESPONSE_RECEIVED" -#define RILEYLINK_EVENT_DEVICE_ADDED @"RILEYLINK_EVENT_DEVICE_ADDED" #define RILEYLINK_EVENT_DEVICE_CONNECTED @"RILEYLINK_EVENT_DEVICE_CONNECTED" #define RILEYLINK_EVENT_DEVICE_DISCONNECTED @"RILEYLINK_EVENT_DEVICE_DISCONNECTED" #define RILEYLINK_EVENT_DEVICE_ATTRS_DISCOVERED @"RILEYLINK_EVENT_DEVICE_ATTRS_DISCOVERED" @@ -33,13 +32,14 @@ @property (nonatomic, nonnull, readonly, copy) NSArray *rileyLinkList; -- (void)connectPeripheral:(nonnull CBPeripheral *)peripheral; -- (void)disconnectPeripheral:(nonnull CBPeripheral *)peripheral; +- (void)connectDevice:(nonnull RileyLinkBLEDevice *)device; +- (void)disconnectDevice:(nonnull RileyLinkBLEDevice *)device; -+ (nonnull instancetype)sharedManager; +- (nonnull instancetype)initWithAutoConnectIDs:(nonnull NSSet *)autoConnectIDs; -@property (nonatomic, nonnull, strong) NSSet *autoConnectIds; -@property (nonatomic, getter=isScanningEnabled) BOOL scanningEnabled; +@property (nonatomic, nonnull, readonly) NSSet *autoConnectIDs; + +- (void)setScanningEnabled:(BOOL)scanningEnabled; /** Converts an array of UUID strings to CBUUID objects, excluding those represented in an array of CBAttribute objects. diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist index 00785f5428..17453b2dbd 100644 Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit b/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit index 19a1b5e65c..6009ab5e2a 100755 Binary files a/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit and b/Carthage/Build/iOS/RileyLinkBLEKit.framework/RileyLinkBLEKit differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist b/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist index ab7b7ca3e7..a59c2d18a0 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist and b/Carthage/Build/iOS/RileyLinkKit.framework/Info.plist differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc index fc88de76bb..a0ab958f8c 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftdoc differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule index defc2b2016..177da6d967 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc index 143a6e8f4b..e3df0c42b8 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftdoc differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule index d72e79d263..1bed55d03f 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc index 8a86bc6708..8a58d87aa7 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftdoc differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule index a0e4dc1123..0914d4cee0 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc index accc6219f2..1891ee5da5 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftdoc differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule index c3f5dc787f..a46abdc8ad 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/RileyLinkKit.framework/Modules/RileyLinkKit.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkDeviceTableViewCell.nib b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkDeviceTableViewCell.nib index a63f2489bc..9a83c7a29d 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkDeviceTableViewCell.nib and b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkDeviceTableViewCell.nib differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit index 7d8c26c692..959eacc9bb 100755 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit and b/Carthage/Build/iOS/RileyLinkKit.framework/RileyLinkKit differ diff --git a/Carthage/Build/iOS/RileyLinkKit.framework/TextFieldTableViewCell.nib b/Carthage/Build/iOS/RileyLinkKit.framework/TextFieldTableViewCell.nib index e61ec876b6..f02dd92f19 100644 Binary files a/Carthage/Build/iOS/RileyLinkKit.framework/TextFieldTableViewCell.nib and b/Carthage/Build/iOS/RileyLinkKit.framework/TextFieldTableViewCell.nib differ diff --git a/Carthage/Build/iOS/ShareClient.framework/Info.plist b/Carthage/Build/iOS/ShareClient.framework/Info.plist index 2c6fa192b8..f459dfabc3 100644 Binary files a/Carthage/Build/iOS/ShareClient.framework/Info.plist and b/Carthage/Build/iOS/ShareClient.framework/Info.plist differ diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule index dcf5c2f443..ab53ec4591 100644 Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm.swiftmodule differ diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule index 1898d96303..9130d373c6 100644 Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/arm64.swiftmodule differ diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule index 2f62da22d5..f5d9c53d9d 100644 Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/i386.swiftmodule differ diff --git a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule index d8103bfe7f..4b6ce58d99 100644 Binary files a/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule and b/Carthage/Build/iOS/ShareClient.framework/Modules/ShareClient.swiftmodule/x86_64.swiftmodule differ diff --git a/Carthage/Build/iOS/ShareClient.framework/ShareClient b/Carthage/Build/iOS/ShareClient.framework/ShareClient index bbce83b2cc..bf559707ee 100755 Binary files a/Carthage/Build/iOS/ShareClient.framework/ShareClient and b/Carthage/Build/iOS/ShareClient.framework/ShareClient differ diff --git a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist index 60493df79f..b0ae5c045c 100644 Binary files a/Carthage/Build/iOS/SwiftCharts.framework/Info.plist and b/Carthage/Build/iOS/SwiftCharts.framework/Info.plist differ diff --git a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts index ea1088cb56..8c568dd605 100755 Binary files a/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts and b/Carthage/Build/iOS/SwiftCharts.framework/SwiftCharts differ diff --git a/Carthage/Build/iOS/xDripG5.framework/Info.plist b/Carthage/Build/iOS/xDripG5.framework/Info.plist index ea1bfbeefa..5f58704fca 100644 Binary files a/Carthage/Build/iOS/xDripG5.framework/Info.plist and b/Carthage/Build/iOS/xDripG5.framework/Info.plist differ diff --git a/Carthage/Build/iOS/xDripG5.framework/xDripG5 b/Carthage/Build/iOS/xDripG5.framework/xDripG5 index 186d5be7f5..76c0ec2ff8 100755 Binary files a/Carthage/Build/iOS/xDripG5.framework/xDripG5 and b/Carthage/Build/iOS/xDripG5.framework/xDripG5 differ diff --git a/Common/Extensions/WCSession+Swift4.swift b/Common/Extensions/WCSession+Swift4.swift new file mode 100644 index 0000000000..455dbeba5f --- /dev/null +++ b/Common/Extensions/WCSession+Swift4.swift @@ -0,0 +1,17 @@ +// +// WCSession.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import WatchConnectivity + + +extension WCSession { + #if swift(>=4) + static func `default`() -> WCSession { + return self.default + } + #endif +} diff --git a/Common/Models/StatusExtensionContext.swift b/Common/Models/StatusExtensionContext.swift index 0e3c476db7..23c4a2e6d5 100644 --- a/Common/Models/StatusExtensionContext.swift +++ b/Common/Models/StatusExtensionContext.swift @@ -25,7 +25,8 @@ struct LoopContext { struct NetBasalContext { let rate: Double let percentage: Double - let startDate: Date + let start: Date + let end: Date } struct SensorDisplayableContext: SensorDisplayable { @@ -122,7 +123,8 @@ extension NetBasalContext: RawRepresentable { return [ "rate": rate, "percentage": percentage, - "startDate": startDate + "start": start, + "end": end ] } @@ -130,14 +132,16 @@ extension NetBasalContext: RawRepresentable { guard let rate = rawValue["rate"] as? Double, let percentage = rawValue["percentage"] as? Double, - let startDate = rawValue["startDate"] as? Date + let start = rawValue["start"] as? Date, + let end = rawValue["end"] as? Date else { return nil } self.rate = rate self.percentage = percentage - self.startDate = startDate + self.start = start + self.end = end } } diff --git a/DoseMathTests/DoseMathTests.swift b/DoseMathTests/DoseMathTests.swift index 7f77e7179c..538e4416bf 100644 --- a/DoseMathTests/DoseMathTests.swift +++ b/DoseMathTests/DoseMathTests.swift @@ -27,14 +27,15 @@ extension XCTestCase { public typealias JSONDictionary = [String: Any] -extension DateFormatter { - static func ISO8601LocalTimeDateFormatter() -> Self { - let dateFormatter = self.init() +extension ISO8601DateFormatter { + static func localTimeDateFormatter() -> Self { + let formatter = self.init() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - dateFormatter.locale = Locale(identifier: "en_US_POSIX") + formatter.formatOptions = .withInternetDateTime + formatter.formatOptions.subtract(.withTimeZone) + formatter.timeZone = .current - return dateFormatter + return formatter } } @@ -56,7 +57,7 @@ class RecommendTempBasalTests: XCTestCase { func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseValue] { let fixture: [JSONDictionary] = loadFixture(resourceName) - let dateFormatter = DateFormatter.ISO8601LocalTimeDateFormatter() + let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() return fixture.map { return GlucoseFixtureValue( @@ -460,7 +461,7 @@ class RecommendBolusTests: XCTestCase { func loadGlucoseValueFixture(_ resourceName: String) -> [GlucoseValue] { let fixture: [JSONDictionary] = loadFixture(resourceName) - let dateFormatter = DateFormatter.ISO8601LocalTimeDateFormatter() + let dateFormatter = ISO8601DateFormatter.localTimeDateFormatter() return fixture.map { return GlucoseFixtureValue( diff --git a/DoseMathTests/Info.plist b/DoseMathTests/Info.plist index 9b64b457c6..c48e13f347 100644 --- a/DoseMathTests/Info.plist +++ b/DoseMathTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/Loop Status Extension/Base.lproj/MainInterface.storyboard b/Loop Status Extension/Base.lproj/MainInterface.storyboard index 181f25d3cd..91b420eed3 100644 --- a/Loop Status Extension/Base.lproj/MainInterface.storyboard +++ b/Loop Status Extension/Base.lproj/MainInterface.storyboard @@ -1,11 +1,11 @@ - + - + @@ -50,7 +50,7 @@ - + diff --git a/Loop Status Extension/Info.plist b/Loop Status Extension/Info.plist index 226afed872..b1e7db14e0 100644 --- a/Loop Status Extension/Info.plist +++ b/Loop Status Extension/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) AppGroupIdentifier diff --git a/Loop Status Extension/StatusViewController.swift b/Loop Status Extension/StatusViewController.swift index 5069544e7b..a5de9e7563 100644 --- a/Loop Status Extension/StatusViewController.swift +++ b/Loop Status Extension/StatusViewController.swift @@ -26,7 +26,7 @@ class StatusViewController: UIViewController, NCWidgetProviding { } } @IBOutlet weak var subtitleLabel: UILabel! - @IBOutlet weak var glucoseChartContentView: LoopUI.ChartContentView! + @IBOutlet weak var glucoseChartContentView: LoopUI.ChartContainerView! private lazy var charts: StatusChartsManager = { let charts = StatusChartsManager( @@ -185,7 +185,7 @@ class StatusViewController: UIViewController, NCWidgetProviding { } if let netBasal = context.netBasal { - basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percentage, at: netBasal.startDate) + basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percentage, at: netBasal.start) } if let loop = context.loop { diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index f962d34ddb..31803b9090 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 430DA5901D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */; }; 4315D2871CA5CC3B00589052 /* CarbEntryEditTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */; }; 4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4315D2891CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift */; }; + 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */; }; 4328E01A1CFBE1DA00E199AA /* StatusInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */; }; 4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */; }; 4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */; }; @@ -32,6 +33,9 @@ 432E73CB1D24B3D6009AD15D /* RemoteDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432E73CA1D24B3D6009AD15D /* RemoteDataManager.swift */; }; 433EA4C21D9F39C900CD78FB /* PumpIDTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C11D9F39C900CD78FB /* PumpIDTableViewController.swift */; }; 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */; }; + 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4341F4EA1EDB92AC001C936B /* LogglyService.swift */; }; + 43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */; }; + 43441AA01EDB4D390087958C /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43441A9F1EDB4D390087958C /* OSLog.swift */; }; 4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */; }; 4346D1F61C78501000ABAFE3 /* ChartPoint+Loop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */; }; 434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 434F54561D287FDB002A9274 /* NibLoadable.swift */; }; @@ -48,8 +52,11 @@ 435400321C9F745500D5819C /* BolusSuggestionUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */; }; 435400341C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */; }; 435400351C9F878D00D5819C /* SetBolusUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */; }; + 436961911F19D11E00447E89 /* ChartPointsContextFillLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */; }; 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436A0DA41D236A2A00104B24 /* LoopError.swift */; }; 436FACEE1D0BA636004E2427 /* InsulinDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */; }; + 437272DF1F09E41200A3DA02 /* WCSession+Swift4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */; }; + 437272E01F09E41600A3DA02 /* WCSession+Swift4.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */; }; 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43776F8F1B8022E90074EA36 /* AppDelegate.swift */; }; 43776F971B8022E90074EA36 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 43776F951B8022E90074EA36 /* Main.storyboard */; }; 43776F991B8022E90074EA36 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43776F981B8022E90074EA36 /* Assets.xcassets */; }; @@ -75,6 +82,7 @@ 439BED2A1E76093C00B0AED5 /* CGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED291E76093C00B0AED5 /* CGMManager.swift */; }; 439BED2C1E760A7A00B0AED5 /* DexCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED2B1E760A7A00B0AED5 /* DexCGMManager.swift */; }; 439BED2E1E760BC600B0AED5 /* EnliteCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439BED2D1E760BC600B0AED5 /* EnliteCGMManager.swift */; }; + 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A51E1E1EB6D62A000736CC /* CarbAbsorptionViewController.swift */; }; 43A51E211EB6DBDD000736CC /* ChartsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A51E201EB6DBDD000736CC /* ChartsTableViewController.swift */; }; 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A567681C94880B00334FAC /* LoopDataManager.swift */; }; 43A5676B1C96155700334FAC /* SwitchTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */; }; @@ -86,6 +94,7 @@ 43A9438E1B926B7B0051FA24 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43A9438D1B926B7B0051FA24 /* ComplicationController.swift */; }; 43A943901B926B7B0051FA24 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 43A9438F1B926B7B0051FA24 /* Assets.xcassets */; }; 43A943941B926B7B0051FA24 /* WatchApp.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 43A943721B926B7B0051FA24 /* WatchApp.app */; }; + 43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43B260481ED248FB008CAA77 /* CarbEntryTableViewCell.swift */; }; 43B371881CE597D10013C5A6 /* ShareClient.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43B371871CE597D10013C5A6 /* ShareClient.framework */; }; 43BFF0B51E45C1E700FF19A9 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */; }; 43BFF0B71E45C20C00FF19A9 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */; }; @@ -99,6 +108,7 @@ 43BFF0CD1E466C8400FF19A9 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0CC1E466C8400FF19A9 /* StateColorPalette.swift */; }; 43C0944A1CACCC73001F6403 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C094491CACCC73001F6403 /* NotificationManager.swift */; }; 43C246A81D89990F0031F8D1 /* Crypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C246A71D89990F0031F8D1 /* Crypto.framework */; }; + 43C2FAE11EB656A500364AFF /* GlucoseEffectVelocity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */; }; 43C418B51CE0575200405B6A /* ShareGlucose+GlucoseKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */; }; 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C513181E864C4E001547C7 /* GlucoseRangeSchedule.swift */; }; 43C6407C1DA051850093E25D /* InsulinKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C6407B1DA051850093E25D /* InsulinKit.framework */; }; @@ -106,6 +116,8 @@ 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CB2B2A1D924D450079823D /* WCSession.swift */; }; 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */; }; 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */; }; + 43D2E8231F00425400AE5CBF /* BolusViewController+LoopDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D2E8221F00425400AE5CBF /* BolusViewController+LoopDataManager.swift */; }; + 43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */; }; 43D848B01E7DCBE100DADCBC /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D848AF1E7DCBE100DADCBC /* Result.swift */; }; 43D848B21E7DF42500DADCBC /* LoopSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D848B11E7DF42500DADCBC /* LoopSettings.swift */; }; 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */; }; @@ -159,7 +171,7 @@ 4F08DEA11E81D90F006741EA /* GlucoseRangeScheduleCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F08DEA01E81D90F006741EA /* GlucoseRangeScheduleCalculator.swift */; }; 4F08DEA31E81E12D006741EA /* DatedRangedContextCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F08DEA21E81E12D006741EA /* DatedRangedContextCalculator.swift */; }; 4F20AE621E6B879C00D07A06 /* ReservoirVolumeHUDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEC71CD84CBB003C8C80 /* ReservoirVolumeHUDView.swift */; }; - 4F20AE631E6B87B100D07A06 /* ChartContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */; }; + 4F20AE631E6B87B100D07A06 /* ChartContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */; }; 4F2C15741E0209F500E160D4 /* NSTimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 439897341CD2F7DE00223065 /* NSTimeInterval.swift */; }; 4F2C15751E0209FA00E160D4 /* GlucoseTrend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EA285E1D50ED3D001BC233 /* GlucoseTrend.swift */; }; 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F2C15801E0495B200E160D4 /* WatchContext+WatchApp.swift */; }; @@ -359,9 +371,10 @@ 430C1ABC1E5568A80067F1AE /* StatusChartsManager+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatusChartsManager+LoopKit.swift"; sourceTree = ""; }; 430DA58D1D4AEC230097D1CA /* NSBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSBundle.swift; sourceTree = ""; }; 430DA58F1D4B0E4C0097D1CA /* MySentryPumpStatusMessageBody.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MySentryPumpStatusMessageBody.swift; sourceTree = ""; }; - 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartContentView.swift; sourceTree = ""; }; + 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ChartContainerView.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryEditTableViewController.swift; sourceTree = ""; }; 4315D2891CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DiagnosticLogger+LoopKit.swift"; sourceTree = ""; }; + 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircleMaskView.swift; sourceTree = ""; }; 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusInterfaceController.swift; sourceTree = ""; }; 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusInterfaceController.swift; sourceTree = ""; }; 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddCarbsInterfaceController.swift; sourceTree = ""; }; @@ -377,7 +390,10 @@ 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseHUDView.swift; sourceTree = ""; }; 433EA4C11D9F39C900CD78FB /* PumpIDTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PumpIDTableViewController.swift; sourceTree = ""; }; 433EA4C31D9F71C800CD78FB /* CommandResponseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommandResponseViewController.swift; sourceTree = ""; }; - 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartTableViewCell.swift; sourceTree = ""; }; + 4341F4EA1EDB92AC001C936B /* LogglyService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogglyService.swift; sourceTree = ""; }; + 43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "StatusExtensionContext+LoopKit.swift"; sourceTree = ""; }; + 43441A9F1EDB4D390087958C /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ChartTableViewCell.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4346D1EF1C781BEA00ABAFE3 /* SwiftCharts.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftCharts.framework; path = Carthage/Build/iOS/SwiftCharts.framework; sourceTree = ""; }; 4346D1F51C78501000ABAFE3 /* ChartPoint+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ChartPoint+Loop.swift"; sourceTree = ""; }; 434AB0B11CBB4C3300422F4A /* RileyLinkBLEKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RileyLinkBLEKit.framework; path = Carthage/Build/iOS/RileyLinkBLEKit.framework; sourceTree = ""; }; @@ -394,8 +410,10 @@ 435400301C9F744E00D5819C /* BolusSuggestionUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BolusSuggestionUserInfo.swift; sourceTree = ""; }; 435400331C9F878D00D5819C /* SetBolusUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetBolusUserInfo.swift; sourceTree = ""; }; 43649A621C7A347F00523D7F /* CollectionType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionType.swift; sourceTree = ""; }; + 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsContextFillLayer.swift; sourceTree = ""; }; 436A0DA41D236A2A00104B24 /* LoopError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopError.swift; sourceTree = ""; }; 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDataSource.swift; sourceTree = ""; }; + 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WCSession+Swift4.swift"; sourceTree = ""; }; 43776F8C1B8022E90074EA36 /* Loop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop.app; sourceTree = BUILT_PRODUCTS_DIR; }; 43776F8F1B8022E90074EA36 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = AppDelegate.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43776F961B8022E90074EA36 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -430,6 +448,7 @@ 439BED291E76093C00B0AED5 /* CGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMManager.swift; sourceTree = ""; }; 439BED2B1E760A7A00B0AED5 /* DexCGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DexCGMManager.swift; sourceTree = ""; }; 439BED2D1E760BC600B0AED5 /* EnliteCGMManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnliteCGMManager.swift; sourceTree = ""; }; + 43A51E1E1EB6D62A000736CC /* CarbAbsorptionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbAbsorptionViewController.swift; sourceTree = ""; }; 43A51E201EB6DBDD000736CC /* ChartsTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartsTableViewController.swift; sourceTree = ""; }; 43A567681C94880B00334FAC /* LoopDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = LoopDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchTableViewCell.swift; sourceTree = ""; }; @@ -443,6 +462,7 @@ 43A9438D1B926B7B0051FA24 /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = ""; }; 43A9438F1B926B7B0051FA24 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43A943911B926B7B0051FA24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 43B260481ED248FB008CAA77 /* CarbEntryTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryTableViewCell.swift; sourceTree = ""; }; 43B371851CE583890013C5A6 /* BasalStateView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasalStateView.swift; sourceTree = ""; }; 43B371871CE597D10013C5A6 /* ShareClient.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ShareClient.framework; path = Carthage/Build/iOS/ShareClient.framework; sourceTree = ""; }; 43BFF0B11E45C18400FF19A9 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; @@ -455,6 +475,7 @@ 43BFF0CC1E466C8400FF19A9 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; 43C094491CACCC73001F6403 /* NotificationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 43C246A71D89990F0031F8D1 /* Crypto.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crypto.framework; path = Carthage/Build/iOS/Crypto.framework; sourceTree = ""; }; + 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseEffectVelocity.swift; sourceTree = ""; }; 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ShareGlucose+GlucoseKit.swift"; sourceTree = ""; }; 43C513181E864C4E001547C7 /* GlucoseRangeSchedule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlucoseRangeSchedule.swift; sourceTree = ""; }; 43C6407B1DA051850093E25D /* InsulinKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InsulinKit.framework; path = Carthage/Build/iOS/InsulinKit.framework; sourceTree = ""; }; @@ -462,6 +483,8 @@ 43CB2B2A1D924D450079823D /* WCSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WCSession.swift; sourceTree = ""; }; 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutUploader.swift; sourceTree = ""; }; + 43D2E8221F00425400AE5CBF /* BolusViewController+LoopDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BolusViewController+LoopDataManager.swift"; sourceTree = ""; }; + 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeaderValuesTableViewCell.swift; sourceTree = ""; }; 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "WatchApp Extension.entitlements"; sourceTree = ""; }; 43D848AF1E7DCBE100DADCBC /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Result.swift; sourceTree = ""; }; 43D848B11E7DF42500DADCBC /* LoopSettings.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopSettings.swift; sourceTree = ""; }; @@ -528,7 +551,7 @@ 4F6663931E905FD2009E74FC /* ChartColorPalette+Loop.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ChartColorPalette+Loop.swift"; sourceTree = ""; }; 4F70C1DC1DE8DCA7006380B7 /* Loop Status Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Loop Status Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 4F70C1DD1DE8DCA7006380B7 /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; }; - 4F70C1E01DE8DCA7006380B7 /* StatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusViewController.swift; sourceTree = ""; }; + 4F70C1E01DE8DCA7006380B7 /* StatusViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = StatusViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 4F70C1E31DE8DCA7006380B7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 4F70C1E51DE8DCA7006380B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 4F70C1FD1DE8E662006380B7 /* Loop Status Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Loop Status Extension.entitlements"; sourceTree = ""; }; @@ -666,6 +689,7 @@ 4309786D1E73DAD100BEBC82 /* CGM.swift */, 540DED961E14C75F002B2491 /* EnliteSensorDisplayable.swift */, 43E397A21D56B9E40028E321 /* Glucose.swift */, + 43C2FAE01EB656A500364AFF /* GlucoseEffectVelocity.swift */, 4D5B7A4A1D457CCA00796CA9 /* GlucoseG4.swift */, C178249D1E19B62300D9D25C /* GlucoseThreshold.swift */, 436FACED1D0BA636004E2427 /* InsulinDataSource.swift */, @@ -676,6 +700,7 @@ 438D42F81D7C88BC003244B0 /* PredictionInputEffect.swift */, 43D848AF1E7DCBE100DADCBC /* Result.swift */, 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */, + 43441A9B1EDB34810087958C /* StatusExtensionContext+LoopKit.swift */, 4328E0311CFC068900E199AA /* WatchContext+LoopKit.swift */, ); path = Models; @@ -736,6 +761,7 @@ isa = PBXGroup; children = ( 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */, + 4341F4EA1EDB92AC001C936B /* LogglyService.swift */, 438849ED1D2A1EBB003B3F23 /* MLabService.swift */, 438849E91D297CB6003B3F23 /* NightscoutService.swift */, 437CCADF1D285C7B0075D2C3 /* ServiceAuthentication.swift */, @@ -839,6 +865,7 @@ 43CEE6E51E56AFD400CB9116 /* NightscoutUploader.swift */, 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */, 43E344A31B9E1B1C00C85C07 /* NSUserDefaults.swift */, + 43441A9F1EDB4D390087958C /* OSLog.swift */, 43BFF0CA1E466C0900FF19A9 /* StateColorPalette.swift */, 43F41C361D3BF32400C11ED6 /* UIAlertController.swift */, 43BFF0BB1E45C80600FF19A9 /* UIColor+Loop.swift */, @@ -853,6 +880,8 @@ children = ( 437CCADD1D2858FD0075D2C3 /* AuthenticationViewController.swift */, 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */, + 43D2E8221F00425400AE5CBF /* BolusViewController+LoopDataManager.swift */, + 43A51E1E1EB6D62A000736CC /* CarbAbsorptionViewController.swift */, 4315D2861CA5CC3B00589052 /* CarbEntryEditTableViewController.swift */, 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */, 43A51E201EB6DBDD000736CC /* ChartsTableViewController.swift */, @@ -876,7 +905,10 @@ 434F545A1D2880D4002A9274 /* AuthenticationTableViewCell.xib */, 437CCADB1D284B830075D2C3 /* ButtonTableViewCell.swift */, 434F54581D28805E002A9274 /* ButtonTableViewCell.xib */, + 43B260481ED248FB008CAA77 /* CarbEntryTableViewCell.swift */, 4346D1E61C77F5FE00ABAFE3 /* ChartTableViewCell.swift */, + 431A8C3F1EC6E8AB00823B9C /* CircleMaskView.swift */, + 43D381611EBD9759007F8C8F /* HeaderValuesTableViewCell.swift */, 438D42FA1D7D11A4003244B0 /* PredictionInputEffectTableViewCell.swift */, 43A5676A1C96155700334FAC /* SwitchTableViewCell.swift */, 43F64DD81D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift */, @@ -953,7 +985,8 @@ 43B371851CE583890013C5A6 /* BasalStateView.swift */, 437CEEBB1CD6DE6A003C8C80 /* BaseHUDView.swift */, 437CEEC91CD84DB7003C8C80 /* BatteryLevelHUDView.swift */, - 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */, + 4313EDDF1D8A6BF90060FA79 /* ChartContainerView.swift */, + 4369618F1F19C86400447E89 /* ChartPointsContextFillLayer.swift */, 4F08DE831E7BB70B006741EA /* ChartPointsScatterDownTrianglesLayer.swift */, 4F08DE841E7BB70B006741EA /* ChartPointsTouchHighlightLayerViewCache.swift */, 4337615E1D52F487004A3647 /* GlucoseHUDView.swift */, @@ -1031,6 +1064,7 @@ 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */, 43BFF0B11E45C18400FF19A9 /* UIColor.swift */, 43BFF0C31E4659E700FF19A9 /* UIColor+HIG.swift */, + 437272DE1F09E41200A3DA02 /* WCSession+Swift4.swift */, ); path = Extensions; sourceTree = ""; @@ -1417,7 +1451,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# /usr/local/bin/carthage build --platform \"$PLATFORM_NAME\" \"$SRCROOT\"\n/usr/local/bin/carthage copy-frameworks"; + shellScript = "# /usr/local/bin/carthage build --platform \"$PLATFORM_NAME\" \"$SRCROOT\"\n# Only run this script if we're not using a workspace or installing\nif [ ! -d $PROJECT_DIR/Loop.xcworkspace ] || [ \"$ACTION\" = \"install\" ]; then\n /usr/local/bin/carthage copy-frameworks\nfi"; }; /* End PBXShellScriptBuildPhase section */ @@ -1429,15 +1463,18 @@ C17824A51E1AD4D100D9D25C /* BolusRecommendation.swift in Sources */, 4F70C2131DE90339006380B7 /* StatusExtensionContext.swift in Sources */, 434F54571D287FDB002A9274 /* NibLoadable.swift in Sources */, + 43441A9C1EDB34810087958C /* StatusExtensionContext+LoopKit.swift in Sources */, 4FF4D1001E18374700846527 /* WatchContext.swift in Sources */, 4315D28A1CA5F45E00589052 /* DiagnosticLogger+LoopKit.swift in Sources */, 43C418B51CE0575200405B6A /* ShareGlucose+GlucoseKit.swift in Sources */, 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, + 43A51E1F1EB6D62A000736CC /* CarbAbsorptionViewController.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, 437CCADA1D284ADF0075D2C3 /* AuthenticationTableViewCell.swift in Sources */, 439BED2E1E760BC600B0AED5 /* EnliteCGMManager.swift in Sources */, + 4341F4EB1EDB92AC001C936B /* LogglyService.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43BFF0CB1E466C0900FF19A9 /* StateColorPalette.swift in Sources */, 438991691E91B571000EEF90 /* ChartPoint.swift in Sources */, @@ -1449,6 +1486,7 @@ 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, 43D848B01E7DCBE100DADCBC /* Result.swift in Sources */, 43E397A31D56B9E40028E321 /* Glucose.swift in Sources */, + 43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, 43E344A41B9E1B1C00C85C07 /* NSUserDefaults.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, @@ -1478,9 +1516,11 @@ C178249A1E1999FA00D9D25C /* CaseCountable.swift in Sources */, 4F08DEA11E81D90F006741EA /* GlucoseRangeScheduleCalculator.swift in Sources */, 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */, + 437272DF1F09E41200A3DA02 /* WCSession+Swift4.swift in Sources */, 4FB76FBB1E8C42CF00B39636 /* UIColor.swift in Sources */, 4F6663941E905FD2009E74FC /* ChartColorPalette+Loop.swift in Sources */, 4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */, + 43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */, 43BFF0C51E465A2D00FF19A9 /* UIColor+HIG.swift in Sources */, 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */, 437CCAE01D285C7B0075D2C3 /* ServiceAuthentication.swift in Sources */, @@ -1495,7 +1535,9 @@ 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */, 43F41C371D3BF32400C11ED6 /* UIAlertController.swift in Sources */, 433EA4C41D9F71C800CD78FB /* CommandResponseViewController.swift in Sources */, + 43D2E8231F00425400AE5CBF /* BolusViewController+LoopDataManager.swift in Sources */, 434F545F1D288345002A9274 /* ShareService.swift in Sources */, + 43441AA01EDB4D390087958C /* OSLog.swift in Sources */, 43CEE6E61E56AFD400CB9116 /* NightscoutUploader.swift in Sources */, 4328E0331CFC091100E199AA /* WatchContext+LoopKit.swift in Sources */, 4F526D611DF8D9A900A04910 /* NetBasal.swift in Sources */, @@ -1510,6 +1552,7 @@ 434F54611D28859B002A9274 /* ServiceCredential.swift in Sources */, 4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */, 436FACEE1D0BA636004E2427 /* InsulinDataSource.swift in Sources */, + 431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */, 439897371CD2F80600223065 /* AnalyticsManager.swift in Sources */, 43A51E211EB6DBDD000736CC /* ChartsTableViewController.swift in Sources */, 4346D1F61C78501000ABAFE3 /* ChartPoint+Loop.swift in Sources */, @@ -1521,6 +1564,7 @@ 43DE92591C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */, 434F54631D28DD80002A9274 /* ValidatingIndicatorView.swift in Sources */, 43DE92611C555C26001FFDE1 /* AbsorptionTimeType+CarbKit.swift in Sources */, + 43C2FAE11EB656A500364AFF /* GlucoseEffectVelocity.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1536,6 +1580,7 @@ 43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */, 4328E0291CFBE2C500E199AA /* NSUserDefaults.swift in Sources */, 4328E02F1CFBF81800E199AA /* WKInterfaceImage.swift in Sources */, + 437272E01F09E41600A3DA02 /* WCSession+Swift4.swift in Sources */, 4F2C15811E0495B200E160D4 /* WatchContext+WatchApp.swift in Sources */, 4328E02A1CFBE2C500E199AA /* UIColor.swift in Sources */, 4328E01B1CFBE1DA00E199AA /* BolusInterfaceController.swift in Sources */, @@ -1603,6 +1648,7 @@ 4F20AE621E6B879C00D07A06 /* ReservoirVolumeHUDView.swift in Sources */, 4FB76FB91E8C42B000B39636 /* CollectionType.swift in Sources */, 4FF4D0F91E17268800846527 /* IdentifiableClass.swift in Sources */, + 436961911F19D11E00447E89 /* ChartPointsContextFillLayer.swift in Sources */, 4FF4D0F81E1725B000846527 /* NibLoadable.swift in Sources */, 4F7528AA1DFE215100C322D6 /* HKUnit.swift in Sources */, 4F7528A91DFE212600C322D6 /* GlucoseTrend.swift in Sources */, @@ -1616,7 +1662,7 @@ 4FB76FB41E8C3F7C00B39636 /* ChartAxisValueDoubleUnit.swift in Sources */, 4FB76FB31E8C3EE400B39636 /* ChartAxisValueDoubleLog.swift in Sources */, 4F7528A11DFE200B00C322D6 /* BasalStateView.swift in Sources */, - 4F20AE631E6B87B100D07A06 /* ChartContentView.swift in Sources */, + 4F20AE631E6B87B100D07A06 /* ChartContainerView.swift in Sources */, 4F7528A21DFE200B00C322D6 /* LevelMaskView.swift in Sources */, 43BFF0C61E465A4400FF19A9 /* UIColor+HIG.swift in Sources */, 4F7528A01DFE1F9D00C322D6 /* LoopStateView.swift in Sources */, @@ -1736,7 +1782,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1794,7 +1840,7 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer: loudnate@gmail.com (XZN842LDLT)"; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -2003,7 +2049,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -2022,7 +2067,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).statuswidget"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; }; name = Release; }; @@ -2035,11 +2079,11 @@ CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 37; + DYLIB_CURRENT_VERSION = 39; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LoopUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -2048,7 +2092,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -2063,11 +2106,11 @@ CLANG_WARN_SUSPICIOUS_MOVES = YES; CODE_SIGN_IDENTITY = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; - CURRENT_PROJECT_VERSION = 37; + CURRENT_PROJECT_VERSION = 39; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; - DYLIB_CURRENT_VERSION = 37; + DYLIB_CURRENT_VERSION = 39; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = LoopUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; @@ -2075,7 +2118,6 @@ PRODUCT_BUNDLE_IDENTIFIER = "$(MAIN_APP_BUNDLE_IDENTIFIER).LoopUI"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index d35f243872..bf79ce0f9e 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -23,7 +23,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { NotificationManager.authorize(delegate: self) - AnalyticsManager.sharedManager.application(application, didFinishLaunchingWithOptions: launchOptions) + let bundle = Bundle(for: type(of: self)) + DiagnosticLogger.shared = DiagnosticLogger(subsystem: bundle.bundleIdentifier!, version: bundle.shortVersionString) + DiagnosticLogger.shared?.forCategory("AppDelegate").info(#function) + + AnalyticsManager.shared.application(application, didFinishLaunchingWithOptions: launchOptions) if let navVC = window?.rootViewController as? UINavigationController, let statusVC = navVC.viewControllers.first as? StatusTableViewController { @@ -75,7 +79,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusStartDate.rawValue] as? Date, startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) { - AnalyticsManager.sharedManager.didRetryBolus() + AnalyticsManager.shared.didRetryBolus() deviceManager.enactBolus(units: units, at: startDate) { (_) in completionHandler() diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json b/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json index ec38b33a9d..dc36435801 100644 --- a/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Loop/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -3,37 +3,37 @@ { "size" : "20x20", "idiom" : "iphone", - "filename" : "Icon-20@2x.png", + "filename" : "Icon-Notification-40.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", - "filename" : "Icon-20@3x.png", + "filename" : "Icon-Small-60.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", - "filename" : "Icon-29@2x.png", + "filename" : "Icon-Small@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", - "filename" : "Icon-29@3x.png", + "filename" : "Icon-Small@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", - "filename" : "Icon-40@2x.png", + "filename" : "Icon-Small-40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", - "filename" : "Icon-40@3x.png", + "filename" : "Icon-Small-60@2x.png", "scale" : "3x" }, { @@ -51,37 +51,37 @@ { "size" : "20x20", "idiom" : "ipad", - "filename" : "Icon-20.png", + "filename" : "Icon-Notification-20.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", - "filename" : "Icon-20@2x-1.png", + "filename" : "Icon-Notification-42.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", - "filename" : "Icon-29.png", + "filename" : "Icon-Small.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", - "filename" : "Icon-29@2x-1.png", + "filename" : "Icon-Small@2x-1.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", - "filename" : "Icon-40.png", + "filename" : "Icon-Notification-41.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", - "filename" : "Icon-40@2x-1.png", + "filename" : "Icon-Small-40@2x-1.png", "scale" : "2x" }, { diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20.png deleted file mode 100644 index f2da6f6d70..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png deleted file mode 100644 index 7bf573b324..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png deleted file mode 100644 index 7bf573b324..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png deleted file mode 100644 index e6454a2e21..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-20@3x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29.png deleted file mode 100644 index 804594487c..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png deleted file mode 100644 index 0be6ed649f..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x-1.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png deleted file mode 100644 index 0be6ed649f..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@2x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png deleted file mode 100644 index 7390ffa97a..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-29@3x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40.png deleted file mode 100644 index 294f7ba752..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png deleted file mode 100644 index 1df0f29b6a..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x-1.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png deleted file mode 100644 index 1df0f29b6a..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@2x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png deleted file mode 100644 index b4e9fbb066..0000000000 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40@3x.png and /dev/null differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png index cf2432256f..754271234a 100644 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png index b6e68e9df7..b08ae598c6 100644 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-60@3x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png index ff0d7ee4a1..d916527459 100644 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png index 2e05d19a84..266aa5829a 100644 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-76@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png index dbba465aed..b2df83a839 100644 Binary files a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-83.5@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png new file mode 100644 index 0000000000..f70accfa03 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-20.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png new file mode 100644 index 0000000000..b3792921d7 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-40.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png new file mode 100644 index 0000000000..b3792921d7 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-41.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png new file mode 100644 index 0000000000..b3792921d7 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Notification-42.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png new file mode 100644 index 0000000000..b9b45b2865 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x-1.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png new file mode 100644 index 0000000000..b9b45b2865 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png new file mode 100644 index 0000000000..0ad6187e0b Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png new file mode 100644 index 0000000000..754271234a Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-60@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png new file mode 100644 index 0000000000..ec13a4cb23 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png new file mode 100644 index 0000000000..02e28324c5 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x-1.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000000..02e28324c5 Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 0000000000..3659954b6e Binary files /dev/null and b/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/Loop/Assets.xcassets/Uploading.imageset/Contents.json b/Loop/Assets.xcassets/Uploading.imageset/Contents.json new file mode 100644 index 0000000000..61b11d2882 --- /dev/null +++ b/Loop/Assets.xcassets/Uploading.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Uploading.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Loop/Assets.xcassets/Uploading.imageset/Uploading.pdf b/Loop/Assets.xcassets/Uploading.imageset/Uploading.pdf new file mode 100644 index 0000000000..d14e9d070d Binary files /dev/null and b/Loop/Assets.xcassets/Uploading.imageset/Uploading.pdf differ diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 41d32bf84e..24f48a7c71 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -1,11 +1,12 @@ - + - + + @@ -134,7 +135,7 @@ - + @@ -145,6 +146,7 @@ Future glucose is predicted by combining the effects of multiple inputs. Use this tool to toggle various inputs to see how they compare to the final prediction. + - + @@ -424,9 +720,9 @@ - + @@ -666,7 +962,7 @@ - + @@ -724,9 +1020,15 @@ + + + + + + diff --git a/Loop/Extensions/CaseCountable.swift b/Loop/Extensions/CaseCountable.swift index 8b6f0eb894..75166df062 100644 --- a/Loop/Extensions/CaseCountable.swift +++ b/Loop/Extensions/CaseCountable.swift @@ -8,10 +8,10 @@ protocol CaseCountable: RawRepresentable {} -extension CaseCountable where RawValue: Integer { +extension CaseCountable where RawValue == Int { static var count: Int { var i: RawValue = 0 while let new = Self(rawValue: i) { i = new.rawValue.advanced(by: 1) } - return Int(i.toIntMax()) + return i } } diff --git a/Loop/Extensions/DoseStore.swift b/Loop/Extensions/DoseStore.swift index 258d327295..97828d2b42 100644 --- a/Loop/Extensions/DoseStore.swift +++ b/Loop/Extensions/DoseStore.swift @@ -26,16 +26,7 @@ extension LoopDataManager { switch event.pumpEvent { case let bolus as BolusNormalPumpEvent: - let unit: DoseUnit - - switch bolus.type { - case .Normal: - unit = .units - case .Square: - unit = .unitsPerHour - } - - dose = DoseEntry(type: .bolus, startDate: event.date, endDate: event.date.addingTimeInterval(bolus.duration), value: bolus.amount, unit: unit) + dose = DoseEntry(type: .bolus, startDate: event.date, endDate: event.date.addingTimeInterval(bolus.duration), value: bolus.amount, unit: .units) case is SuspendPumpEvent: dose = DoseEntry(suspendDate: event.date) case is ResumePumpEvent: @@ -50,10 +41,14 @@ extension LoopDataManager { type: .tempBasal, startDate: event.date, endDate: event.date.addingTimeInterval(TimeInterval(minutes: Double(temp.duration))), - value: amount.value, - unit: amount.unit + value: amount.unitsPerHour, + unit: .unitsPerHour ) } + case is BasalProfileStartPumpEvent: + break + case is RewindPumpEvent: + break case is PrimePumpEvent: eventType = .prime default: diff --git a/Loop/Extensions/MealBolusNightscoutTreatment.swift b/Loop/Extensions/MealBolusNightscoutTreatment.swift index 43b1dda056..dc7d940dd8 100644 --- a/Loop/Extensions/MealBolusNightscoutTreatment.swift +++ b/Loop/Extensions/MealBolusNightscoutTreatment.swift @@ -14,6 +14,6 @@ import HealthKit extension MealBolusNightscoutTreatment { public convenience init(carbEntry: CarbEntry) { let carbGrams = carbEntry.quantity.doubleValue(for: HKUnit.gram()) - self.init(timestamp: carbEntry.startDate, enteredBy: "loop://\(UIDevice.current.name)", id: carbEntry.externalId, carbs: lround(carbGrams), absorptionTime: carbEntry.absorptionTime) + self.init(timestamp: carbEntry.startDate, enteredBy: "loop://\(UIDevice.current.name)", id: carbEntry.externalID, carbs: lround(carbGrams), absorptionTime: carbEntry.absorptionTime, foodType: carbEntry.foodType) } } diff --git a/Loop/Extensions/NSUserDefaults.swift b/Loop/Extensions/NSUserDefaults.swift index 8e84f5019b..5f5709574b 100644 --- a/Loop/Extensions/NSUserDefaults.swift +++ b/Loop/Extensions/NSUserDefaults.swift @@ -173,6 +173,20 @@ extension UserDefaults { } } + var insulinCounteractionEffects: [GlucoseEffectVelocity]? { + get { + guard let rawValue = array(forKey: Key.insulinCounteractionEffects.rawValue) as? [GlucoseEffectVelocity.RawValue] else { + return nil + } + return rawValue.flatMap { + GlucoseEffectVelocity(rawValue: $0) + } + } + set { + set(newValue?.map({ $0.rawValue }), forKey: Key.insulinCounteractionEffects.rawValue) + } + } + var insulinSensitivitySchedule: InsulinSensitivitySchedule? { get { if let rawValue = dictionary(forKey: Key.insulinSensitivitySchedule.rawValue) { diff --git a/Loop/Extensions/NightscoutUploader.swift b/Loop/Extensions/NightscoutUploader.swift index ef4de90478..bc913138f3 100644 --- a/Loop/Extensions/NightscoutUploader.swift +++ b/Loop/Extensions/NightscoutUploader.swift @@ -13,41 +13,45 @@ import NightscoutUploadKit extension NightscoutUploader: CarbStoreSyncDelegate { - public func carbStore(_ carbStore: CarbStore, hasEntriesNeedingUpload entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { - + static let logger = DiagnosticLogger.shared!.forCategory("NightscoutUploader") + + + public func carbStore(_ carbStore: CarbStore, hasEntriesNeedingUpload entries: [CarbEntry], completion: @escaping ([String]) -> Void) { let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) upload(nsCarbEntries) { (result) in switch result { case .success(let ids): // Pass new ids back - completionHandler(ids) - case .failure: - completionHandler([]) + completion(ids) + case .failure(let error): + NightscoutUploader.logger.error(error) + completion([]) } } } - public func carbStore(_ carbStore: CarbStore, hasModifiedEntries entries: [CarbEntry], withCompletion completionHandler: @escaping (_ uploadedObjects: [String]) -> Void) { + public func carbStore(_ carbStore: CarbStore, hasModifiedEntries entries: [CarbEntry], completion: @escaping (_ uploadedObjects: [String]) -> Void) { let nsCarbEntries = entries.map({ MealBolusNightscoutTreatment(carbEntry: $0)}) modifyTreatments(nsCarbEntries) { (error) in - if error != nil { - completionHandler([]) + if let error = error { + NightscoutUploader.logger.error(error) + completion([]) } else { - completionHandler(entries.map { $0.externalId ?? "" } ) + completion(entries.map { $0.externalID ?? "" } ) } } } - public func carbStore(_ carbStore: CarbStore, hasDeletedEntries ids: [String], withCompletion completionHandler: @escaping ([String]) -> Void) { + public func carbStore(_ carbStore: CarbStore, hasDeletedEntries ids: [String], completion: @escaping ([String]) -> Void) { deleteTreatmentsById(ids) { (error) in - if error != nil { - completionHandler([]) + if let error = error { + NightscoutUploader.logger.error(error) } else { - completionHandler(ids) + completion(ids) } } } diff --git a/Loop/Extensions/OSLog.swift b/Loop/Extensions/OSLog.swift new file mode 100644 index 0000000000..b0902d98c7 --- /dev/null +++ b/Loop/Extensions/OSLog.swift @@ -0,0 +1,27 @@ +// +// OSLog.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import os.log + + +extension OSLog { + func debug(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .debug, args) + } + + func info(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .info, args) + } + + func error(_ message: StaticString, _ args: CVarArg...) { + log(message, type: .error, args) + } + + private func log(_ message: StaticString, type: OSLogType, _ args: CVarArg...) { + os_log(message, log: self, type: type, args) + } +} diff --git a/Loop/Info.plist b/Loop/Info.plist index 42fed4b548..bae119566c 100644 --- a/Loop/Info.plist +++ b/Loop/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleSignature ???? CFBundleURLTypes diff --git a/Loop/Managers/AnalyticsManager.swift b/Loop/Managers/AnalyticsManager.swift index 5f48b269de..5c828c6575 100644 --- a/Loop/Managers/AnalyticsManager.swift +++ b/Loop/Managers/AnalyticsManager.swift @@ -26,7 +26,7 @@ final class AnalyticsManager { } } - static let sharedManager = AnalyticsManager() + static let shared = AnalyticsManager() // MARK: - Helpers diff --git a/Loop/Managers/CGM/CGMManager.swift b/Loop/Managers/CGM/CGMManager.swift index 2abc5f04e6..64615f545a 100644 --- a/Loop/Managers/CGM/CGMManager.swift +++ b/Loop/Managers/CGM/CGMManager.swift @@ -43,6 +43,9 @@ protocol CGMManager: CustomDebugStringConvertible { /// Whether the device is capable of waking the app var providesBLEHeartbeat: Bool { get } + /// The length of time to keep samples in HealthKit before removal. Return nil to never remove samples. + var managedDataInterval: TimeInterval? { get } + var sensorState: SensorDisplayable? { get } /// The representation of the device for use in HealthKit diff --git a/Loop/Managers/CGM/DexCGMManager.swift b/Loop/Managers/CGM/DexCGMManager.swift index c118e8bfba..e591d1f276 100644 --- a/Loop/Managers/CGM/DexCGMManager.swift +++ b/Loop/Managers/CGM/DexCGMManager.swift @@ -34,6 +34,10 @@ class DexCGMManager: CGMManager { return shareManager?.sensorState } + var managedDataInterval: TimeInterval? { + return shareManager?.managedDataInterval + } + fileprivate var shareManager: ShareClientManager? = ShareClientManager() var device: HKDevice? { @@ -53,12 +57,14 @@ class DexCGMManager: CGMManager { final class ShareClientManager: CGMManager { weak var delegate: CGMManagerDelegate? - var providesBLEHeartbeat = false + let providesBLEHeartbeat = false var sensorState: SensorDisplayable? { return latestBackfill } + let managedDataInterval: TimeInterval? = nil + private var latestBackfill: ShareGlucose? func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) { @@ -109,6 +115,7 @@ final class ShareClientManager: CGMManager { final class G5CGMManager: DexCGMManager, TransmitterDelegate { private let transmitter: Transmitter? + let logger = DiagnosticLogger.shared!.forCategory("G5CGMManager") init(transmitterID: String?) { if let transmitterID = transmitterID { @@ -128,6 +135,14 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate { return latestReading ?? super.sensorState } + override var managedDataInterval: TimeInterval? { + if let transmitter = transmitter, transmitter.passiveModeEnabled { + return .hours(3) + } + + return super.managedDataInterval + } + private var latestReading: Glucose? { didSet { // Once we have our first reading, disable backfill @@ -161,10 +176,15 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate { // MARK: - TransmitterDelegate func transmitter(_ transmitter: Transmitter, didError error: Error) { + logger.error(error) delegate?.cgmManager(self, didUpdateWith: .error(error)) } func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose) { + if !glucose.state.hasReliableGlucose { + logger.error(String(describing: glucose.state)) + } + guard glucose != latestReading, let quantity = glucose.glucose else { delegate?.cgmManager(self, didUpdateWith: .noData) return @@ -177,6 +197,7 @@ final class G5CGMManager: DexCGMManager, TransmitterDelegate { } func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data) { + logger.error("Unknown sensor data: " + data.hexadecimalString) // This can be used for protocol discovery, but isn't necessary for normal operation } } @@ -195,6 +216,11 @@ final class G4CGMManager: DexCGMManager, ReceiverDelegate { return latestReading ?? super.sensorState } + + override var managedDataInterval: TimeInterval? { + return .hours(3) + } + private var latestReading: GlucoseG4? { didSet { // Once we have our first reading, disable backfill @@ -255,3 +281,20 @@ final class G4CGMManager: DexCGMManager, ReceiverDelegate { // NSLog(["event": "\(event)", "collectedAt": NSDateFormatter.ISO8601StrictDateFormatter().stringFromDate(NSDate())]) } } + +extension CalibrationState: CustomStringConvertible { + public var description: String { + switch self { + case .needCalibration, .needFirstInitialCalibration, .needSecondInitialCalibration: + return NSLocalizedString("Sensor needs calibration", comment: "The description of sensor calibration state when sensor needs calibration.") + case .ok: + return NSLocalizedString("Sensor calibration is OK", comment: "The description of sensor calibration state when sensor calibration is ok.") + case .stopped: + return NSLocalizedString("Sensor is stopped", comment: "The description of sensor calibration state when sensor sensor is stopped.") + case .warmup: + return NSLocalizedString("Sensor is warming up", comment: "The description of sensor calibration state when sensor sensor is warming up.") + case .unknown(let rawValue): + return String(format: NSLocalizedString("Sensor is in unknown state %1$d", comment: "The description of sensor calibration state when raw value is unknown. (1: missing data details)"), rawValue) + } + } +} diff --git a/Loop/Managers/CGM/EnliteCGMManager.swift b/Loop/Managers/CGM/EnliteCGMManager.swift index c0a6587413..247137c803 100644 --- a/Loop/Managers/CGM/EnliteCGMManager.swift +++ b/Loop/Managers/CGM/EnliteCGMManager.swift @@ -12,12 +12,14 @@ import MinimedKit final class EnliteCGMManager: CGMManager { - var providesBLEHeartbeat = false + let providesBLEHeartbeat = false weak var delegate: CGMManagerDelegate? var sensorState: SensorDisplayable? + let managedDataInterval: TimeInterval? = nil + func fetchNewDataIfNeeded(with deviceManager: DeviceDataManager, _ completion: @escaping (CGMResult) -> Void) { guard let device = deviceManager.rileyLinkManager.firstConnectedDevice?.ops else { diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index a18e681e67..3af4b0c9cb 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -23,7 +23,7 @@ final class DeviceDataManager { // MARK: - Utilities - let logger = DiagnosticLogger() + let logger = DiagnosticLogger.shared! /// Remember the launch date of the app for diagnostic reporting fileprivate let launchDate = Date() @@ -61,7 +61,7 @@ final class DeviceDataManager { } if let oldVal = oldVal, newVal - oldVal >= 0.5 { - AnalyticsManager.sharedManager.pumpBatteryWasReplaced() + AnalyticsManager.shared.pumpBatteryWasReplaced() } } } @@ -93,7 +93,7 @@ final class DeviceDataManager { case is MySentryAlertMessageBody, is MySentryAlertClearedMessageBody: break case let body: - logger.addMessage(["messageType": Int(message.messageType.rawValue), "messageBody": body.txData.hexadecimalString], toCollection: "sentryOther") + logger.forCategory("MySentry").info(["messageType": Int(message.messageType.rawValue), "messageBody": body.txData.hexadecimalString]) } default: break @@ -112,7 +112,7 @@ final class DeviceDataManager { rileyLinkManager.connectDevice(device) - AnalyticsManager.sharedManager.didChangeRileyLinkConnectionState() + AnalyticsManager.shared.didChangeRileyLinkConnectionState() } func disconnectFromRileyLink(_ device: RileyLinkDevice) { @@ -120,7 +120,7 @@ final class DeviceDataManager { rileyLinkManager.disconnectDevice(device) - AnalyticsManager.sharedManager.didChangeRileyLinkConnectionState() + AnalyticsManager.shared.didChangeRileyLinkConnectionState() if connectedPeripheralIDs.count == 0 { NotificationManager.clearPendingNotificationRequests() @@ -246,7 +246,7 @@ final class DeviceDataManager { } if newValue.unitVolume > previousVolume + 1 { - AnalyticsManager.sharedManager.reservoirWasRewound() + AnalyticsManager.shared.reservoirWasRewound() } } } @@ -309,7 +309,7 @@ final class DeviceDataManager { completion(.failure(LoopError.invalidData(details: errorStr))) return } - completion(.success(status: status, date: date)) + completion(.success((status: status, date: date))) case .failure(let error): self.logger.addError("Failed to fetch pump status: \(error)", fromSource: "RileyLink") completion(.failure(error)) @@ -329,12 +329,16 @@ final class DeviceDataManager { Ensures pump data is current by either waking and polling, or ensuring we're listening to sentry packets. */ fileprivate func assertCurrentPumpData() { - guard let device = rileyLinkManager.firstConnectedDevice, pumpDataIsStale() else { + guard let device = rileyLinkManager.firstConnectedDevice else { return } device.assertIdleListening() + guard pumpDataIsStale() else { + return + } + readPumpData { (result) in let nsPumpStatus: NightscoutUploadKit.PumpStatus? switch result { @@ -469,6 +473,7 @@ final class DeviceDataManager { private func setupCGM() { cgmManager = cgm?.createManager() cgmManager?.delegate = self + loopManager.glucoseStore.managedDataInterval = cgmManager?.managedDataInterval /// Controls the management of the RileyLink timer tick, which is a reliably-changing BLE /// characteristic which can cause the app to wake. For most users, the G5 Transmitter and @@ -627,7 +632,8 @@ final class DeviceDataManager { statusExtensionManager = StatusExtensionDataManager(deviceDataManager: self) loopManager = LoopDataManager( delegate: self, - lastLoopCompleted: statusExtensionManager.context?.loop?.lastCompleted + lastLoopCompleted: statusExtensionManager.context?.loop?.lastCompleted, + lastTempBasal: statusExtensionManager.context?.netBasal?.tempBasal ) watchManager = WatchDataManager(deviceDataManager: self) nightscoutDataManager = NightscoutDataManager(deviceDataManager: self) @@ -680,7 +686,8 @@ extension DeviceDataManager: DoseStoreDelegate { case .success(let objects): completionHandler(objects) case .failure(let error): - self.logger.addError(error, fromSource: "NightscoutUploadKit") + let logger = DiagnosticLogger.shared!.forCategory("NightscoutUploader") + logger.error(error) completionHandler([]) } } diff --git a/Loop/Managers/DiagnosticLogger+LoopKit.swift b/Loop/Managers/DiagnosticLogger+LoopKit.swift index 5e610942a6..0bdf1e1717 100644 --- a/Loop/Managers/DiagnosticLogger+LoopKit.swift +++ b/Loop/Managers/DiagnosticLogger+LoopKit.swift @@ -13,16 +13,14 @@ import LoopKit extension DiagnosticLogger { func addError(_ message: String, fromSource source: String) { - let info = [ - "source": source, - "message": message, - "reportedAt": DateFormatter.ISO8601StrictDateFormatter().string(from: Date()) + let message = [ + "message": message ] - addMessage(info, toCollection: "errors") + forCategory(source).error(message) } func addError(_ message: Error, fromSource source: String) { - addError(String(describing: message), fromSource: source) + forCategory(source).error(message) } } diff --git a/Loop/Managers/DiagnosticLogger.swift b/Loop/Managers/DiagnosticLogger.swift index 84d6e707a2..e647cf21b1 100644 --- a/Loop/Managers/DiagnosticLogger.swift +++ b/Loop/Managers/DiagnosticLogger.swift @@ -7,10 +7,13 @@ // import Foundation +import os.log final class DiagnosticLogger { - private lazy var isSimulator: Bool = TARGET_OS_SIMULATOR != 0 + private let isSimulator: Bool = TARGET_OS_SIMULATOR != 0 + let subsystem: String + let version: String var mLabService: MLabService { didSet { @@ -18,23 +21,121 @@ final class DiagnosticLogger { } } - init() { + var logglyService: LogglyService { + didSet { + try! KeychainManager().setLogglyCustomerToken(logglyService.customerToken) + } + } + + let remoteLogLevel: OSLogType + + static var shared: DiagnosticLogger? + + init(subsystem: String, version: String) { + self.subsystem = subsystem + self.version = version + remoteLogLevel = isSimulator ? .fault : .info + if let (databaseName, APIKey) = KeychainManager().getMLabCredentials() { mLabService = MLabService(databaseName: databaseName, APIKey: APIKey) } else { mLabService = MLabService(databaseName: nil, APIKey: nil) } + + let customerToken = KeychainManager().getLogglyCustomerToken() + logglyService = LogglyService(customerToken: customerToken) } - func addMessage(_ message: [String: Any], toCollection collection: String) { - if !isSimulator, - let messageData = try? JSONSerialization.data(withJSONObject: message, options: []), - let task = mLabService.uploadTaskWithData(messageData, inCollection: collection) - { - task.resume() - } else { - NSLog("%@: %@", collection, message) + func forCategory(_ category: String) -> CategoryLogger { + return CategoryLogger(logger: self, category: category) + } +} + + +extension OSLogType { + fileprivate var tagName: String { + switch self { + case let t where t == .info: + return "info" + case let t where t == .debug: + return "debug" + case let t where t == .error: + return "error" + case let t where t == .fault: + return "fault" + default: + return "default" + } + } +} + + +final class CategoryLogger { + private let logger: DiagnosticLogger + let category: String + + private let systemLog: OSLog + + fileprivate init(logger: DiagnosticLogger, category: String) { + self.logger = logger + self.category = category + + systemLog = OSLog(subsystem: logger.subsystem, category: category) + } + + private func remoteLog(_ type: OSLogType, message: String) { + guard logger.remoteLogLevel.rawValue <= type.rawValue else { + return + } + + logger.logglyService.client?.send(message, tags: [type.tagName, category]) + } + + private func remoteLog(_ type: OSLogType, message: [String: Any]) { + guard logger.remoteLogLevel.rawValue <= type.rawValue else { + return } + + logger.logglyService.client?.send(message, tags: [type.tagName, category]) + + // Legacy mLab logging. To be removed. + if let messageData = try? JSONSerialization.data(withJSONObject: message, options: []) { + logger.mLabService.uploadTaskWithData(messageData, inCollection: category)?.resume() + } + } + + func debug(_ message: [String: Any]) { + systemLog.debug("%{public}@", String(describing: message)) + remoteLog(.debug, message: message) + } + + func debug(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.debug, message: message) + } + + func info(_ message: [String: Any]) { + systemLog.info("%{public}@", String(describing: message)) + remoteLog(.info, message: message) + } + + func info(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.info, message: message) + } + + func error(_ message: [String: Any]) { + systemLog.error("%{public}@", String(reflecting: message)) + remoteLog(.error, message: message) + } + + func error(_ message: String) { + systemLog.error("%{public}@", message) + remoteLog(.error, message: message) + } + + func error(_ error: Error) { + self.error(String(reflecting: error)) } } diff --git a/Loop/Managers/DoseMath.swift b/Loop/Managers/DoseMath.swift index 3511a7d142..aee2fd3ce2 100644 --- a/Loop/Managers/DoseMath.swift +++ b/Loop/Managers/DoseMath.swift @@ -7,12 +7,13 @@ // import Foundation +import CarbKit import HealthKit import InsulinKit import LoopKit -struct DoseMath { +enum DoseMath { /// The allowed precision static let basalStrokes: Double = 40 @@ -115,7 +116,7 @@ struct DoseMath { if let lastTempBasal = lastTempBasal, lastTempBasal.unit == .unitsPerHour && lastTempBasal.endDate > date { if let determinedRate = rate { // Ignore the dose if the current dose is the same rate and has more than 10 minutes remaining - if determinedRate == lastTempBasal.value && lastTempBasal.endDate.timeIntervalSince(date) > TimeInterval(minutes: 11) { + if determinedRate == lastTempBasal.unitsPerHour && lastTempBasal.endDate.timeIntervalSince(date) > TimeInterval(minutes: 11) { rate = nil } } else { diff --git a/Loop/Managers/KeychainManager+Loop.swift b/Loop/Managers/KeychainManager+Loop.swift index d2e2225c48..d54a562c77 100644 --- a/Loop/Managers/KeychainManager+Loop.swift +++ b/Loop/Managers/KeychainManager+Loop.swift @@ -9,42 +9,10 @@ import Foundation -private let AmplitudeAPIKeyService = "AmplitudeAPIKey" -private let DexcomShareURL = URL(string: "https://share1.dexcom.com")! private let NightscoutAccount = "NightscoutAPI" extension KeychainManager { - func setAmplitudeAPIKey(_ key: String?) throws { - try replaceGenericPassword(key, forService: AmplitudeAPIKeyService) - } - - func getAmplitudeAPIKey() -> String? { - return try? getGenericPasswordForService(AmplitudeAPIKeyService) - } - - func setDexcomShareUsername(_ username: String?, password: String?) throws { - let credentials: InternetCredentials? - - if let username = username, let password = password { - credentials = InternetCredentials(username: username, password: password, url: DexcomShareURL) - } else { - credentials = nil - } - - try replaceInternetCredentials(credentials, forURL: DexcomShareURL) - } - - func getDexcomShareCredentials() -> (username: String, password: String)? { - do { - let credentials = try getInternetCredentials(url: DexcomShareURL) - - return (username: credentials.username, password: credentials.password) - } catch { - return nil - } - } - func setNightscoutURL(_ url: URL?, secret: String?) { let credentials: InternetCredentials? diff --git a/Loop/Managers/KeychainManager.swift b/Loop/Managers/KeychainManager.swift index ff0e0db0d6..32258252f7 100644 --- a/Loop/Managers/KeychainManager.swift +++ b/Loop/Managers/KeychainManager.swift @@ -55,7 +55,7 @@ struct KeychainManager { return query } - private func queryForInternetPassword(account: String? = nil, url: URL? = nil) -> Query { + private func queryForInternetPassword(account: String? = nil, url: URL? = nil, label: String? = nil) -> Query { var query = self.query(by: kSecClassInternetPassword) if let account = account { @@ -68,6 +68,10 @@ struct KeychainManager { } } + if let label = label { + query[kSecAttrLabel as String] = label as NSObject? + } + return query } @@ -135,8 +139,8 @@ struct KeychainManager { // MARK – Internet Passwords - func setInternetPassword(_ password: String, forAccount account: String, atURL url: URL) throws { - var query = try updatedQuery(queryForInternetPassword(account: account, url: url), withPassword: password) + func setInternetPassword(_ password: String, account: String, atURL url: URL, label: String? = nil) throws { + var query = try updatedQuery(queryForInternetPassword(account: account, url: url, label: label), withPassword: password) query[kSecAttrAccount as String] = account as NSObject? @@ -146,6 +150,10 @@ struct KeychainManager { } } + if let label = label { + query[kSecAttrLabel as String] = label as NSObject? + } + let statusCode = SecItemAdd(query as CFDictionary, nil) guard statusCode == errSecSuccess else { @@ -159,7 +167,17 @@ struct KeychainManager { try delete(query) if let credentials = credentials { - try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url) + try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url) + } + } + + func replaceInternetCredentials(_ credentials: InternetCredentials?, forLabel label: String) throws { + let query = queryForInternetPassword(label: label) + + try delete(query) + + if let credentials = credentials { + try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url, label: label) } } @@ -169,12 +187,12 @@ struct KeychainManager { try delete(query) if let credentials = credentials { - try setInternetPassword(credentials.password, forAccount: credentials.username, atURL: credentials.url) + try setInternetPassword(credentials.password, account: credentials.username, atURL: credentials.url) } } - func getInternetCredentials(account: String? = nil, url: URL? = nil) throws -> InternetCredentials { - var query = queryForInternetPassword(account: account, url: url) + func getInternetCredentials(account: String? = nil, url: URL? = nil, label: String? = nil) throws -> InternetCredentials { + var query = queryForInternetPassword(account: account, url: url, label: label) query[kSecReturnData as String] = kCFBooleanTrue query[kSecReturnAttributes as String] = kCFBooleanTrue diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 79a3a84f41..4d60c51252 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -27,7 +27,7 @@ final class LoopDataManager { typealias TempBasalRecommendation = (recommendedDate: Date, rate: Double, duration: TimeInterval) - private typealias GlucoseChange = (start: GlucoseValue, end: GlucoseValue) + fileprivate typealias GlucoseChange = (start: GlucoseValue, end: GlucoseValue) let carbStore: CarbStore! @@ -37,19 +37,24 @@ final class LoopDataManager { unowned let delegate: LoopDataManagerDelegate - private let logger = DiagnosticLogger() + private let logger: CategoryLogger init( delegate: LoopDataManagerDelegate, lastLoopCompleted: Date?, + lastTempBasal: DoseEntry?, basalRateSchedule: BasalRateSchedule? = UserDefaults.standard.basalRateSchedule, carbRatioSchedule: CarbRatioSchedule? = UserDefaults.standard.carbRatioSchedule, insulinActionDuration: TimeInterval? = UserDefaults.standard.insulinActionDuration, + insulinCounteractionEffects: [GlucoseEffectVelocity]? = UserDefaults.standard.insulinCounteractionEffects, insulinSensitivitySchedule: InsulinSensitivitySchedule? = UserDefaults.standard.insulinSensitivitySchedule, settings: LoopSettings = UserDefaults.standard.loopSettings ?? LoopSettings() ) { self.delegate = delegate + self.logger = DiagnosticLogger.shared!.forCategory("LoopDataManager") + self.insulinCounteractionEffects = insulinCounteractionEffects ?? [] self.lastLoopCompleted = lastLoopCompleted + self.lastTempBasal = lastTempBasal self.settings = settings carbStore = CarbStore( @@ -76,7 +81,7 @@ final class LoopDataManager { ) { (note) -> Void in self.dataAccessQueue.async { self.carbEffect = nil - self.carbsOnBoardSeries = nil + self.carbsOnBoard = nil self.notify(forChange: .carbs) } } @@ -91,7 +96,7 @@ final class LoopDataManager { didSet { UserDefaults.standard.loopSettings = settings notify(forChange: .preferences) - AnalyticsManager.sharedManager.didChangeLoopSettings(from: oldValue, to: settings) + AnalyticsManager.shared.didChangeLoopSettings(from: oldValue, to: settings) } } @@ -116,6 +121,11 @@ final class LoopDataManager { set { carbStore.carbRatioSchedule = newValue UserDefaults.standard.carbRatioSchedule = newValue + + // Invalidate cached effects based on this schedule + carbEffect = nil + carbsOnBoard = nil + notify(forChange: .preferences) } } @@ -157,12 +167,24 @@ final class LoopDataManager { UserDefaults.standard.insulinActionDuration = newValue + // Invalidate cached effects based on this schedule + insulinEffect = nil + if oldValue != newValue { - AnalyticsManager.sharedManager.didChangeInsulinActionDuration() + AnalyticsManager.shared.didChangeInsulinActionDuration() } } } + /// A timeline of average velocity of glucose change counteracting predicted insulin effects + fileprivate var insulinCounteractionEffects: [GlucoseEffectVelocity] { + didSet { + UserDefaults.standard.insulinCounteractionEffects = insulinCounteractionEffects + carbEffect = nil + carbsOnBoard = nil + } + } + /// The daily schedule of insulin sensitivity (also known as ISF) /// This is measured in /Unit var insulinSensitivitySchedule: InsulinSensitivitySchedule? { @@ -175,6 +197,11 @@ final class LoopDataManager { UserDefaults.standard.insulinSensitivitySchedule = newValue + // Invalidate cached effects based on this schedule + carbEffect = nil + carbsOnBoard = nil + insulinEffect = nil + notify(forChange: .preferences) } } @@ -223,6 +250,7 @@ final class LoopDataManager { if success { self.dataAccessQueue.async { self.glucoseMomentumEffect = nil + self.lastGlucoseChange = nil self.retrospectiveGlucoseChange = nil self.notify(forChange: .glucose) } @@ -242,12 +270,12 @@ final class LoopDataManager { /// - carbEntry: The new carb value /// - completion: A closure called once upon completion /// - result: The bolus recommendation - func addCarbEntryAndRecommendBolus(_ carbEntry: CarbEntry, completion: @escaping (_ result: Result) -> Void) { - carbStore.addCarbEntry(carbEntry) { (success, _, error) in + func addCarbEntryAndRecommendBolus(_ carbEntry: CarbEntry, replacing replacingEntry: CarbEntry? = nil, completion: @escaping (_ result: Result) -> Void) { + let addCompletion: (Bool, CarbEntry?, CarbStore.CarbStoreError?) -> Void = { (success, _, error) in self.dataAccessQueue.async { if success { self.carbEffect = nil - self.carbsOnBoardSeries = nil + self.carbsOnBoard = nil defer { self.notify(forChange: .carbs) @@ -267,6 +295,12 @@ final class LoopDataManager { } } } + + if let replacingEntry = replacingEntry { + carbStore.replaceCarbEntry(replacingEntry, withEntry: carbEntry, resultHandler: addCompletion) + } else { + carbStore.addCarbEntry(carbEntry, resultHandler: addCompletion) + } } /// Adds a bolus enacted by the pump, but not fully delivered. @@ -291,7 +325,6 @@ final class LoopDataManager { doseStore.addPumpEvents(events) { (error) in if error != nil { self.insulinEffect = nil - self.insulinOnBoard = nil } completion(error) @@ -314,7 +347,6 @@ final class LoopDataManager { completion(.failure(error)) } else if let newValue = newValue { self.insulinEffect = nil - self.insulinOnBoard = nil completion(.success(( newValue: newValue, @@ -353,7 +385,7 @@ final class LoopDataManager { self.lastLoopError = error if let error = error { - self.logger.addError(error, fromSource: "TempBasal") + self.logger.error(error) } else { self.lastLoopCompleted = Date() } @@ -392,7 +424,15 @@ final class LoopDataManager { let updateGroup = DispatchGroup() // Fetch glucose effects as far back as we want to make retroactive analysis - guard let lastGlucoseDate = glucoseStore.latestGlucose?.startDate else { + var latestGlucoseDate: Date? + updateGroup.enter() + glucoseStore.getCachedGlucoseValues(start: Date(timeIntervalSinceNow: -recencyInterval)) { (values) in + latestGlucoseDate = values.last?.startDate + updateGroup.leave() + } + _ = updateGroup.wait(timeout: .distantFuture) + + guard let lastGlucoseDate = latestGlucoseDate else { throw LoopError.missingDataError(details: "Glucose data not available", recovery: "Check your CGM data source") } @@ -400,20 +440,27 @@ final class LoopDataManager { if retrospectiveGlucoseChange == nil { updateGroup.enter() - glucoseStore.getRecentGlucoseChange { (change, error) in - if let error = error { - self.logger.addError(error, fromSource: "GlucoseStore") - } + glucoseStore.getGlucoseChange(start: retrospectiveStart) { (change) in self.retrospectiveGlucoseChange = change updateGroup.leave() } } + if lastGlucoseChange == nil { + updateGroup.enter() + let start = insulinCounteractionEffects.last?.endDate ?? lastGlucoseDate.addingTimeInterval(.minutes(-5.1)) + + glucoseStore.getGlucoseChange(start: start) { (change) in + self.lastGlucoseChange = change + updateGroup.leave() + } + } + if glucoseMomentumEffect == nil { updateGroup.enter() glucoseStore.getRecentMomentumEffect { (effects, error) -> Void in if let error = error, effects.count == 0 { - self.logger.addError(error, fromSource: "GlucoseStore") + self.logger.error(error) self.glucoseMomentumEffect = nil } else { self.glucoseMomentumEffect = effects @@ -423,56 +470,59 @@ final class LoopDataManager { } } - if carbEffect == nil { + if insulinEffect == nil { updateGroup.enter() - carbStore.getGlucoseEffects(startDate: retrospectiveStart) { (effects, error) -> Void in - if let error = error { - self.logger.addError(error, fromSource: "CarbStore") - self.carbEffect = nil - } else { - self.carbEffect = effects + doseStore.getGlucoseEffects(start: retrospectiveStart) { (result) -> Void in + switch result { + case .failure(let error): + self.logger.error(error) + self.insulinEffect = nil + case .success(let effects): + self.insulinEffect = effects } updateGroup.leave() } } - if carbsOnBoardSeries == nil { - updateGroup.enter() - carbStore.getCarbsOnBoardValues(startDate: retrospectiveStart) { (values, error) in - if let error = error { - self.logger.addError(error, fromSource: "CarbStore") - } + _ = updateGroup.wait(timeout: .distantFuture) - self.carbsOnBoardSeries = values - updateGroup.leave() + if insulinCounteractionEffects.last == nil || + insulinCounteractionEffects.last!.endDate < lastGlucoseDate { + do { + try updateObservedInsulinCounteractionEffects() + } catch let error { + logger.error(error) } } - if insulinEffect == nil { + if carbEffect == nil { updateGroup.enter() - doseStore.getGlucoseEffects(start: retrospectiveStart) { (result) -> Void in + carbStore.getGlucoseEffects( + start: retrospectiveStart, + effectVelocities: settings.dynamicCarbAbsorptionEnabled ? insulinCounteractionEffects : nil + ) { (result) -> Void in switch result { case .failure(let error): - self.logger.addError(error, fromSource: "DoseStore") - self.insulinEffect = nil + self.logger.error(error) + self.carbEffect = nil case .success(let effects): - self.insulinEffect = effects + self.carbEffect = effects } updateGroup.leave() } } - if insulinOnBoard == nil { + if carbsOnBoard == nil { updateGroup.enter() - doseStore.insulinOnBoard(at: Date()) { (result) in + carbStore.carbsOnBoard(at: Date(), effectVelocities: settings.dynamicCarbAbsorptionEnabled ? insulinCounteractionEffects : nil) { (result) in switch result { - case .failure(let error): - self.logger.addError(error, fromSource: "DoseStore") - self.insulinOnBoard = nil + case .failure: + // Failure is expected when there is no carb data + self.carbsOnBoard = nil case .success(let value): - self.insulinOnBoard = value + self.carbsOnBoard = value } updateGroup.leave() } @@ -484,7 +534,7 @@ final class LoopDataManager { do { try updateRetrospectiveGlucoseEffect() } catch let error { - logger.addError(error, fromSource: "RetrospectiveGlucose") + logger.error(error) } } @@ -492,7 +542,7 @@ final class LoopDataManager { do { try updatePredictedGlucoseAndRecommendedBasal() } catch let error { - logger.addError(error, fromSource: "PredictGlucose") + logger.error(error) throw error } @@ -522,10 +572,10 @@ final class LoopDataManager { let pendingTempBasalInsulin: Double let date = Date() - if let lastTempBasal = lastTempBasal, lastTempBasal.unit == .unitsPerHour && lastTempBasal.endDate > date { + if let lastTempBasal = lastTempBasal, lastTempBasal.endDate > date { let normalBasalRate = basalRates.value(at: date) let remainingTime = lastTempBasal.endDate.timeIntervalSince(date) - let remainingUnits = (lastTempBasal.value - normalBasalRate) * remainingTime / TimeInterval(hours: 1) + let remainingUnits = (lastTempBasal.unitsPerHour - normalBasalRate) * remainingTime.hours pendingTempBasalInsulin = max(0, remainingUnits) } else { @@ -601,11 +651,13 @@ final class LoopDataManager { } /// The change in glucose over the reflection time interval (default is 30 min) - private var retrospectiveGlucoseChange: GlucoseChange? { + fileprivate var retrospectiveGlucoseChange: GlucoseChange? { didSet { retrospectivePredictedGlucose = nil } } + /// The change in glucose over the last loop interval (5 min) + fileprivate var lastGlucoseChange: GlucoseChange? fileprivate var predictedGlucose: [GlucoseValue]? { didSet { @@ -619,8 +671,7 @@ final class LoopDataManager { } fileprivate var recommendedTempBasal: TempBasalRecommendation? - fileprivate var carbsOnBoardSeries: [CarbValue]? - fileprivate var insulinOnBoard: InsulinValue? + fileprivate var carbsOnBoard: CarbValue? fileprivate var lastTempBasal: DoseEntry? fileprivate var lastBolus: (units: Double, date: Date)? @@ -628,13 +679,13 @@ final class LoopDataManager { didSet { NotificationManager.scheduleLoopNotRunningNotifications() - AnalyticsManager.sharedManager.loopDidSucceed() + AnalyticsManager.shared.loopDidSucceed() } } fileprivate var lastLoopError: Error? { didSet { if lastLoopError != nil { - AnalyticsManager.sharedManager.loopDidError() + AnalyticsManager.shared.loopDidError() } } } @@ -662,7 +713,7 @@ final class LoopDataManager { // Run a retrospective prediction over the duration of the recorded glucose change, using the current carb and insulin effects let startDate = change.start.startDate - let endDate = change.end.endDate.addingTimeInterval(TimeInterval(minutes: 5)) + let endDate = change.end.endDate let retrospectivePrediction = LoopMath.predictGlucose(change.start, effects: carbEffect.filterDateRange(startDate, endDate), insulinEffect.filterDateRange(startDate, endDate) @@ -682,6 +733,44 @@ final class LoopDataManager { self.retrospectiveGlucoseEffect = LoopMath.decayEffect(from: glucose, atRate: velocity, for: effectDuration) } + /// Measure the effects counteracting insulin observed in the CGM glucose. + /// + /// If you assume insulin is "right", this allows for some validation of carb algorithm settings. + /// + /// - Throws: LoopError.missingDataError if effect data isn't available + private func updateObservedInsulinCounteractionEffects() throws { + dispatchPrecondition(condition: .onQueue(dataAccessQueue)) + + guard + let insulinEffect = self.insulinEffect + else { + throw LoopError.missingDataError(details: "Cannot calculate insulin counteraction due to missing input data", recovery: nil) + } + + guard let change = lastGlucoseChange else { + return // Expected case for calibrations + } + + // Predict glucose change using only insulin effects over the last loop interval + let startDate = change.start.startDate + let endDate = change.end.endDate.addingTimeInterval(TimeInterval(minutes: 5)) + let prediction = LoopMath.predictGlucose(change.start, effects: + insulinEffect.filterDateRange(startDate, endDate) + ) + + // Compare that retrospective, insulin-driven prediction to the actual glucose change to + // calculate the effect of all insulin counteraction + guard let lastGlucose = prediction.last else { return } + let glucoseUnit = HKUnit.milligramsPerDeciliter() + let velocityUnit = glucoseUnit.unitDivided(by: HKUnit.second()) + let discrepancy = change.end.quantity.doubleValue(for: glucoseUnit) - lastGlucose.quantity.doubleValue(for: glucoseUnit) // mg/dL + let averageVelocity = HKQuantity(unit: velocityUnit, doubleValue: discrepancy / change.end.endDate.timeIntervalSince(change.start.endDate)) + let effect = GlucoseEffectVelocity(startDate: change.start.startDate, endDate: change.end.startDate, quantity: averageVelocity) + + insulinCounteractionEffects.append(effect) + // For now, only keep the last 24 hours of values + insulinCounteractionEffects = insulinCounteractionEffects.filterDateRange(Date(timeIntervalSinceNow: .hours(-24)), nil) + } /// Runs the glucose prediction on the latest effect data. /// @@ -835,9 +924,15 @@ final class LoopDataManager { /// Describes a view into the loop state protocol LoopState { + /// The last-calculated carbs on board + var carbsOnBoard: CarbValue? { get } + /// An error in the current state of the loop, or one that happened during the last attempt to loop. var error: Error? { get } + /// A timeline of average velocity of glucose change counteracting predicted insulin effects + var insulinCounteractionEffects: [GlucoseEffectVelocity] { get } + /// The last date at which a loop completed, from prediction to dose (if dosing is enabled) var lastLoopCompleted: Date? { get } @@ -870,14 +965,6 @@ protocol LoopState { /// - LoopError.glucoseTooOld /// - LoopError.missingDataError func recommendBolus() throws -> BolusRecommendation - - // TODO: These values are duplicative and don't need to be cached in LoopDataManager - - /// Current carbs on board - var carbsOnBoard: CarbValue? { get } - - /// Current insulin on board - var insulinOnBoard: InsulinValue? { get } } @@ -891,11 +978,21 @@ extension LoopDataManager { self.updateError = updateError } + var carbsOnBoard: CarbValue? { + dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) + return loopDataManager.carbsOnBoard + } + var error: Error? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) return updateError ?? loopDataManager.lastLoopError } + var insulinCounteractionEffects: [GlucoseEffectVelocity] { + dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) + return loopDataManager.insulinCounteractionEffects + } + var lastLoopCompleted: Date? { dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) return loopDataManager.lastLoopCompleted @@ -921,16 +1018,6 @@ extension LoopDataManager { return loopDataManager.retrospectivePredictedGlucose } - var carbsOnBoard: CarbValue? { - dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) - return loopDataManager.carbsOnBoardSeries?.closestPriorToDate(Date()) - } - - var insulinOnBoard: InsulinValue? { - dispatchPrecondition(condition: .onQueue(loopDataManager.dataAccessQueue)) - return loopDataManager.insulinOnBoard - } - func predictGlucose(using inputs: PredictionInputEffect) throws -> [GlucoseValue] { return try loopDataManager.predictGlucose(using: inputs) } @@ -971,33 +1058,56 @@ extension LoopDataManager { /// - parameter completion: A closure called once the report has been generated. The closure takes a single argument of the report string. func generateDiagnosticReport(_ completion: @escaping (_ report: String) -> Void) { getLoopState { (manager, state) in + var entries = [ "## LoopDataManager", "settings: \(String(reflecting: manager.settings))", + "insulinCounteractionEffects: \(String(reflecting: manager.insulinCounteractionEffects))", "predictedGlucose: \(state.predictedGlucose ?? [])", "retrospectivePredictedGlucose: \(state.retrospectivePredictedGlucose ?? [])", "recommendedTempBasal: \(String(describing: state.recommendedTempBasal))", - "lastTempBasal: \(String(describing: state.lastTempBasal))", "lastBolus: \(String(describing: manager.lastBolus))", + "lastGlucoseChange: \(String(describing: manager.lastGlucoseChange))", + "retrospectiveGlucoseChange: \(String(describing: manager.retrospectiveGlucoseChange))", "lastLoopCompleted: \(String(describing: state.lastLoopCompleted))", - "insulinOnBoard: \(String(describing: state.insulinOnBoard))", - "carbsOnBoard: \(String(describing: state.carbsOnBoard))", - "error: \(String(describing: state.error))" + "lastTempBasal: \(String(describing: state.lastTempBasal))", + "carbsOnBoard: \(String(describing: state.carbsOnBoard))" ] + var loopError = state.error + + // TODO: this should be moved to doseStore.generateDiagnosticReport + self.doseStore.insulinOnBoard(at: Date()) { (result) in - self.glucoseStore.generateDiagnosticReport { (report) in - entries.append(report) - entries.append("") + let insulinOnBoard: InsulinValue? + + switch result { + case .success(let value): + insulinOnBoard = value + case .failure(let error): + insulinOnBoard = nil + + if loopError == nil { + loopError = error + } + } + + entries.append("insulinOnBoard: \(String(describing: insulinOnBoard))") + entries.append("error: \(String(describing: loopError))") - self.carbStore.generateDiagnosticReport { (report) in + self.glucoseStore.generateDiagnosticReport { (report) in entries.append(report) entries.append("") - self.doseStore.generateDiagnosticReport { (report) in + self.carbStore.generateDiagnosticReport { (report) in entries.append(report) entries.append("") - completion(entries.joined(separator: "\n")) + self.doseStore.generateDiagnosticReport { (report) in + entries.append(report) + entries.append("") + + completion(entries.joined(separator: "\n")) + } } } } diff --git a/Loop/Managers/NightscoutDataManager.swift b/Loop/Managers/NightscoutDataManager.swift index 24f3f62041..db3714ae0a 100644 --- a/Loop/Managers/NightscoutDataManager.swift +++ b/Loop/Managers/NightscoutDataManager.swift @@ -37,29 +37,49 @@ final class NightscoutDataManager { return } - deviceDataManager.loopManager.getLoopState { (_, state) in + deviceDataManager.loopManager.getLoopState { (manager, state) in var loopError = state.error - let recommendation: Double? + let recommendedBolus: Double? do { - recommendation = try state.recommendBolus().amount + recommendedBolus = try state.recommendBolus().amount } catch let error { - recommendation = nil + recommendedBolus = nil if loopError == nil { loopError = error } } - self.uploadLoopStatus( - insulinOnBoard: state.insulinOnBoard, - carbsOnBoard: state.carbsOnBoard, - predictedGlucose: state.predictedGlucose, - recommendedTempBasal: state.recommendedTempBasal, - recommendedBolus: recommendation, - lastTempBasal: state.lastTempBasal, - loopError: loopError - ) + let carbsOnBoard = state.carbsOnBoard + let predictedGlucose = state.predictedGlucose + let recommendedTempBasal = state.recommendedTempBasal + let lastTempBasal = state.lastTempBasal + + manager.doseStore.insulinOnBoard(at: Date()) { (result) in + let insulinOnBoard: InsulinValue? + + switch result { + case .success(let value): + insulinOnBoard = value + case .failure(let error): + insulinOnBoard = nil + + if loopError == nil { + loopError = error + } + } + + self.uploadLoopStatus( + insulinOnBoard: insulinOnBoard, + carbsOnBoard: carbsOnBoard, + predictedGlucose: predictedGlucose, + recommendedTempBasal: recommendedTempBasal, + recommendedBolus: recommendedBolus, + lastTempBasal: lastTempBasal, + loopError: loopError + ) + } } } @@ -106,10 +126,9 @@ final class NightscoutDataManager { } let loopEnacted: LoopEnacted? - if let tempBasal = lastTempBasal, tempBasal.unit == .unitsPerHour && - lastTempBasalUploaded?.startDate != tempBasal.startDate { + if let tempBasal = lastTempBasal, lastTempBasalUploaded?.startDate != tempBasal.startDate { let duration = tempBasal.endDate.timeIntervalSince(tempBasal.startDate) - loopEnacted = LoopEnacted(rate: tempBasal.value, duration: duration, timestamp: tempBasal.startDate, received: + loopEnacted = LoopEnacted(rate: tempBasal.unitsPerHour, duration: duration, timestamp: tempBasal.startDate, received: true) lastTempBasalUploaded = tempBasal } else { diff --git a/Loop/Managers/RemoteDataManager.swift b/Loop/Managers/RemoteDataManager.swift index d30c539656..9c1eb6deb2 100644 --- a/Loop/Managers/RemoteDataManager.swift +++ b/Loop/Managers/RemoteDataManager.swift @@ -25,17 +25,17 @@ final class RemoteDataManager { var shareService: ShareService { didSet { - try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password) + try! keychain.setDexcomShareUsername(shareService.username, password: shareService.password, url: shareService.url) } } private let keychain = KeychainManager() init() { - if let (username, password) = keychain.getDexcomShareCredentials() { - shareService = ShareService(username: username, password: password) + if let (username, password, url) = keychain.getDexcomShareCredentials() { + shareService = ShareService(username: username, password: password, url: url) } else { - shareService = ShareService(username: nil, password: nil) + shareService = ShareService(username: nil, password: nil, url: nil) } if let (siteURL, APISecret) = keychain.getNightscoutCredentials() { diff --git a/Loop/Managers/StatusChartsManager+LoopKit.swift b/Loop/Managers/StatusChartsManager+LoopKit.swift index 601596acb2..9430fa1863 100644 --- a/Loop/Managers/StatusChartsManager+LoopKit.swift +++ b/Loop/Managers/StatusChartsManager+LoopKit.swift @@ -84,17 +84,17 @@ extension StatusChartsManager { var allDosePoints = [ChartPoint]() for entry in doseEntries { - switch entry.unit { - case .unitsPerHour: + switch entry.endDate.timeIntervalSince(entry.startDate) { + case let time where time > 0: // TODO: Display the DateInterval let startX = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) let endX = ChartAxisValueDate(date: entry.endDate, formatter: dateFormatter) let zero = ChartAxisValueInt(0) - let value = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U/hour", formatter: doseFormatter) + let value = ChartAxisValueDoubleLog(actualDouble: entry.unitsPerHour, unitString: "U/hour", formatter: doseFormatter) let valuePoints: [ChartPoint] - if entry.value != 0 { + if entry.unitsPerHour != 0 { valuePoints = [ ChartPoint(x: startX, y: value), ChartPoint(x: endX, y: value) @@ -110,13 +110,15 @@ extension StatusChartsManager { ] allDosePoints += valuePoints - case .units: - let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) - let y = ChartAxisValueDoubleLog(actualDouble: entry.value, unitString: "U", formatter: doseFormatter) - - let point = ChartPoint(x: x, y: y) - bolusDosePoints.append(point) - allDosePoints.append(point) + default: + if entry.type == .bolus && entry.units > 0 { + let x = ChartAxisValueDate(date: entry.startDate, formatter: dateFormatter) + let y = ChartAxisValueDoubleLog(actualDouble: entry.units, unitString: "U", formatter: doseFormatter) + + let point = ChartPoint(x: x, y: y) + bolusDosePoints.append(point) + allDosePoints.append(point) + } } } @@ -143,4 +145,82 @@ extension StatusChartsManager { ) } } + + /// Convert an array of GlucoseEffects (as glucose values) into glucose effect velocity (glucose/min) for charting + /// + /// - Parameter effects: A timeline of glucose values representing glucose change + func setCarbEffects(_ effects: [GlucoseEffect]) { + let dateFormatter = self.dateFormatter + let decimalFormatter = self.doseFormatter + let unit = glucoseUnit.unitDivided(by: .minute()) + let unitString = unit.unitString + + var lastDate = effects.first?.endDate + var lastValue = effects.first?.quantity.doubleValue(for: glucoseUnit) + let minuteInterval = 5.0 + + var carbEffectPoints = [ChartPoint]() + + let zero = ChartAxisValueInt(0) + + for effect in effects.dropFirst() { + let value = effect.quantity.doubleValue(for: glucoseUnit) + let valuePerMinute = (value - lastValue!) / minuteInterval + lastValue = value + + let startX = ChartAxisValueDate(date: lastDate!, formatter: dateFormatter) + let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter) + lastDate = effect.endDate + + let valueY = ChartAxisValueDoubleUnit(valuePerMinute, unitString: unitString, formatter: decimalFormatter) + + carbEffectPoints += [ + ChartPoint(x: startX, y: zero), + ChartPoint(x: startX, y: valueY), + ChartPoint(x: endX, y: valueY), + ChartPoint(x: endX, y: zero) + ] + } + + self.carbEffectPoints = carbEffectPoints + } + + /// Charts glucose effect velocity + /// + /// - Parameter effects: A timeline of glucose velocity values + func setInsulinCounteractionEffects(_ effects: [GlucoseEffectVelocity]) { + let dateFormatter = self.dateFormatter + let decimalFormatter = self.doseFormatter + let unit = glucoseUnit.unitDivided(by: .minute()) + let unitString = String(format: NSLocalizedString("%1$@/min", comment: "Format string describing glucose units per minute (1: glucose unit string)"), glucoseUnit.glucoseUnitDisplayString) + + var insulinCounteractionEffectPoints: [ChartPoint] = [] + var allCarbEffectPoints: [ChartPoint] = [] + + let zero = ChartAxisValueInt(0) + + for effect in effects { + let startX = ChartAxisValueDate(date: effect.startDate, formatter: dateFormatter) + let endX = ChartAxisValueDate(date: effect.endDate, formatter: dateFormatter) + let value = ChartAxisValueDoubleUnit(effect.quantity.doubleValue(for: unit), unitString: unitString, formatter: decimalFormatter) + + guard value.scalar != 0 else { + continue + } + + let valuePoint = ChartPoint(x: endX, y: value) + + insulinCounteractionEffectPoints += [ + ChartPoint(x: startX, y: zero), + ChartPoint(x: startX, y: value), + valuePoint, + ChartPoint(x: endX, y: zero) + ] + + allCarbEffectPoints.append(valuePoint) + } + + self.insulinCounteractionEffectPoints = insulinCounteractionEffectPoints + self.allCarbEffectPoints = allCarbEffectPoints + } } diff --git a/Loop/Managers/StatusExtensionDataManager.swift b/Loop/Managers/StatusExtensionDataManager.swift index 137c4be6fb..1ec54a2c95 100644 --- a/Loop/Managers/StatusExtensionDataManager.swift +++ b/Loop/Managers/StatusExtensionDataManager.swift @@ -55,8 +55,9 @@ final class StatusExtensionDataManager { context.netBasal = NetBasalContext( rate: 2.1, percentage: 0.6, - startDate: - Date(timeIntervalSinceNow: -250) + start: + Date(timeIntervalSinceNow: -250), + end: Date(timeIntervalSinceNow: .minutes(30)) ) context.predictedGlucose = PredictedGlucoseContext( values: (1...36).map { 89.123 + Double($0 * 5) }, // 3 hours of linear data @@ -120,7 +121,7 @@ final class StatusExtensionDataManager { scheduledBasal: scheduledBasal ) - context.netBasal = NetBasalContext(rate: netBasal.rate, percentage: netBasal.percent, startDate: netBasal.startDate) + context.netBasal = NetBasalContext(rate: netBasal.rate, percentage: netBasal.percent, start: netBasal.start, end: netBasal.end) } if let reservoir = manager.doseStore.lastReservoirValue, diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 1834842d8a..eabc1b780d 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -130,7 +130,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { deviceDataManager.loopManager.addCarbEntryAndRecommendBolus(newEntry) { (result) in switch result { case .success(let recommendation): - AnalyticsManager.sharedManager.didAddCarbsFromWatch(carbEntry.value) + AnalyticsManager.shared.didAddCarbsFromWatch(carbEntry.value) completionHandler?(recommendation?.amount) case .failure(let error): self.deviceDataManager.logger.addError(error, fromSource: error is CarbStore.CarbStoreError ? "CarbStore" : "Bolus") @@ -154,7 +154,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { if let bolus = SetBolusUserInfo(rawValue: message as SetBolusUserInfo.RawValue) { self.deviceDataManager.enactBolus(units: bolus.value, at: bolus.startDate) { (error) in if error == nil { - AnalyticsManager.sharedManager.didSetBolusFromWatch(bolus.value) + AnalyticsManager.shared.didSetBolusFromWatch(bolus.value) } replyHandler([:]) diff --git a/Loop/Models/GlucoseEffectVelocity.swift b/Loop/Models/GlucoseEffectVelocity.swift new file mode 100644 index 0000000000..35b7ba4b0f --- /dev/null +++ b/Loop/Models/GlucoseEffectVelocity.swift @@ -0,0 +1,38 @@ +// +// GlucoseEffectVelocity.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit + + +extension GlucoseEffectVelocity: RawRepresentable { + public typealias RawValue = [String: Any] + + static let unit = HKUnit.milligramsPerDeciliter().unitDivided(by: .minute()) + + public init?(rawValue: RawValue) { + guard let startDate = rawValue["startDate"] as? Date, + let doubleValue = rawValue["doubleValue"] as? Double + else { + return nil + } + + self.init( + startDate: startDate, + endDate: rawValue["endDate"] as? Date ?? startDate, + quantity: HKQuantity(unit: type(of: self).unit, doubleValue: doubleValue) + ) + } + + public var rawValue: RawValue { + return [ + "startDate": startDate, + "endDate": endDate, + "doubleValue": quantity.doubleValue(for: type(of: self).unit) + ] + } +} diff --git a/Loop/Models/LoopSettings.swift b/Loop/Models/LoopSettings.swift index be8490fe51..002942d01c 100644 --- a/Loop/Models/LoopSettings.swift +++ b/Loop/Models/LoopSettings.swift @@ -11,6 +11,8 @@ import LoopKit struct LoopSettings { var dosingEnabled = false + let dynamicCarbAbsorptionEnabled = true + var glucoseTargetRangeSchedule: GlucoseRangeSchedule? var maximumBasalRatePerHour: Double? diff --git a/Loop/Models/NetBasal.swift b/Loop/Models/NetBasal.swift index 0420ac2170..eb68ecbd91 100644 --- a/Loop/Models/NetBasal.swift +++ b/Loop/Models/NetBasal.swift @@ -13,12 +13,14 @@ import LoopKit struct NetBasal { let rate: Double let percent: Double - let startDate: Date + let start: Date + let end: Date init(lastTempBasal: DoseEntry?, maxBasal: Double?, scheduledBasal: AbsoluteScheduleValue) { if let lastTempBasal = lastTempBasal, lastTempBasal.endDate > Date(), let maxBasal = maxBasal { - rate = lastTempBasal.value - scheduledBasal.value - startDate = lastTempBasal.startDate + rate = lastTempBasal.unitsPerHour - scheduledBasal.value + start = lastTempBasal.startDate + end = lastTempBasal.endDate if rate < 0 { percent = rate / scheduledBasal.value @@ -30,9 +32,11 @@ struct NetBasal { percent = 0 if let lastTempBasal = lastTempBasal, lastTempBasal.endDate > scheduledBasal.startDate { - startDate = lastTempBasal.endDate + start = lastTempBasal.endDate + end = scheduledBasal.endDate } else { - startDate = scheduledBasal.startDate + start = scheduledBasal.startDate + end = scheduledBasal.endDate } } } diff --git a/Loop/Models/ServiceAuthentication/AmplitudeService.swift b/Loop/Models/ServiceAuthentication/AmplitudeService.swift index 63074738c4..f9a2e496d4 100644 --- a/Loop/Models/ServiceAuthentication/AmplitudeService.swift +++ b/Loop/Models/ServiceAuthentication/AmplitudeService.swift @@ -10,7 +10,7 @@ import Foundation import Amplitude -struct AmplitudeService: ServiceAuthentication { +class AmplitudeService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Amplitude", comment: "The title of the Amplitude service") @@ -37,7 +37,7 @@ struct AmplitudeService: ServiceAuthentication { var isAuthorized: Bool = true - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let APIKey = APIKey else { isAuthorized = false completion(false, nil) @@ -52,9 +52,23 @@ struct AmplitudeService: ServiceAuthentication { completion(true, nil) } - mutating func reset() { - credentials[0].value = nil + func reset() { + credentials[0].reset() isAuthorized = false client = nil } } + + +private let AmplitudeAPIKeyService = "AmplitudeAPIKey" + + +extension KeychainManager { + func setAmplitudeAPIKey(_ key: String?) throws { + try replaceGenericPassword(key, forService: AmplitudeAPIKeyService) + } + + func getAmplitudeAPIKey() -> String? { + return try? getGenericPasswordForService(AmplitudeAPIKeyService) + } +} diff --git a/Loop/Models/ServiceAuthentication/LogglyService.swift b/Loop/Models/ServiceAuthentication/LogglyService.swift new file mode 100644 index 0000000000..f233bbca53 --- /dev/null +++ b/Loop/Models/ServiceAuthentication/LogglyService.swift @@ -0,0 +1,141 @@ +// +// LogglyService.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import Foundation + + +class LogglyService: ServiceAuthentication { + var credentials: [ServiceCredential] + + let title: String = NSLocalizedString("Loggly", comment: "The title of the loggly service") + + init(customerToken: String?) { + credentials = [ + ServiceCredential( + title: NSLocalizedString("Customer Token", comment: "The title of the Loggly customer token credential"), + placeholder: nil, + isSecret: false, + keyboardType: .asciiCapable, + value: customerToken + ) + ] + + verify { _, _ in } + } + + var client: LogglyClient? + + var isAuthorized: Bool = true + + var customerToken: String? { + return credentials[0].value + } + + func verify(_ completion: @escaping (Bool, Error?) -> Void) { + guard let customerToken = customerToken else { + isAuthorized = false + completion(false, nil) + return + } + + isAuthorized = true + client = LogglyClient(customerToken: customerToken) + completion(true, nil) + } + + func reset() { + credentials[0].reset() + isAuthorized = false + client = nil + } +} + + +private let LogglyURLSessionConfiguration = "LogglyURLSessionConfiguration" +private let LogglyCustomerTokenService = "LogglyCustomerToken" + + +extension KeychainManager { + func setLogglyCustomerToken(_ token: String?) throws { + try replaceGenericPassword(token, forService: LogglyCustomerTokenService) + } + + func getLogglyCustomerToken() -> String? { + return try? getGenericPasswordForService(LogglyCustomerTokenService) + } +} + + +enum LogglyAPIEndpoint: String { + case inputs + + private var base: String { + return "https://logs-01.loggly.com/\(rawValue)/" + } + + func url(token: String, tags: [String]) -> URL { + let tags = tags.count > 0 ? tags : ["http"] + return URL(string: "\(base)\(token)/tag/\(tags.joined(separator: ","))/")! + } +} + + +extension URLSession { + fileprivate static func logglySession() -> URLSession { + let configuration = URLSessionConfiguration.ephemeral + + configuration.isDiscretionary = true + configuration.sessionSendsLaunchEvents = false + configuration.networkServiceType = .background + + return URLSession(configuration: configuration) + } + + private func inputTask(body: Data, contentType: String, token: String, tags: [String]) -> URLSessionUploadTask? { + let url = LogglyAPIEndpoint.inputs.url(token: token, tags: tags) + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue(contentType, forHTTPHeaderField: "content-type") + + return uploadTask(with: request, from: body) + } + + fileprivate func inputTask(body: String, token: String, tags: [String]) -> URLSessionUploadTask? { + guard let data = body.data(using: .utf8) else { + return nil + } + + return inputTask(body: data, contentType: "text/plain", token: token, tags: tags) + } + + fileprivate func inputTask(body: [String: Any], token: String, tags: [String]) -> URLSessionUploadTask? { + do { + let data = try JSONSerialization.data(withJSONObject: body, options: []) + return inputTask(body: data, contentType: "application/json", token: token, tags: tags) + } catch { + return nil + } + } +} + + +class LogglyClient { + let customerToken: String + let session = URLSession.logglySession() + + init(customerToken: String) { + self.customerToken = customerToken + } + + func send(_ body: String, tags: [String]) { + session.inputTask(body: body, token: customerToken, tags: tags)?.resume() + } + + func send(_ body: [String: Any], tags: [String]) { + session.inputTask(body: body, token: customerToken, tags: tags)?.resume() + } +} diff --git a/Loop/Models/ServiceAuthentication/MLabService.swift b/Loop/Models/ServiceAuthentication/MLabService.swift index 174f205b1e..7465669283 100644 --- a/Loop/Models/ServiceAuthentication/MLabService.swift +++ b/Loop/Models/ServiceAuthentication/MLabService.swift @@ -12,7 +12,7 @@ import Foundation private let mLabAPIHost = URL(string: "https://api.mongolab.com/api/1/databases")! -struct MLabService: ServiceAuthentication { +class MLabService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("mLab", comment: "The title of the mLab service") @@ -50,7 +50,7 @@ struct MLabService: ServiceAuthentication { var isAuthorized: Bool = false - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let APIURL = APIURLForCollection("") else { completion(false, nil) return @@ -66,9 +66,9 @@ struct MLabService: ServiceAuthentication { }).resume() } - mutating func reset() { - credentials[0].value = nil - credentials[1].value = nil + func reset() { + credentials[0].reset() + credentials[1].reset() isAuthorized = false } diff --git a/Loop/Models/ServiceAuthentication/NightscoutService.swift b/Loop/Models/ServiceAuthentication/NightscoutService.swift index 3c796e480b..5b6e98186e 100644 --- a/Loop/Models/ServiceAuthentication/NightscoutService.swift +++ b/Loop/Models/ServiceAuthentication/NightscoutService.swift @@ -11,7 +11,7 @@ import NightscoutUploadKit // Encapsulates a Nightscout site and its authentication -struct NightscoutService: ServiceAuthentication { +class NightscoutService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Nightscout", comment: "The title of the Nightscout service") @@ -40,8 +40,9 @@ struct NightscoutService: ServiceAuthentication { // The uploader instance, if credentials are present private(set) var uploader: NightscoutUploader? { didSet { + let logger = DiagnosticLogger.shared?.forCategory("NightscoutService") uploader?.errorHandler = { (error: Error, context: String) -> Void in - print("Error \(error), while \(context)") + logger?.error("Error \(error), while \(context)") } } } @@ -60,7 +61,7 @@ struct NightscoutService: ServiceAuthentication { var isAuthorized: Bool = true - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { guard let siteURL = siteURL, let APISecret = APISecret else { isAuthorized = false completion(false, nil) @@ -74,9 +75,9 @@ struct NightscoutService: ServiceAuthentication { self.uploader = uploader } - mutating func reset() { - credentials[0].value = nil - credentials[1].value = nil + func reset() { + credentials[0].reset() + credentials[1].reset() isAuthorized = false uploader = nil } diff --git a/Loop/Models/ServiceAuthentication/ServiceCredential.swift b/Loop/Models/ServiceAuthentication/ServiceCredential.swift index 2556617541..36cc75e757 100644 --- a/Loop/Models/ServiceAuthentication/ServiceCredential.swift +++ b/Loop/Models/ServiceAuthentication/ServiceCredential.swift @@ -9,20 +9,36 @@ import UIKit -// Represents a credential for a service, including its text input traits +/// Represents a credential for a service, including its text input traits struct ServiceCredential { - // The localized title of the credential (e.g. "Username") + /// The localized title of the credential (e.g. "Username") let title: String - // The localized placeholder text to assist text input + /// The localized placeholder text to assist text input let placeholder: String? - // Whether the credential is considered secret. Correponds to the `secureTextEntry` trait. + /// Whether the credential is considered secret. Correponds to the `secureTextEntry` trait. let isSecret: Bool - // The type of keyboard to use to enter the credential + /// The type of keyboard to use to enter the credential let keyboardType: UIKeyboardType - // The credential value + /// The credential value var value: String? + + /// A set of valid values for presenting a selection. The first item is the default. + var options: [(title: String, value: String)]? + + init(title: String, placeholder: String? = nil, isSecret: Bool, keyboardType: UIKeyboardType = .asciiCapable, value: String?, options: [(title: String, value: String)]? = nil) { + self.title = title + self.placeholder = placeholder + self.isSecret = isSecret + self.keyboardType = keyboardType + self.value = value ?? options?.first?.value + self.options = options + } + + mutating func reset() { + self.value = options?.first?.value + } } diff --git a/Loop/Models/ServiceAuthentication/ShareService.swift b/Loop/Models/ServiceAuthentication/ShareService.swift index 93b40f9370..2d5167533f 100644 --- a/Loop/Models/ServiceAuthentication/ShareService.swift +++ b/Loop/Models/ServiceAuthentication/ShareService.swift @@ -11,32 +11,59 @@ import ShareClient // Encapsulates the Dexcom Share client service and its authentication -struct ShareService: ServiceAuthentication { +class ShareService: ServiceAuthentication { var credentials: [ServiceCredential] let title: String = NSLocalizedString("Dexcom Share", comment: "The title of the Dexcom Share service") - init(username: String?, password: String?) { + init(username: String?, password: String?, url: URL?) { credentials = [ ServiceCredential( title: NSLocalizedString("Username", comment: "The title of the Dexcom share username credential"), - placeholder: nil, isSecret: false, keyboardType: .asciiCapable, value: username ), ServiceCredential( title: NSLocalizedString("Password", comment: "The title of the Dexcom share password credential"), - placeholder: nil, isSecret: true, keyboardType: .asciiCapable, value: password + ), + ServiceCredential( + title: NSLocalizedString("Server", comment: "The title of the Dexcom share server URL credential"), + isSecret: false, + value: url?.absoluteString, + options: [ + (title: NSLocalizedString("US", comment: "U.S. share server option title"), + value: KnownShareServers.US.rawValue), + (title: NSLocalizedString("Outside US", comment: "Outside US share server option title"), + value: KnownShareServers.NON_US.rawValue) + + ] ) ] - if let username = username, let password = password { + /* + To enable Loop to use a custom share server, change the value of customServer + and remove the comment markers on line 55 and 62. + + You can find installation instructions for one such custom share server at + https://github.com/dabear/NightscoutShareServer + */ + + /* + let customServer = "https://REPLACEME" + let customServerTitle = "Custom" + + credentials[2].options?.append( + (title: NSLocalizedString(customServerTitle, comment: "Custom share server option title"), + value: customServer)) + */ + + if let username = username, let password = password, let url = url { isAuthorized = true - client = ShareClient(username: username, password: password) + client = ShareClient(username: username, password: password, shareServer: url.absoluteString) } } @@ -51,25 +78,76 @@ struct ShareService: ServiceAuthentication { return credentials[1].value } + var url: URL? { + guard let urlString = credentials[2].value else { + return nil + } + + return URL(string: urlString) + } + var isAuthorized: Bool = false - mutating func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { - guard let username = username, let password = password else { + func verify(_ completion: @escaping (_ success: Bool, _ error: Error?) -> Void) { + guard let username = username, let password = password, let url = url else { completion(false, nil) return } - let client = ShareClient(username: username, password: password) + let client = ShareClient(username: username, password: password, shareServer: url.absoluteString) client.fetchLast(1) { (error, _) in completion(true, error) + } self.client = client } - mutating func reset() { - credentials[0].value = nil - credentials[1].value = nil + func reset() { + credentials[0].reset() + credentials[1].reset() + credentials[2].reset() isAuthorized = false client = nil } } + + +private let DexcomShareURL = URL(string: KnownShareServers.US.rawValue)! +private let DexcomShareServiceLabel = "DexcomShare1" + + +extension KeychainManager { + func setDexcomShareUsername(_ username: String?, password: String?, url: URL?) throws { + let credentials: InternetCredentials? + + if let username = username, let password = password, let url = url { + credentials = InternetCredentials(username: username, password: password, url: url) + } else { + credentials = nil + } + + // Replace the legacy URL-keyed credentials + try replaceInternetCredentials(nil, forURL: DexcomShareURL) + + try replaceInternetCredentials(credentials, forLabel: DexcomShareServiceLabel) + } + + func getDexcomShareCredentials() -> (username: String, password: String, url: URL)? { + do { // Silence all errors and return nil + do { + let credentials = try getInternetCredentials(label: DexcomShareServiceLabel) + + return (username: credentials.username, password: credentials.password, url: credentials.url) + } catch KeychainManagerError.copy { + // Fetch and replace the legacy URL-keyed credentials + let credentials = try getInternetCredentials(url: DexcomShareURL) + + try setDexcomShareUsername(credentials.username, password: credentials.password, url: credentials.url) + + return (username: credentials.username, password: credentials.password, url: credentials.url) + } + } catch { + return nil + } + } +} diff --git a/Loop/Models/StatusExtensionContext+LoopKit.swift b/Loop/Models/StatusExtensionContext+LoopKit.swift new file mode 100644 index 0000000000..d1601c1cdb --- /dev/null +++ b/Loop/Models/StatusExtensionContext+LoopKit.swift @@ -0,0 +1,24 @@ +// +// StatusExtensionContext+LoopKit.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import InsulinKit + +extension NetBasalContext { + var tempBasal: DoseEntry? { + guard rate != 0 else { + return nil + } + + return DoseEntry( + type: .tempBasal, + startDate: start, + endDate: end, + value: rate, + unit: .unitsPerHour + ) + } +} diff --git a/Loop/View Controllers/AuthenticationViewController.swift b/Loop/View Controllers/AuthenticationViewController.swift index adc690fd72..3ebbc04508 100644 --- a/Loop/View Controllers/AuthenticationViewController.swift +++ b/Loop/View Controllers/AuthenticationViewController.swift @@ -59,6 +59,15 @@ final class AuthenticationViewController: UITableViewC } } + var credentials: [ServiceCredential] { + switch state { + case .authorized: + return authentication.credentials.filter({ !$0.isSecret }) + default: + return authentication.credentials + } + } + init(authentication: T) { self.authentication = authentication @@ -89,12 +98,7 @@ final class AuthenticationViewController: UITableViewC override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Section(rawValue: section)! { case .credentials: - switch state { - case .authorized: - return authentication.credentials.filter({ !$0.isSecret }).count - default: - return authentication.credentials.count - } + return credentials.count case .button: return 1 } @@ -126,16 +130,23 @@ final class AuthenticationViewController: UITableViewC case .credentials: let cell = tableView.dequeueReusableCell(withIdentifier: AuthenticationTableViewCell.className, for: indexPath) as! AuthenticationTableViewCell - let credential = authentication.credentials[indexPath.row] + let credentials = self.credentials + let credential = credentials[indexPath.row] cell.titleLabel.text = credential.title - cell.textField.tag = indexPath.row cell.textField.keyboardType = credential.keyboardType cell.textField.isSecureTextEntry = credential.isSecret - cell.textField.returnKeyType = (indexPath.row < authentication.credentials.count - 1) ? .next : .done + cell.textField.returnKeyType = (indexPath.row < credentials.count - 1) ? .next : .done cell.textField.text = credential.value cell.textField.placeholder = credential.placeholder ?? NSLocalizedString("Required", comment: "The default placeholder string for a credential") + if let options = credential.options { + let picker = CredentialOptionPicker(options: options) + picker.value = credential.value + + cell.credentialOptionPicker = picker + } + cell.textField.delegate = self switch state { @@ -149,7 +160,7 @@ final class AuthenticationViewController: UITableViewC } } - private func validate() { + fileprivate func validate() { state = .verifying } @@ -173,7 +184,16 @@ final class AuthenticationViewController: UITableViewC // MARK: - UITextFieldDelegate func textFieldDidEndEditing(_ textField: UITextField) { - authentication.credentials[textField.tag].value = textField.text + let point = tableView.convert(textField.frame.origin, from: textField.superview) + + guard case .unauthorized = state, + let indexPath = tableView.indexPathForRow(at: point), + let cell = tableView.cellForRow(at: IndexPath(row: indexPath.row, section: indexPath.section)) as? AuthenticationTableViewCell + else { + return + } + + authentication.credentials[indexPath.row].value = cell.value } func textFieldShouldReturn(_ textField: UITextField) -> Bool { @@ -191,6 +211,10 @@ final class AuthenticationViewController: UITableViewC return true } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return textField.inputView == nil + } } diff --git a/Loop/View Controllers/BolusViewController+LoopDataManager.swift b/Loop/View Controllers/BolusViewController+LoopDataManager.swift new file mode 100644 index 0000000000..280d333530 --- /dev/null +++ b/Loop/View Controllers/BolusViewController+LoopDataManager.swift @@ -0,0 +1,49 @@ +// +// BolusViewController+LoopDataManager.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit + + +extension BolusViewController { + func configureWithLoopManager(_ manager: LoopDataManager, recommendation: BolusRecommendation?, glucoseUnit: HKUnit) { + manager.getLoopState { (manager, state) in + let maximumBolus = manager.settings.maximumBolus + + let activeCarbohydrates = state.carbsOnBoard?.quantity.doubleValue(for: .gram()) + let bolusRecommendation: BolusRecommendation? + + if let recommendation = recommendation { + bolusRecommendation = recommendation + } else { + bolusRecommendation = try? state.recommendBolus() + } + + manager.doseStore.insulinOnBoard(at: Date()) { (result) in + let activeInsulin: Double? + + switch result { + case .success(let value): + activeInsulin = value.value + case .failure: + activeInsulin = nil + } + + DispatchQueue.main.async { + if let maxBolus = maximumBolus { + self.maxBolus = maxBolus + } + + self.glucoseUnit = glucoseUnit + self.activeInsulin = activeInsulin + self.activeCarbohydrates = activeCarbohydrates + self.bolusRecommendation = bolusRecommendation + } + } + } + } +} diff --git a/Loop/View Controllers/BolusViewController.swift b/Loop/View Controllers/BolusViewController.swift index 53b9045857..2fb9c5b208 100644 --- a/Loop/View Controllers/BolusViewController.swift +++ b/Loop/View Controllers/BolusViewController.swift @@ -39,7 +39,7 @@ final class BolusViewController: UITableViewController, IdentifiableClass, UITex bolusAmountTextField.becomeFirstResponder() - AnalyticsManager.sharedManager.didDisplayBolusScreen() + AnalyticsManager.shared.didDisplayBolusScreen() } func generateActiveInsulinDescription(activeInsulin: Double?, pendingInsulin: Double?) -> String diff --git a/Loop/View Controllers/CarbAbsorptionViewController.swift b/Loop/View Controllers/CarbAbsorptionViewController.swift new file mode 100644 index 0000000000..0bbc487b5a --- /dev/null +++ b/Loop/View Controllers/CarbAbsorptionViewController.swift @@ -0,0 +1,551 @@ +// +// CarbAbsorptionViewController.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit +import HealthKit + +import CarbKit +import LoopKit +import LoopUI + + +private extension RefreshContext { + static let all: RefreshContext = [.glucose, .carbs, .targets, .status] +} + + +final class CarbAbsorptionViewController: ChartsTableViewController, IdentifiableClass { + + override func viewDidLoad() { + super.viewDidLoad() + + charts.glucoseDisplayRange = ( + min: HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: 100), + max: HKQuantity(unit: HKUnit.milligramsPerDeciliter(), doubleValue: 175) + ) + + let notificationCenter = NotificationCenter.default + + notificationObservers += [ + notificationCenter.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { [unowned self] note in + let context = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as! LoopDataManager.LoopUpdateContext.RawValue + DispatchQueue.main.async { + switch LoopDataManager.LoopUpdateContext(rawValue: context) { + case .preferences?: + self.refreshContext.update(with: .targets) + case .carbs?: + self.refreshContext.update(with: [.carbs, .glucose]) + case .glucose?: + self.refreshContext.update(with: .glucose) + default: + break + } + + self.refreshContext.update(with: .status) + self.reloadData(animated: true) + } + } + ] + + if let gestureRecognizer = charts.gestureRecognizer { + tableView.addGestureRecognizer(gestureRecognizer) + } + + navigationItem.rightBarButtonItems?.append(editButtonItem) + + reloadData(animated: false) + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + + if !visible { + refreshContext = .all + } + } + + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + refreshContext.update(with: .status) + + super.viewWillTransition(to: size, with: coordinator) + } + + // MARK: - State + + private var refreshContext: RefreshContext = .all + + private var chartStartDate: Date { + get { + return charts.startDate + } + set { + if newValue != chartStartDate { + refreshContext = .all + } + + charts.startDate = newValue + } + } + + private var carbStatuses: [CarbStatus] = [] + + private var carbsOnBoard: CarbValue? + + private var carbTotal: CarbValue? + + // MARK: - Data loading + + override func reloadData(animated: Bool, to size: CGSize? = nil) { + guard active && !self.refreshContext.isEmpty else { return } + + // How far back should we show data? Use the screen size as a guide. + let minimumSegmentWidth: CGFloat = 75 + let availableWidth = (size ?? self.tableView.bounds.size).width - self.charts.fixedHorizontalMargin + let totalHours = floor(Double(availableWidth / minimumSegmentWidth)) + + var components = DateComponents() + components.minute = 0 + let date = Date(timeIntervalSinceNow: -TimeInterval(hours: max(1, totalHours))) + chartStartDate = Calendar.current.nextDate(after: date, matching: components, matchingPolicy: .strict, direction: .backward) ?? date + + let midnight = Calendar.current.startOfDay(for: Date()) + let listStart = min(midnight, chartStartDate) + + let reloadGroup = DispatchGroup() + let shouldUpdateGlucose = self.refreshContext.remove(.glucose) != nil + let shouldUpdateCarbs = self.refreshContext.remove(.carbs) != nil + + var refreshContext = self.refreshContext + var carbEffects: [GlucoseEffect]? + var carbStatuses: [CarbStatus]? + var carbsOnBoard: CarbValue? + var carbTotal: CarbValue? + + reloadGroup.enter() + deviceManager.loopManager.glucoseStore.preferredUnit { (unit, error) in + if let unit = unit { + self.charts.glucoseUnit = unit + } + + _ = refreshContext.remove(.status) + reloadGroup.enter() + self.deviceManager.loopManager.getLoopState { (manager, state) in + if shouldUpdateGlucose || shouldUpdateCarbs { + let insulinCounteractionEffects = state.insulinCounteractionEffects + self.charts.setInsulinCounteractionEffects(state.insulinCounteractionEffects.filterDateRange(self.chartStartDate, nil)) + + reloadGroup.enter() + manager.carbStore.getCarbStatus(start: listStart, effectVelocities: manager.settings.dynamicCarbAbsorptionEnabled ? insulinCounteractionEffects : nil) { (result) in + switch result { + case .success(let status): + carbStatuses = status + case .failure(let error): + self.deviceManager.logger.addError(error, fromSource: "CarbStore") + refreshContext.update(with: .carbs) + } + + reloadGroup.leave() + } + + reloadGroup.enter() + manager.carbStore.getGlucoseEffects(start: self.chartStartDate, effectVelocities: manager.settings.dynamicCarbAbsorptionEnabled ? insulinCounteractionEffects : nil) { (result) in + switch result { + case .success(let effects): + carbEffects = effects + case .failure(let error): + carbEffects = [] + self.deviceManager.logger.addError(error, fromSource: "CarbStore") + refreshContext.update(with: .carbs) + } + reloadGroup.leave() + } + + carbsOnBoard = state.carbsOnBoard + } + + if refreshContext.remove(.targets) != nil { + if let schedule = manager.settings.glucoseTargetRangeSchedule { + self.charts.targetPointsCalculator = GlucoseRangeScheduleCalculator(schedule) + } else { + self.charts.targetPointsCalculator = nil + } + } + + reloadGroup.leave() + } + + if shouldUpdateCarbs { + reloadGroup.enter() + self.deviceManager.loopManager.carbStore.getTotalCarbs(since: midnight) { (result) in + switch result { + case .success(let total): + carbTotal = total + case .failure(let error): + self.deviceManager.logger.addError(error, fromSource: "CarbStore") + refreshContext.update(with: .carbs) + } + + reloadGroup.leave() + } + } + + reloadGroup.leave() + } + + reloadGroup.notify(queue: .main) { + self.refreshContext = refreshContext + if let carbEffects = carbEffects { + self.charts.setCarbEffects(carbEffects) + } + + self.charts.prerender() + + for case let cell as ChartTableViewCell in self.tableView.visibleCells { + cell.reloadChart() + } + + if shouldUpdateCarbs || shouldUpdateGlucose { + // Change to descending order for display + self.carbStatuses = carbStatuses?.reversed() ?? [] + + if shouldUpdateCarbs { + self.carbTotal = carbTotal + } + + self.carbsOnBoard = carbsOnBoard + + self.tableView.reloadSections(IndexSet(integer: Section.entries.rawValue), with: .fade) + } + + if let cell = self.tableView.cellForRow(at: IndexPath(row: 0, section: Section.totals.rawValue)) as? HeaderValuesTableViewCell { + self.updateCell(cell) + } + } + } + + // MARK: - UITableViewDataSource + + private enum Section: Int { + case charts + case totals + case entries + + static let count = 3 + } + + private enum ChartRow: Int { + case carbEffect + + static let count = 1 + } + + private lazy var carbFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .none + return formatter + }() + + private lazy var absorptionFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.collapsesLargestUnit = true + formatter.unitsStyle = .abbreviated + formatter.allowsFractionalUnits = true + formatter.allowedUnits = [.hour, .minute] + return formatter + }() + + private lazy var timeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + override func numberOfSections(in tableView: UITableView) -> Int { + return Section.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section(rawValue: section)! { + case .charts: + return ChartRow.count + case .totals: + return 1 + case .entries: + return carbStatuses.count + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section(rawValue: indexPath.section)! { + case .charts: + let cell = tableView.dequeueReusableCell(withIdentifier: ChartTableViewCell.className, for: indexPath) as! ChartTableViewCell + + switch ChartRow(rawValue: indexPath.row)! { + case .carbEffect: + cell.chartContentView.chartGenerator = { [unowned self] (frame) in + return self.charts.carbEffectChartWithFrame(frame)?.view + } + } + + let alpha: CGFloat = charts.gestureRecognizer?.state == .possible ? 1 : 0 + cell.titleLabel?.alpha = alpha + cell.subtitleLabel?.alpha = alpha + + cell.subtitleLabel?.textColor = UIColor.secondaryLabelColor + + return cell + case .totals: + let cell = tableView.dequeueReusableCell(withIdentifier: HeaderValuesTableViewCell.className, for: indexPath) as! HeaderValuesTableViewCell + updateCell(cell) + + return cell + case .entries: + let unit = HKUnit.gram() + let cell = tableView.dequeueReusableCell(withIdentifier: CarbEntryTableViewCell.className, for: indexPath) as! CarbEntryTableViewCell + + // Entry value + let status = carbStatuses[indexPath.row] + let carbText = carbFormatter.string(from: status.entry.quantity.doubleValue(for: unit), unit: unit.unitString) + + if let carbText = carbText, let foodType = status.entry.foodType { + cell.valueLabel?.text = String( + format: NSLocalizedString("%1$@: %2$@", comment: "Formats (1: carb value) and (2: food type)"), + carbText, foodType + ) + } else { + cell.valueLabel?.text = carbText + } + + // Entry time + let startTime = timeFormatter.string(from: status.entry.startDate) + if let absorptionTime = status.entry.absorptionTime, + let duration = absorptionFormatter.string(from: absorptionTime) + { + cell.dateLabel?.text = String( + format: NSLocalizedString("%1$@ + %2$@", comment: "Formats (1: carb start time) and (2: carb absorption duration)"), + startTime, duration + ) + } else { + cell.dateLabel?.text = startTime + } + + if let absorption = status.absorption { + // Absorbed value + let observedProgress = Float(absorption.observedProgress.doubleValue(for: .percent())) + let observedCarbs = max(0, absorption.observed.doubleValue(for: unit)) + + if let observedCarbsText = carbFormatter.string(from: observedCarbs, unit: unit.unitString) { + cell.observedValueText = String( + format: NSLocalizedString("%@ absorbed", comment: "Formats absorbed carb value"), + observedCarbsText + ) + + if absorption.isActive { + cell.observedValueTextColor = UIColor.COBTintColor + } else if 0.9 <= observedProgress && observedProgress <= 1.1 { + cell.observedValueTextColor = UIColor.HIGGrayColor() + } else { + cell.observedValueTextColor = UIColor.agingColor + } + } + + cell.observedProgress = observedProgress + cell.clampedProgress = Float(absorption.clampedProgress.doubleValue(for: .percent())) + cell.observedDateText = absorptionFormatter.string(from: absorption.estimatedDate.duration) + + // Absorbed time + if absorption.isActive { + cell.observedDateTextColor = UIColor.COBTintColor + } else { + cell.observedDateTextColor = UIColor.HIGGrayColor() + + if let absorptionTime = status.entry.absorptionTime { + let durationProgress = absorption.estimatedDate.duration / absorptionTime + if 0.9 > durationProgress || durationProgress > 1.1 { + cell.observedDateTextColor = UIColor.agingColor + } + } + } + } + + cell.isUploading = !status.entry.isUploaded && (deviceManager.loopManager.carbStore.syncDelegate != nil) + return cell + } + } + + private func updateCell(_ cell: HeaderValuesTableViewCell) { + let unit = HKUnit.gram() + + if let carbsOnBoard = carbsOnBoard, carbsOnBoard.quantity.doubleValue(for: unit) > 0 { + cell.COBDateLabel.text = String( + format: NSLocalizedString("at %@", comment: "Format fragment for a specific time"), + timeFormatter.string(from: carbsOnBoard.startDate) + ) + cell.COBValueLabel.text = carbFormatter.string(from: NSNumber(value: carbsOnBoard.quantity.doubleValue(for: unit))) + + // Warn the user if the carbsOnBoard value isn't recent + let textColor: UIColor + switch carbsOnBoard.startDate.timeIntervalSinceNow { + case let t where t < .minutes(-30): + textColor = .staleColor + case let t where t < .minutes(-15): + textColor = .agingColor + default: + textColor = .secondaryLabelColor + } + + cell.COBDateLabel.textColor = textColor + } else { + cell.COBDateLabel.text = nil + cell.COBValueLabel.text = carbFormatter.string(from: NSNumber(value: 0)) + } + + if let carbTotal = carbTotal { + cell.totalDateLabel.text = String( + format: NSLocalizedString("since %@", comment: "Format fragment for a start time"), + timeFormatter.string(from: carbTotal.startDate) + ) + cell.totalValueLabel.text = carbFormatter.string(from: NSNumber(value: carbTotal.quantity.doubleValue(for: unit))) + } else { + cell.totalDateLabel.text = nil + cell.totalValueLabel.text = carbFormatter.string(from: NSNumber(value: 0)) + } + } + + override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + switch Section(rawValue: indexPath.section)! { + case .charts, .totals: + return false + case .entries: + return carbStatuses[indexPath.row].entry.createdByCurrentApp + } + } + + public override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + let status = carbStatuses.remove(at: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .automatic) + + deviceManager.loopManager.carbStore.deleteCarbEntry(status.entry) { (success, error) -> Void in + DispatchQueue.main.async { + if success { + // TODO: CarbStore doesn't automatically post this for deletes + NotificationCenter.default.post(name: .CarbEntriesDidUpdate, object: self) + } else if let error = error { + self.refreshContext.update(with: .carbs) + self.presentAlertController(with: error) + } + } + } + } + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { + switch Section(rawValue: indexPath.section)! { + case .charts, .totals: + return self.tableView(tableView, heightForRowAt: indexPath) + case .entries: + return 66 + } + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + switch Section(rawValue: indexPath.section)! { + case .charts: + // 20: Status bar + // 44: Toolbar + let availableSize = max(tableView.bounds.width, tableView.bounds.height) - 20 - (tableView.tableHeaderView?.frame.height ?? 0) - 44 + + switch ChartRow(rawValue: indexPath.row)! { + case .carbEffect: + return max(100, 0.40 * availableSize) + } + case .totals, .entries: + return UITableViewAutomaticDimension + } + } + + override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + switch Section(rawValue: indexPath.section)! { + case .charts: + return indexPath + case .totals: + return nil + case .entries: + return carbStatuses[indexPath.row].entry.createdByCurrentApp ? indexPath : nil + } + } + + // MARK: - Navigation + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + super.prepare(for: segue, sender: sender) + + var targetViewController = segue.destination + + if let navVC = targetViewController as? UINavigationController, let topViewController = navVC.topViewController { + targetViewController = topViewController + } + + switch targetViewController { + case let vc as BolusViewController: + vc.configureWithLoopManager(self.deviceManager.loopManager, + recommendation: sender as? BolusRecommendation, + glucoseUnit: self.charts.glucoseUnit + ) + case let vc as CarbEntryEditViewController: + if let selectedCell = sender as? UITableViewCell, let indexPath = tableView.indexPath(for: selectedCell), indexPath.row < carbStatuses.count { + vc.originalCarbEntry = carbStatuses[indexPath.row].entry + } + + vc.defaultAbsorptionTimes = deviceManager.loopManager.carbStore.defaultAbsorptionTimes + vc.preferredUnit = deviceManager.loopManager.carbStore.preferredUnit + default: + break + } + } + + /// Unwind segue action from the CarbEntryEditViewController + /// + /// - parameter segue: The unwind segue + @IBAction func unwindFromEditing(_ segue: UIStoryboardSegue) { + guard let editVC = segue.source as? CarbEntryEditViewController, + let updatedEntry = editVC.updatedCarbEntry + else { + return + } + + deviceManager.loopManager.addCarbEntryAndRecommendBolus(updatedEntry, replacing: editVC.originalCarbEntry) { (result) in + DispatchQueue.main.async { + switch result { + case .success(let recommendation): + if self.active && self.visible, let bolus = recommendation?.amount, bolus > 0 { + self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation) + } + case .failure(let error): + // Ignore bolus wizard errors + if error is CarbStore.CarbStoreError { + self.presentAlertController(with: error) + } + } + } + } + } + + @IBAction func unwindFromBolusViewController(_ segue: UIStoryboardSegue) { + if let bolusViewController = segue.source as? BolusViewController { + if let bolus = bolusViewController.bolus, bolus > 0 { + deviceManager.enactBolus(units: bolus) { (_) in + } + } + } + } + +} diff --git a/Loop/View Controllers/ChartsTableViewController.swift b/Loop/View Controllers/ChartsTableViewController.swift index 13b5588731..8eec318e6e 100644 --- a/Loop/View Controllers/ChartsTableViewController.swift +++ b/Loop/View Controllers/ChartsTableViewController.swift @@ -30,10 +30,10 @@ class ChartsTableViewController: UITableViewController, UIGestureRecognizerDeleg let notificationCenter = NotificationCenter.default notificationObservers += [ - notificationCenter.addObserver(forName: .UIApplicationWillResignActive, object: UIApplication.shared, queue: .main) { _ in + notificationCenter.addObserver(forName: .UIApplicationWillResignActive, object: UIApplication.shared, queue: .main) { [unowned self] _ in self.active = false }, - notificationCenter.addObserver(forName: .UIApplicationDidBecomeActive, object: UIApplication.shared, queue: .main) { _ in + notificationCenter.addObserver(forName: .UIApplicationDidBecomeActive, object: UIApplication.shared, queue: .main) { [unowned self] _ in self.active = true } ] diff --git a/Loop/View Controllers/PredictionTableViewController.swift b/Loop/View Controllers/PredictionTableViewController.swift index 72dd55d39e..d1d870b52d 100644 --- a/Loop/View Controllers/PredictionTableViewController.swift +++ b/Loop/View Controllers/PredictionTableViewController.swift @@ -32,7 +32,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas let notificationCenter = NotificationCenter.default notificationObservers += [ - notificationCenter.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { note in + notificationCenter.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { [unowned self] note in let context = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as! LoopDataManager.LoopUpdateContext.RawValue DispatchQueue.main.async { switch LoopDataManager.LoopUpdateContext(rawValue: context) { @@ -154,13 +154,24 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas reloadGroup.notify(queue: .main) { self.charts.prerender() - for case let cell as ChartTableViewCell in self.tableView.visibleCells { - cell.reloadChart() + self.tableView.beginUpdates() + for cell in self.tableView.visibleCells { + switch cell { + case let cell as ChartTableViewCell: + cell.reloadChart() - if let indexPath = self.tableView.indexPath(for: cell) { - self.tableView(self.tableView, updateTitleFor: cell, at: indexPath) + if let indexPath = self.tableView.indexPath(for: cell) { + self.tableView(self.tableView, updateTitleFor: cell, at: indexPath) + } + case let cell as PredictionInputEffectTableViewCell: + if let indexPath = self.tableView.indexPath(for: cell) { + self.tableView(self.tableView, updateTextFor: cell, at: indexPath) + } + default: + break } } + self.tableView.endUpdates() } } @@ -214,35 +225,7 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas return cell case .inputs: let cell = tableView.dequeueReusableCell(withIdentifier: PredictionInputEffectTableViewCell.className, for: indexPath) as! PredictionInputEffectTableViewCell - - let input = availableInputs[indexPath.row] - - cell.titleLabel?.text = input.localizedTitle - cell.accessoryType = selectedInputs.contains(input) ? .checkmark : .none - cell.enabled = input != .retrospection || deviceManager.loopManager.settings.retrospectiveCorrectionEnabled - - var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) ?? "" - - if input == .retrospection, - let startGlucose = retrospectivePredictedGlucose?.first, - let endGlucose = retrospectivePredictedGlucose?.last, - let currentGlucose = self.deviceManager.loopManager.glucoseStore.latestGlucose - { - let formatter = NumberFormatter.glucoseFormatter(for: charts.glucoseUnit) - let values = [startGlucose, endGlucose, currentGlucose].map { formatter.string(from: NSNumber(value: $0.quantity.doubleValue(for: charts.glucoseUnit))) ?? "?" } - - let retro = String( - format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"), - values[0], values[1], values[2] - ) - - subtitleText = String(format: "%@\n%@", subtitleText, retro) - } - - cell.subtitleLabel?.text = subtitleText - - cell.contentView.layoutMargins.left = tableView.separatorInset.left - + self.tableView(tableView, updateTextFor: cell, at: indexPath) return cell case .settings: let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell @@ -259,16 +242,47 @@ class PredictionTableViewController: ChartsTableViewController, IdentifiableClas } private func tableView(_ tableView: UITableView, updateTitleFor cell: ChartTableViewCell, at indexPath: IndexPath) { - switch Section(rawValue: indexPath.section)! { - case .charts: - if let eventualGlucose = eventualGlucoseDescription { - cell.titleLabel?.text = String(format: NSLocalizedString("Eventually %@", comment: "The subtitle format describing eventual glucose. (1: localized glucose value description)"), eventualGlucose) - } else { - cell.titleLabel?.text = "–" - } - default: - break + guard case .charts? = Section(rawValue: indexPath.section) else { + return + } + + if let eventualGlucose = eventualGlucoseDescription { + cell.titleLabel?.text = String(format: NSLocalizedString("Eventually %@", comment: "The subtitle format describing eventual glucose. (1: localized glucose value description)"), eventualGlucose) + } else { + cell.titleLabel?.text = "–" + } + } + + private func tableView(_ tableView: UITableView, updateTextFor cell: PredictionInputEffectTableViewCell, at indexPath: IndexPath) { + guard case .inputs? = Section(rawValue: indexPath.section) else { + return + } + + let input = availableInputs[indexPath.row] + + cell.titleLabel?.text = input.localizedTitle + cell.accessoryType = selectedInputs.contains(input) ? .checkmark : .none + cell.enabled = input != .retrospection || deviceManager.loopManager.settings.retrospectiveCorrectionEnabled + + var subtitleText = input.localizedDescription(forGlucoseUnit: charts.glucoseUnit) ?? "" + + if input == .retrospection, + let startGlucose = retrospectivePredictedGlucose?.first, + let endGlucose = retrospectivePredictedGlucose?.last, + let currentGlucose = self.deviceManager.loopManager.glucoseStore.latestGlucose + { + let formatter = NumberFormatter.glucoseFormatter(for: charts.glucoseUnit) + let values = [startGlucose, endGlucose, currentGlucose].map { formatter.string(from: NSNumber(value: $0.quantity.doubleValue(for: charts.glucoseUnit))) ?? "?" } + + let retro = String( + format: NSLocalizedString("Last comparison: %1$@ → %2$@ vs %3$@", comment: "Format string describing retrospective glucose prediction comparison. (1: Previous glucose)(2: Predicted glucose)(3: Actual glucose)"), + values[0], values[1], values[2] + ) + + subtitleText = String(format: "%@\n%@", subtitleText, retro) } + + cell.subtitleLabel?.text = subtitleText } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { diff --git a/Loop/View Controllers/SettingsTableViewController.swift b/Loop/View Controllers/SettingsTableViewController.swift index b19bb60d82..a594208f98 100644 --- a/Loop/View Controllers/SettingsTableViewController.swift +++ b/Loop/View Controllers/SettingsTableViewController.swift @@ -14,6 +14,7 @@ import MinimedKit private let ConfigCellIdentifier = "ConfigTableViewCell" +private let EnabledString = NSLocalizedString("Enabled", comment: "The detail text describing an enabled setting") private let TapToSetString = NSLocalizedString("Tap to set", comment: "The empty-state text for a configuration value") @@ -27,16 +28,20 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu tableView.register(RileyLinkDeviceTableViewCell.nib(), forCellReuseIdentifier: RileyLinkDeviceTableViewCell.className) dataManagerObserver = NotificationCenter.default.addObserver(forName: nil, object: dataManager, queue: nil) { [weak self = self] (note) -> Void in - if let deviceManager = self?.dataManager.rileyLinkManager { - switch note.name { - case Notification.Name.DeviceManagerDidDiscoverDevice: - self?.tableView.insertRows(at: [IndexPath(row: deviceManager.devices.count - 1, section: Section.devices.rawValue)], with: .automatic) - case Notification.Name.DeviceConnectionStateDidChange: - if let device = note.userInfo?[RileyLinkDeviceManager.RileyLinkDeviceKey] as? RileyLinkDevice, let index = deviceManager.devices.index(where: { $0 === device }) { - self?.tableView.reloadRows(at: [IndexPath(row: index, section: Section.devices.rawValue)], with: .none) + DispatchQueue.main.async { + if let deviceManager = self?.dataManager.rileyLinkManager { + switch note.name { + case Notification.Name.DeviceManagerDidDiscoverDevice: + self?.tableView.insertRows(at: [IndexPath(row: deviceManager.devices.count - 1, section: Section.devices.rawValue)], with: .automatic) + case Notification.Name.DeviceConnectionStateDidChange, + Notification.Name.DeviceRSSIDidChange, + Notification.Name.DeviceNameDidChange: + if let device = note.userInfo?[RileyLinkDeviceManager.RileyLinkDeviceKey] as? RileyLinkDevice, let index = deviceManager.devices.index(where: { $0 === device }) { + self?.tableView.reloadRows(at: [IndexPath(row: index, section: Section.devices.rawValue)], with: .none) + } + default: + break } - default: - break } } } @@ -45,7 +50,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - dataManager.rileyLinkManager.deviceScanningEnabled = true + dataManager.rileyLinkManager.setDeviceScanningEnabled(true) if case .some = dataManager.cgm, dataManager.loopManager.glucoseStore.authorizationRequired { dataManager.loopManager.glucoseStore.authorize { (success, error) -> Void in @@ -53,13 +58,13 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } } - AnalyticsManager.sharedManager.didDisplaySettingsScreen() + AnalyticsManager.shared.didDisplaySettingsScreen() } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - dataManager.rileyLinkManager.deviceScanningEnabled = false + dataManager.rileyLinkManager.setDeviceScanningEnabled(false) } deinit { @@ -100,6 +105,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case enlite = 0 case g4 case g5 + case dexcomShare // only displayed if g4 or g5 switched on case g5TransmitterID // only displayed if g5 switched on } @@ -115,9 +121,9 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } fileprivate enum ServiceRow: Int, CaseCountable { - case share = 0 - case nightscout + case nightscout = 0 case mLab + case loggly case amplitude } @@ -145,10 +151,12 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu return PumpRow.count case .cgm: switch dataManager.cgm { + case .g4?: + return CGMRow.count - 1 // No Transmitter ID cell case .g5?: return CGMRow.count default: - return CGMRow.count - 1 + return CGMRow.count - 2 // No Share or Transmitter ID cell } case .configuration: return ConfigurationRow.count @@ -160,8 +168,6 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell: UITableViewCell - switch Section(rawValue: indexPath.section)! { case .loop: switch LoopRow(rawValue: indexPath.row)! { @@ -199,20 +205,28 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu configCell.textLabel?.text = NSLocalizedString("Pump Battery Type", comment: "The title text for the battery type value") configCell.detailTextLabel?.text = String(describing: dataManager.batteryChemistry) } - cell = configCell + return configCell case .cgm: let row = CGMRow(rawValue: indexPath.row)! switch row { + case .dexcomShare: + let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) + let shareService = dataManager.remoteDataManager.shareService + + configCell.textLabel?.text = shareService.title + configCell.detailTextLabel?.text = shareService.username ?? TapToSetString + + return configCell case .g5TransmitterID: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) - configCell.textLabel?.text = NSLocalizedString("G5 Transmitter ID", comment: "The title text for the Dexcom G5 transmitter ID config value") + configCell.textLabel?.text = NSLocalizedString("Transmitter ID", comment: "The title text for the Dexcom G5 transmitter ID config value") if case .g5(let transmitterID)? = dataManager.cgm { configCell.detailTextLabel?.text = transmitterID ?? TapToSetString } - cell = configCell + return configCell default: let switchCell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.className, for: indexPath) as! SwitchTableViewCell @@ -234,11 +248,11 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu switchCell.titleLabel.text = NSLocalizedString("G5 Transmitter", comment: "The title text for the G5 Transmitter switch cell") switchCell.switch?.addTarget(self, action: #selector(g5Changed(_:)), for: .valueChanged) - case .g5TransmitterID: + case .dexcomShare, .g5TransmitterID: assertionFailure() } - cell = switchCell + return switchCell } case .configuration: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) @@ -312,7 +326,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu configCell.detailTextLabel?.text = TapToSetString } case .maxBasal: - configCell.textLabel?.text = NSLocalizedString("Maximum Basal Rate", comment: "The title text for the maximum basal rate value") + configCell.textLabel?.text = NSLocalizedString("Maximum Basal Rate", comment: "The title text for the maximum basal rate value") if let maxBasal = dataManager.loopManager.settings.maximumBasalRatePerHour { configCell.detailTextLabel?.text = "\(valueNumberFormatter.string(from: NSNumber(value: maxBasal))!) U/hour" @@ -329,7 +343,7 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } } - cell = configCell + return configCell case .devices: let deviceCell = tableView.dequeueReusableCell(withIdentifier: RileyLinkDeviceTableViewCell.className) as! RileyLinkDeviceTableViewCell let device = dataManager.rileyLinkManager.devices[indexPath.row] @@ -341,16 +355,11 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu deviceCell.connectSwitch.addTarget(self, action: #selector(deviceConnectionChanged(_:)), for: .valueChanged) - cell = deviceCell + return deviceCell case .services: let configCell = tableView.dequeueReusableCell(withIdentifier: ConfigCellIdentifier, for: indexPath) switch ServiceRow(rawValue: indexPath.row)! { - case .share: - let shareService = dataManager.remoteDataManager.shareService - - configCell.textLabel?.text = shareService.title - configCell.detailTextLabel?.text = shareService.username ?? TapToSetString case .nightscout: let nightscoutService = dataManager.remoteDataManager.nightscoutService @@ -361,16 +370,20 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu configCell.textLabel?.text = mLabService.title configCell.detailTextLabel?.text = mLabService.databaseName ?? TapToSetString + case .loggly: + let logglyService = dataManager.logger.logglyService + + configCell.textLabel?.text = logglyService.title + configCell.detailTextLabel?.text = logglyService.isAuthorized ? EnabledString : TapToSetString case .amplitude: - let amplitudeService = AnalyticsManager.sharedManager.amplitudeService + let amplitudeService = AnalyticsManager.shared.amplitudeService configCell.textLabel?.text = amplitudeService.title - configCell.detailTextLabel?.text = amplitudeService.isAuthorized ? NSLocalizedString("Enabled", comment: "The detail text describing an enabled setting") : TapToSetString + configCell.detailTextLabel?.text = amplitudeService.isAuthorized ? EnabledString : TapToSetString } return configCell } - return cell } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { @@ -392,6 +405,22 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu // MARK: - UITableViewDelegate + override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { + switch Section(rawValue: indexPath.section)! { + case .cgm: + switch CGMRow(rawValue: indexPath.row)! { + case .dexcomShare, .g5TransmitterID: + return 1 + default: + break + } + default: + break + } + + return 0 + } + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let sender = tableView.cellForRow(at: indexPath) @@ -421,6 +450,16 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } case .cgm: switch CGMRow(rawValue: indexPath.row)! { + case .dexcomShare: + let service = dataManager.remoteDataManager.shareService + let vc = AuthenticationViewController(authentication: service) + vc.authenticationObserver = { [unowned self] (service) in + self.dataManager.remoteDataManager.shareService = service + + self.tableView.reloadRows(at: [indexPath], with: .none) + } + + show(vc, sender: sender) case .g5TransmitterID: let vc: TextFieldTableViewController var value: String? @@ -476,25 +515,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu scheduleVC.delegate = self scheduleVC.title = NSLocalizedString("Carb Ratios", comment: "The title of the carb ratios schedule screen") + scheduleVC.unit = .gram() if let schedule = dataManager.loopManager.carbRatioSchedule { scheduleVC.timeZone = schedule.timeZone scheduleVC.scheduleItems = schedule.items scheduleVC.unit = schedule.unit - - show(scheduleVC, sender: sender) - } else { - dataManager.loopManager.carbStore.preferredUnit { (unit, error) -> Void in - DispatchQueue.main.async { - if let error = error { - self.presentAlertController(with: error) - } else if let unit = unit { - scheduleVC.unit = unit - self.show(scheduleVC, sender: sender) - } - } - } } + + show(scheduleVC, sender: sender) case .insulinSensitivity: let scheduleVC = DailyQuantityScheduleTableViewController() @@ -590,41 +619,41 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu } case .services: switch ServiceRow(rawValue: indexPath.row)! { - case .share: - let service = dataManager.remoteDataManager.shareService + case .nightscout: + let service = dataManager.remoteDataManager.nightscoutService let vc = AuthenticationViewController(authentication: service) vc.authenticationObserver = { [unowned self] (service) in - self.dataManager.remoteDataManager.shareService = service + self.dataManager.remoteDataManager.nightscoutService = service self.tableView.reloadRows(at: [indexPath], with: .none) } show(vc, sender: sender) - case .nightscout: - let service = dataManager.remoteDataManager.nightscoutService + case .mLab: + let service = dataManager.logger.mLabService let vc = AuthenticationViewController(authentication: service) vc.authenticationObserver = { [unowned self] (service) in - self.dataManager.remoteDataManager.nightscoutService = service + self.dataManager.logger.mLabService = service self.tableView.reloadRows(at: [indexPath], with: .none) } show(vc, sender: sender) - case .mLab: - let service = dataManager.logger.mLabService + case .loggly: + let service = dataManager.logger.logglyService let vc = AuthenticationViewController(authentication: service) vc.authenticationObserver = { [unowned self] (service) in - self.dataManager.logger.mLabService = service + self.dataManager.logger.logglyService = service self.tableView.reloadRows(at: [indexPath], with: .none) } show(vc, sender: sender) case .amplitude: - let service = AnalyticsManager.sharedManager.amplitudeService + let service = AnalyticsManager.shared.amplitudeService let vc = AuthenticationViewController(authentication: service) vc.authenticationObserver = { [unowned self] (service) in - AnalyticsManager.sharedManager.amplitudeService = service + AnalyticsManager.shared.amplitudeService = service self.tableView.reloadRows(at: [indexPath], with: .none) } @@ -676,10 +705,17 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if sender.isOn { setG4SwitchOff() setEnliteSwitchOff() + let shareRowExists = tableView.numberOfRows(inSection: Section.cgm.rawValue) > CGMRow.dexcomShare.rawValue dataManager.cgm = .g5(transmitterID: g5TransmitterID) - tableView.insertRows(at: [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) + var indexPaths = [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section:Section.cgm.rawValue)] + if !shareRowExists { + indexPaths.insert(IndexPath(row: CGMRow.dexcomShare.rawValue, section:Section.cgm.rawValue), at: 0) + } + + tableView.insertRows(at: indexPaths, with: .top) } else { + removeDexcomShareRow() removeG5TransmitterIDRow() dataManager.cgm = nil } @@ -691,8 +727,15 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if sender.isOn { setG5SwitchOff() setEnliteSwitchOff() + removeG5TransmitterIDRow() + let shareRowExists = tableView.numberOfRows(inSection: Section.cgm.rawValue) > CGMRow.dexcomShare.rawValue dataManager.cgm = .g4 + + if !shareRowExists { + tableView.insertRows(at: [IndexPath(row: CGMRow.dexcomShare.rawValue, section:Section.cgm.rawValue)], with: .top) + } } else { + removeDexcomShareRow() dataManager.cgm = nil } tableView.endUpdates() @@ -703,6 +746,8 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu if sender.isOn { setG5SwitchOff() setG4SwitchOff() + removeDexcomShareRow() + removeG5TransmitterIDRow() dataManager.cgm = .enlite } else { dataManager.cgm = nil @@ -712,18 +757,25 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu // MARK: Views + private func removeDexcomShareRow() { + switch dataManager.cgm { + case .g4?, .g5?: + tableView.deleteRows(at: [IndexPath(row: CGMRow.dexcomShare.rawValue, section: Section.cgm.rawValue)], with: .top) + default: + break; + } + } + private func removeG5TransmitterIDRow() { if case .g5(let transmitterID)? = dataManager.cgm { g5TransmitterID = transmitterID - tableView.deleteRows(at: [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section:Section.cgm.rawValue)], with: .top) + tableView.deleteRows(at: [IndexPath(row: CGMRow.g5TransmitterID.rawValue, section: Section.cgm.rawValue)], with: .top) } } private func setG5SwitchOff() { let switchCell = tableView.cellForRow(at: IndexPath(row: CGMRow.g5.rawValue, section: Section.cgm.rawValue)) as! SwitchTableViewCell switchCell.switch?.setOn(false, animated: true) - - removeG5TransmitterIDRow() } private func setG4SwitchOff() { @@ -746,22 +798,22 @@ final class SettingsTableViewController: UITableViewController, DailyValueSchedu case .basalRate: if let controller = controller as? SingleValueScheduleTableViewController { dataManager.loopManager.basalRateSchedule = BasalRateSchedule(dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeBasalRateSchedule() + AnalyticsManager.shared.didChangeBasalRateSchedule() } case .glucoseTargetRange: if let controller = controller as? GlucoseRangeScheduleTableViewController { dataManager.loopManager.settings.glucoseTargetRangeSchedule = GlucoseRangeSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, workoutRange: controller.workoutRange, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeGlucoseTargetRangeSchedule() + AnalyticsManager.shared.didChangeGlucoseTargetRangeSchedule() } case let row: if let controller = controller as? DailyQuantityScheduleTableViewController { switch row { case .carbRatio: dataManager.loopManager.carbRatioSchedule = CarbRatioSchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeCarbRatioSchedule() + AnalyticsManager.shared.didChangeCarbRatioSchedule() case .insulinSensitivity: dataManager.loopManager.insulinSensitivitySchedule = InsulinSensitivitySchedule(unit: controller.unit, dailyItems: controller.scheduleItems, timeZone: controller.timeZone) - AnalyticsManager.sharedManager.didChangeInsulinSensitivitySchedule() + AnalyticsManager.shared.didChangeInsulinSensitivitySchedule() default: break } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index a531e521e8..682966ee95 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -44,7 +44,7 @@ final class StatusTableViewController: ChartsTableViewController { let notificationCenter = NotificationCenter.default notificationObservers += [ - notificationCenter.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { note in + notificationCenter.addObserver(forName: .LoopDataUpdated, object: deviceManager.loopManager, queue: nil) { [unowned self] note in let context = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as! LoopDataManager.LoopUpdateContext.RawValue DispatchQueue.main.async { switch LoopDataManager.LoopUpdateContext(rawValue: context) { @@ -55,7 +55,7 @@ final class StatusTableViewController: ChartsTableViewController { case .carbs?: self.refreshContext.update(with: .carbs) case .glucose?: - self.refreshContext.update(with: .glucose) + self.refreshContext.update(with: [.glucose, .carbs]) case .tempBasal?: self.refreshContext.update(with: .insulin) } @@ -64,7 +64,7 @@ final class StatusTableViewController: ChartsTableViewController { self.reloadData(animated: true) } }, - notificationCenter.addObserver(forName: .LoopRunning, object: deviceManager.loopManager, queue: nil) { _ in + notificationCenter.addObserver(forName: .LoopRunning, object: deviceManager.loopManager, queue: nil) { [unowned self] _ in DispatchQueue.main.async { self.hudView.loopCompletionHUD.loopInProgress = true } @@ -101,7 +101,7 @@ final class StatusTableViewController: ChartsTableViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - AnalyticsManager.sharedManager.didDisplayStatusScreen() + AnalyticsManager.shared.didDisplayStatusScreen() } override func viewWillDisappear(_ animated: Bool) { @@ -162,8 +162,9 @@ final class StatusTableViewController: ChartsTableViewController { chartStartDate = Calendar.current.nextDate(after: date, matching: components, matchingPolicy: .strict, direction: .backward) ?? date let reloadGroup = DispatchGroup() - var newLastLoopCompleted: Date? - var newLastTempBasal: DoseEntry? + var lastLoopCompleted: Date? + var lastReservoirValue: ReservoirValue? + var lastTempBasal: DoseEntry? var newRecommendedTempBasal: LoopDataManager.TempBasalRecommendation? reloadGroup.enter() @@ -206,8 +207,8 @@ final class StatusTableViewController: ChartsTableViewController { newRecommendedTempBasal = state.recommendedTempBasal } - newLastTempBasal = state.lastTempBasal - newLastLoopCompleted = state.lastLoopCompleted + lastTempBasal = state.lastTempBasal + lastLoopCompleted = state.lastLoopCompleted if let lastPoint = self.charts.predictedGlucosePoints.last?.y { self.eventualGlucoseDescription = String(describing: lastPoint) @@ -223,6 +224,14 @@ final class StatusTableViewController: ChartsTableViewController { } } + if self.refreshContext.remove(.carbs) != nil { + reloadGroup.enter() + manager.carbStore.getCarbsOnBoardValues(start: self.chartStartDate, effectVelocities: manager.settings.dynamicCarbAbsorptionEnabled ? state.insulinCounteractionEffects : nil) { (values) in + self.charts.setCOBValues(values) + reloadGroup.leave() + } + } + reloadGroup.leave() } @@ -268,30 +277,20 @@ final class StatusTableViewController: ChartsTableViewController { reloadGroup.leave() } - } - if refreshContext.remove(.carbs) != nil { reloadGroup.enter() - deviceManager.loopManager.carbStore.getCarbsOnBoardValues(startDate: chartStartDate) { (values, error) -> Void in - if let error = error { - self.deviceManager.logger.addError(error, fromSource: "CarbStore") - self.refreshContext.update(with: .carbs) + deviceManager.loopManager.doseStore.getReservoirValues(since: Date(timeIntervalSinceNow: .minutes(-30))) { (result) in + switch result { + case .success(let values): + lastReservoirValue = values.first + case .failure: + self.refreshContext.update(with: .insulin) } - self.charts.setCOBValues(values) - reloadGroup.leave() } } - if let reservoir = deviceManager.loopManager.doseStore.lastReservoirValue { - if let capacity = deviceManager.pumpState?.pumpModel?.reservoirCapacity { - hudView.reservoirVolumeHUD.reservoirLevel = min(1, max(0, Double(reservoir.unitVolume / Double(capacity)))) - } - - hudView.reservoirVolumeHUD.setReservoirVolume(volume: reservoir.unitVolume, at: reservoir.startDate) - } - if let level = deviceManager.pumpBatteryChargeRemaining { hudView.batteryHUD.batteryLevel = level } @@ -309,21 +308,29 @@ final class StatusTableViewController: ChartsTableViewController { ) } + if let reservoir = lastReservoirValue { + if let capacity = self.deviceManager.pumpState?.pumpModel?.reservoirCapacity { + self.hudView.reservoirVolumeHUD.reservoirLevel = min(1, max(0, Double(reservoir.unitVolume / Double(capacity)))) + } + + self.hudView.reservoirVolumeHUD.setReservoirVolume(volume: reservoir.unitVolume, at: reservoir.startDate) + } + // Loop completion HUD - self.hudView.loopCompletionHUD.lastLoopCompleted = newLastLoopCompleted + self.hudView.loopCompletionHUD.lastLoopCompleted = lastLoopCompleted // Net basal rate HUD - let date = newLastTempBasal?.startDate ?? Date() + let date = lastTempBasal?.startDate ?? Date() if let scheduledBasal = self.deviceManager.loopManager.basalRateSchedule?.between(start: date, end: date).first { let netBasal = NetBasal( - lastTempBasal: newLastTempBasal, + lastTempBasal: lastTempBasal, maxBasal: self.deviceManager.loopManager.settings.maximumBasalRatePerHour, scheduledBasal: scheduledBasal ) - self.hudView.basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percent, at: netBasal.startDate) + self.hudView.basalRateHUD.setNetBasalRate(netBasal.rate, percent: netBasal.percent, at: netBasal.start) } // Fetch the current IOB subtitle @@ -588,7 +595,7 @@ final class StatusTableViewController: ChartsTableViewController { case .iob, .dose: performSegue(withIdentifier: InsulinDeliveryTableViewController.className, sender: indexPath) case .cob: - performSegue(withIdentifier: CarbEntryTableViewController.className, sender: indexPath) + performSegue(withIdentifier: CarbAbsorptionViewController.className, sender: indexPath) } case .status: switch StatusRow(rawValue: indexPath.row)! { @@ -618,15 +625,18 @@ final class StatusTableViewController: ChartsTableViewController { // MARK: - Actions override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool { - if identifier == CarbEntryEditViewController.className { + switch identifier { + case CarbEntryEditViewController.className, CarbAbsorptionViewController.className: if deviceManager.loopManager.carbStore.authorizationRequired { deviceManager.loopManager.carbStore.authorize { (success, error) in if success { - self.performSegue(withIdentifier: CarbEntryEditViewController.className, sender: sender) + self.performSegue(withIdentifier: identifier, sender: sender) } } return false } + default: + break } return true @@ -642,6 +652,9 @@ final class StatusTableViewController: ChartsTableViewController { } switch targetViewController { + case let vc as CarbAbsorptionViewController: + vc.deviceManager = deviceManager + vc.hidesBottomBarWhenPushed = true case let vc as CarbEntryTableViewController: vc.carbStore = deviceManager.loopManager.carbStore vc.hidesBottomBarWhenPushed = true @@ -652,35 +665,10 @@ final class StatusTableViewController: ChartsTableViewController { vc.doseStore = deviceManager.loopManager.doseStore vc.hidesBottomBarWhenPushed = true case let vc as BolusViewController: - self.deviceManager.loopManager.getLoopState { (manager, state) in - let maximumBolus = manager.settings.maximumBolus - - let activeInsulin = state.insulinOnBoard?.value - let activeCarbohydrates = state.carbsOnBoard?.quantity.doubleValue(for: HKUnit.gram()) - let bolusRecommendation: BolusRecommendation? - - if let recommendation = sender as? BolusRecommendation { - bolusRecommendation = recommendation - } else { - do { - bolusRecommendation = try state.recommendBolus() - } catch let error { - bolusRecommendation = nil - self.deviceManager.logger.addError(error, fromSource: "Bolus") - } - } - - DispatchQueue.main.async { - if let maxBolus = maximumBolus { - vc.maxBolus = maxBolus - } - - vc.glucoseUnit = self.charts.glucoseUnit - vc.activeInsulin = activeInsulin - vc.activeCarbohydrates = activeCarbohydrates - vc.bolusRecommendation = bolusRecommendation - } - } + vc.configureWithLoopManager(self.deviceManager.loopManager, + recommendation: sender as? BolusRecommendation, + glucoseUnit: self.charts.glucoseUnit + ) case let vc as PredictionTableViewController: vc.deviceManager = deviceManager case let vc as SettingsTableViewController: @@ -694,22 +682,24 @@ final class StatusTableViewController: ChartsTableViewController { /// /// - parameter segue: The unwind segue @IBAction func unwindFromEditing(_ segue: UIStoryboardSegue) { - if let carbVC = segue.source as? CarbEntryEditViewController, let updatedEntry = carbVC.updatedCarbEntry { - deviceManager.loopManager.addCarbEntryAndRecommendBolus(updatedEntry) { (result) -> Void in - DispatchQueue.main.async { - switch result { - case .success(let recommendation): - if self.active && self.visible, let bolus = recommendation?.amount, bolus > 0 { - self.bolusState = .recommended - self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation) - } - case .failure(let error): - // Ignore bolus wizard errors - if error is CarbStore.CarbStoreError { - self.presentAlertController(with: error) - } else { - self.deviceManager.logger.addError(error, fromSource: "Bolus") - } + guard let carbVC = segue.source as? CarbEntryEditViewController, let updatedEntry = carbVC.updatedCarbEntry else { + return + } + + deviceManager.loopManager.addCarbEntryAndRecommendBolus(updatedEntry) { (result) -> Void in + DispatchQueue.main.async { + switch result { + case .success(let recommendation): + if self.active && self.visible, let bolus = recommendation?.amount, bolus > 0 { + self.bolusState = .recommended + self.performSegue(withIdentifier: BolusViewController.className, sender: recommendation) + } + case .failure(let error): + // Ignore bolus wizard errors + if error is CarbStore.CarbStoreError { + self.presentAlertController(with: error) + } else { + self.deviceManager.logger.addError(error, fromSource: "Bolus") } } } diff --git a/Loop/Views/AuthenticationTableViewCell.swift b/Loop/Views/AuthenticationTableViewCell.swift index 7d8eb85504..157ea04780 100644 --- a/Loop/Views/AuthenticationTableViewCell.swift +++ b/Loop/Views/AuthenticationTableViewCell.swift @@ -8,6 +8,7 @@ import UIKit + final class AuthenticationTableViewCell: UITableViewCell, NibLoadable { @IBOutlet weak var titleLabel: UILabel! @@ -26,6 +27,107 @@ final class AuthenticationTableViewCell: UITableViewCell, NibLoadable { super.prepareForReuse() textField.delegate = nil + credentialOptionPicker = nil + } + + var credentialOptionPicker: CredentialOptionPicker? { + didSet { + if let picker = credentialOptionPicker { + picker.delegate = self + + textField.text = picker.selectedOption.title + textField.inputView = picker.view + textField.tintColor = .clear // Makes the cursor invisible + } else { + textField.inputView = nil + textField.tintColor = nil + } + } + } + + var value: String? { + if let picker = credentialOptionPicker { + return picker.value + } else { + return textField.text + } + } +} + +extension AuthenticationTableViewCell: CredentialOptionPickerDelegate { + func credentialOptionDataSourceDidUpdateValue(_ picker: CredentialOptionPicker) { + textField.text = picker.selectedOption.title + textField.delegate?.textFieldDidEndEditing?(textField) } +} + + +protocol CredentialOptionPickerDelegate: class { + func credentialOptionDataSourceDidUpdateValue(_ picker: CredentialOptionPicker) +} + + +class CredentialOptionPicker: NSObject, UIPickerViewDataSource, UIPickerViewDelegate { + let options: [(title: String, value: String)] + weak var delegate: CredentialOptionPickerDelegate? + + let view = UIPickerView() + + var selectedOption: (title: String, value: String) { + let index = view.selectedRow(inComponent: 0) + guard index >= options.startIndex && index < options.endIndex else { + return options[0] + } + + return options[index] + } + + var value: String? { + get { + return selectedOption.value + } + set { + let index: Int + + if let value = newValue, let optionIndex = options.index(where: { $0.value == value }) { + index = optionIndex + } else { + index = 0 + } + + view.selectRow(index, inComponent: 0, animated: view.superview != nil) + } + } + + init(options: [(title: String, value: String)]) { + assert(options.count > 0, "At least one option must be specified") + + self.options = options + + super.init() + + view.dataSource = self + view.delegate = self + } + + // MARK: - UIPickerViewDataSource + + func numberOfComponents(in pickerView: UIPickerView) -> Int { + return 1 + } + + func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { + return options.count + } + + // MARK: - UIPickerViewDelegate + + func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { + return options[row].title + } + + func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { + delegate?.credentialOptionDataSourceDidUpdateValue(self) + } } diff --git a/Loop/Views/CarbEntryTableViewCell.swift b/Loop/Views/CarbEntryTableViewCell.swift new file mode 100644 index 0000000000..86112e212c --- /dev/null +++ b/Loop/Views/CarbEntryTableViewCell.swift @@ -0,0 +1,123 @@ +// +// CarbEntryTableViewCell.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit + +class CarbEntryTableViewCell: UITableViewCell { + + @IBOutlet private weak var clampedProgressView: UIProgressView! + + @IBOutlet private weak var observedProgressView: UIProgressView! + + @IBOutlet weak var valueLabel: UILabel! + + @IBOutlet weak var dateLabel: UILabel! + + @IBOutlet private weak var observedValueLabel: UILabel! + + @IBOutlet private weak var observedDateLabel: UILabel! + + @IBOutlet private weak var uploadingIndicator: UIImageView! + + var clampedProgress: Float { + get { + return clampedProgressView.progress + } + set { + clampedProgressView.progress = newValue + clampedProgressView.isHidden = clampedProgress <= 0 + } + } + + var observedProgress: Float { + get { + return observedProgressView.progress + } + set { + observedProgressView.progress = newValue + observedProgressView.isHidden = observedProgress <= 0 + } + } + + var observedValueText: String? { + get { + return observedValueLabel.text + } + set { + observedValueLabel.text = newValue + if newValue != nil { + observedValueLabel.superview?.isHidden = false + } + } + } + + var observedDateText: String? { + get { + return observedDateLabel.text + } + set { + observedDateLabel.text = newValue + if newValue != nil { + observedDateLabel.superview?.isHidden = false + } + } + } + + var observedValueTextColor: UIColor { + get { + return observedValueLabel.textColor + } + set { + observedValueLabel.textColor = newValue + } + } + + var observedDateTextColor: UIColor { + get { + return observedDateLabel.textColor + } + set { + observedDateLabel.textColor = newValue + } + } + + var isUploading = false { + didSet { + uploadingIndicator.isHidden = !isUploading + } + } + + override func layoutSubviews() { + super.layoutSubviews() + + contentView.layoutMargins.left = separatorInset.left + contentView.layoutMargins.right = separatorInset.left + } + + override func awakeFromNib() { + super.awakeFromNib() + + resetViews() + } + + override func prepareForReuse() { + super.prepareForReuse() + + resetViews() + } + + private func resetViews() { + observedProgress = 0 + clampedProgress = 0 + valueLabel.text = nil + dateLabel.text = nil + observedValueText = nil + observedDateText = nil + observedValueLabel.superview?.isHidden = true + uploadingIndicator.isHidden = true + } +} diff --git a/Loop/Views/ChartTableViewCell.swift b/Loop/Views/ChartTableViewCell.swift index 049023f2a9..a283c9fc60 100644 --- a/Loop/Views/ChartTableViewCell.swift +++ b/Loop/Views/ChartTableViewCell.swift @@ -12,7 +12,7 @@ import LoopUI final class ChartTableViewCell: UITableViewCell { - @IBOutlet weak var chartContentView: ChartContentView! + @IBOutlet weak var chartContentView: ChartContainerView! @IBOutlet weak var titleLabel: UILabel? diff --git a/Loop/Views/CircleMaskView.swift b/Loop/Views/CircleMaskView.swift new file mode 100644 index 0000000000..3b4c8898ef --- /dev/null +++ b/Loop/Views/CircleMaskView.swift @@ -0,0 +1,18 @@ +// +// CircleMaskView.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit + +class CircleMaskView: UIView { + + override func layoutSubviews() { + super.layoutSubviews() + + self.layer.cornerRadius = self.frame.height / 2 + } + +} diff --git a/Loop/Views/HeaderValuesTableViewCell.swift b/Loop/Views/HeaderValuesTableViewCell.swift new file mode 100644 index 0000000000..1b4c974dce --- /dev/null +++ b/Loop/Views/HeaderValuesTableViewCell.swift @@ -0,0 +1,19 @@ +// +// HeaderValuesTableViewCell.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import UIKit + +class HeaderValuesTableViewCell: UITableViewCell { + + @IBOutlet weak var COBValueLabel: UILabel! + + @IBOutlet weak var COBDateLabel: UILabel! + + @IBOutlet weak var totalValueLabel: UILabel! + + @IBOutlet weak var totalDateLabel: UILabel! +} diff --git a/Loop/Views/PredictionInputEffectTableViewCell.swift b/Loop/Views/PredictionInputEffectTableViewCell.swift index e9736b4708..567a043f8c 100644 --- a/Loop/Views/PredictionInputEffectTableViewCell.swift +++ b/Loop/Views/PredictionInputEffectTableViewCell.swift @@ -14,6 +14,13 @@ class PredictionInputEffectTableViewCell: UITableViewCell { @IBOutlet weak var subtitleLabel: UILabel! + override func layoutSubviews() { + super.layoutSubviews() + + contentView.layoutMargins.left = separatorInset.left + contentView.layoutMargins.right = separatorInset.left + } + var enabled: Bool = true { didSet { if enabled { diff --git a/LoopTests/Info.plist b/LoopTests/Info.plist index 9b64b457c6..c48e13f347 100644 --- a/LoopTests/Info.plist +++ b/LoopTests/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/LoopTests/KeychainManagerTests.swift b/LoopTests/KeychainManagerTests.swift index 5035c399f3..4195fae0bf 100644 --- a/LoopTests/KeychainManagerTests.swift +++ b/LoopTests/KeychainManagerTests.swift @@ -15,10 +15,10 @@ class KeychainManagerTests: XCTestCase { func testInvalidData() throws { let manager = KeychainManager() - try manager.setDexcomShareUsername(nil, password: "foo") + try manager.setDexcomShareUsername(nil, password: "foo", url: URL(string: "https://share1.dexcom.com")!) XCTAssertNil(manager.getDexcomShareCredentials()) - try manager.setDexcomShareUsername("foo", password: nil) + try manager.setDexcomShareUsername("foo", password: nil, url: URL(string: "https://share1.dexcom.com")!) XCTAssertNil(manager.getDexcomShareCredentials()) manager.setNightscoutURL(nil, secret: "foo") @@ -37,12 +37,13 @@ class KeychainManagerTests: XCTestCase { try manager.setAmplitudeAPIKey(nil) XCTAssertNil(manager.getAmplitudeAPIKey()) - try manager.setDexcomShareUsername("sugarman", password: "rodriguez") + try manager.setDexcomShareUsername("sugarman", password: "rodriguez", url: URL(string: "https://share1.dexcom.com")!) let dexcomCredentials = manager.getDexcomShareCredentials()! XCTAssertEqual("sugarman", dexcomCredentials.username) XCTAssertEqual("rodriguez", dexcomCredentials.password) + XCTAssertEqual("https://share1.dexcom.com", dexcomCredentials.url.absoluteString) - try manager.setDexcomShareUsername(nil, password: nil) + try manager.setDexcomShareUsername(nil, password: nil, url: nil) XCTAssertNil(manager.getDexcomShareCredentials()) manager.setNightscoutURL(URL(string: "http://mysite.azurewebsites.net")!, secret: "ABCDEFG") diff --git a/LoopUI/Info.plist b/LoopUI/Info.plist index 3e57c2d8b8..8e70af7782 100644 --- a/LoopUI/Info.plist +++ b/LoopUI/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass diff --git a/LoopUI/Managers/StatusChartsManager.swift b/LoopUI/Managers/StatusChartsManager.swift index 3f2325b9c6..613e679e93 100644 --- a/LoopUI/Managers/StatusChartsManager.swift +++ b/LoopUI/Managers/StatusChartsManager.swift @@ -68,11 +68,15 @@ public final class StatusChartsManager { basalDosePoints = [] bolusDosePoints = [] allDosePoints = [] + carbEffectPoints = [] + insulinCounteractionEffectPoints = [] + allCarbEffectPoints = [] glucoseChartCache = nil iobChartCache = nil cobChartCache = nil doseChartCache = nil + carbEffectChartCache = nil } // MARK: - Data @@ -83,7 +87,8 @@ public final class StatusChartsManager { if startDate != oldValue { xAxisValues = nil - updateEndDate(startDate.addingTimeInterval(TimeInterval(hours: 4))) + // Set a new minimum end date + endDate = startDate.addingTimeInterval(.hours(3)) } } } @@ -318,25 +323,27 @@ public final class StatusChartsManager { let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame) // The glucose targets - var targetLayer: ChartPointsAreaLayer? = nil - - if targetGlucosePoints.count > 1 { - let alpha: CGFloat = targetOverridePoints.count > 1 ? 0.15 : 0.3 - - targetLayer = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: targetGlucosePoints, areaColor: colors.glucoseTint.withAlphaComponent(alpha), animDuration: 0, animDelay: 0, addContainerPoints: false) - } - - var targetOverrideLayer: ChartPointsAreaLayer? = nil - - if targetOverridePoints.count > 1 { - targetOverrideLayer = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: targetOverridePoints, areaColor: colors.glucoseTint.withAlphaComponent(0.3), animDuration: 0, animDelay: 0, addContainerPoints: false) - } - - var targetOverrideDurationLayer: ChartPointsAreaLayer? = nil - - if targetOverrideDurationPoints.count > 1 { - targetOverrideDurationLayer = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: targetOverrideDurationPoints, areaColor: colors.glucoseTint.withAlphaComponent(0.3), animDuration: 0, animDelay: 0, addContainerPoints: false) - } + let targetsLayer = ChartPointsFillsLayer( + xAxis: xAxisLayer.axis, + yAxis: yAxisLayer.axis, + fills: [ + ChartPointsFill( + chartPoints: targetGlucosePoints, + fillColor: colors.glucoseTint.withAlphaComponent(targetOverridePoints.count > 1 ? 0.15 : 0.3), + createContainerPoints: false + ), + ChartPointsFill( + chartPoints: targetOverridePoints, + fillColor: colors.glucoseTint.withAlphaComponent(0.3), + createContainerPoints: false + ), + ChartPointsFill( + chartPoints: targetOverrideDurationPoints, + fillColor: colors.glucoseTint.withAlphaComponent(0.3), + createContainerPoints: false + ) + ] + ) // Grid lines let gridLayer = ChartGuideLinesForValuesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, settings: guideLinesLayerSettings, axisValuesX: Array(xAxisValues.dropFirst().dropLast()), axisValuesY: yAxisValues) @@ -383,9 +390,7 @@ public final class StatusChartsManager { let layers: [ChartLayer?] = [ gridLayer, - targetLayer, - targetOverrideLayer, - targetOverrideDurationLayer, + targetsLayer, xAxisLayer, yAxisLayer, glucoseChartCache?.highlightLayer, @@ -414,17 +419,6 @@ public final class StatusChartsManager { return nil } - var containerPoints = iobPoints - - // Create a container line at 0 - if let first = iobPoints.first { - containerPoints.insert(ChartPoint(x: first.x, y: ChartAxisValueInt(0)), at: 0) - } - - if let last = iobPoints.last { - containerPoints.append(ChartPoint(x: last.x, y: ChartAxisValueInt(0))) - } - let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(iobPoints + iobDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: 0.5, axisValueGenerator: { ChartAxisValueDouble($0, labelSettings: self.axisLabelSettings) }, addPaddingSegmentIfEdge: false) let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY)) @@ -437,13 +431,7 @@ public final class StatusChartsManager { let lineModel = ChartLineModel(chartPoints: iobPoints, lineColor: UIColor.IOBTintColor, lineWidth: 2, animDuration: 0, animDelay: 0) let iobLine = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel]) - let iobArea: ChartPointsAreaLayer? - - if containerPoints.count > 1 { - iobArea = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: containerPoints, areaColor: UIColor.IOBTintColor.withAlphaComponent(0.5), animDuration: 0, animDelay: 0, addContainerPoints: false) - } else { - iobArea = nil - } + let iobArea = ChartPointsFillsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, fills: [ChartPointsFill(chartPoints: iobPoints, fillColor: UIColor.IOBTintColor.withAlphaComponent(0.5))]) // Grid lines let gridLayer = ChartGuideLinesForValuesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, settings: guideLinesLayerSettings, axisValuesX: Array(xAxisValues.dropFirst().dropLast()), axisValuesY: yAxisValues) @@ -500,17 +488,6 @@ public final class StatusChartsManager { return nil } - var containerPoints = cobPoints - - // Create a container line at 0 - if let first = cobPoints.first { - containerPoints.insert(ChartPoint(x: first.x, y: ChartAxisValueInt(0)), at: 0) - } - - if let last = cobPoints.last { - containerPoints.append(ChartPoint(x: last.x, y: ChartAxisValueInt(0))) - } - let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(cobPoints + cobDisplayRangePoints, minSegmentCount: 2, maxSegmentCount: 3, multiple: 10, axisValueGenerator: { ChartAxisValueDouble($0, labelSettings: self.axisLabelSettings) }, addPaddingSegmentIfEdge: false) let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY)) @@ -523,13 +500,7 @@ public final class StatusChartsManager { let lineModel = ChartLineModel(chartPoints: cobPoints, lineColor: UIColor.COBTintColor, lineWidth: 2, animDuration: 0, animDelay: 0) let cobLine = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel]) - let cobArea: ChartPointsAreaLayer? - - if containerPoints.count > 0 { - cobArea = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: containerPoints, areaColor: UIColor.COBTintColor.withAlphaComponent(0.5), animDuration: 0, animDelay: 0, addContainerPoints: false) - } else { - cobArea = nil - } + let cobArea = ChartPointsFillsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, fills: [ChartPointsFill(chartPoints: cobPoints, fillColor: UIColor.COBTintColor.withAlphaComponent(0.5))]) // Grid lines let gridLayer = ChartGuideLinesForValuesLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, settings: guideLinesLayerSettings, axisValuesX: Array(xAxisValues.dropFirst().dropLast()), axisValuesY: yAxisValues) @@ -588,13 +559,15 @@ public final class StatusChartsManager { let lineModel = ChartLineModel(chartPoints: basalDosePoints, lineColor: colors.doseTint, lineWidth: 2, animDuration: 0, animDelay: 0) let doseLine = ChartPointsLineLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, lineModels: [lineModel]) - let doseArea: ChartPointsAreaLayer? - - if basalDosePoints.count > 1 { - doseArea = ChartPointsAreaLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: basalDosePoints, areaColor: colors.doseTint.withAlphaComponent(0.5), animDuration: 0, animDelay: 0, addContainerPoints: false) - } else { - doseArea = nil - } + let doseArea = ChartPointsFillsLayer( + xAxis: xAxisLayer.axis, + yAxis: yAxisLayer.axis, + fills: [ChartPointsFill( + chartPoints: basalDosePoints, + fillColor: colors.doseTint.withAlphaComponent(0.5), + createContainerPoints: false + )] + ) let bolusLayer: ChartPointsScatterDownTrianglesLayer? @@ -643,6 +616,142 @@ public final class StatusChartsManager { return Chart(frame: frame, innerFrame: innerFrame, settings: chartSettings, layers: layers.flatMap { $0 }) } + // MARK: - Carb Effect + + /// The chart points for expected carb effect velocity + public var carbEffectPoints: [ChartPoint] = [] { + didSet { + carbEffectChart = nil + // don't extend the end date for carb effects + } + } + + /// The chart points for observed insulin counteraction effect velocity + public var insulinCounteractionEffectPoints: [ChartPoint] = [] { + didSet { + carbEffectChart = nil + + // Extend 1 hour past the seen effect to ensure some future prediction is displayed + if let lastDate = insulinCounteractionEffectPoints.last?.x as? ChartAxisValueDate { + updateEndDate(lastDate.date.addingTimeInterval(.hours(1))) + } + } + } + + /// The chart points used for selection in the carb effect chart + public var allCarbEffectPoints: [ChartPoint] = [] { + didSet { + carbEffectChart = nil + } + } + + private var carbEffectChart: Chart? + + private var carbEffectChartCache: ChartPointsTouchHighlightLayerViewCache? + + public func carbEffectChartWithFrame(_ frame: CGRect) -> Chart? { + if let chart = carbEffectChart, chart.frame != frame { + self.carbEffectChart = nil + } + + if carbEffectChart == nil { + carbEffectChart = generateCarbEffectChartWithFrame(frame) + } + + return carbEffectChart + } + + private func generateCarbEffectChartWithFrame(_ frame: CGRect) -> Chart? { + guard let xAxisModel = xAxisModel, let xAxisValues = xAxisValues else { + return nil + } + + /// The minimum range to display for carb effect values. + let carbEffectDisplayRangePoints: [ChartPoint] = [0, glucoseUnit.glucoseUnitYAxisSegmentSize / 25.0].map { + return ChartPoint( + x: ChartAxisValue(scalar: 0), + y: ChartAxisValueDouble($0) + ) + } + + let yAxisValues = ChartAxisValuesStaticGenerator.generateYAxisValuesWithChartPoints(carbEffectPoints + allCarbEffectPoints + carbEffectDisplayRangePoints, + minSegmentCount: 2, + maxSegmentCount: 4, + multiple: glucoseUnit.glucoseUnitYAxisSegmentSize / 50, + axisValueGenerator: { + ChartAxisValueDouble($0, labelSettings: self.axisLabelSettings) + }, + addPaddingSegmentIfEdge: false + ) + + let yAxisModel = ChartAxisModel(axisValues: yAxisValues, lineColor: colors.axisLine, labelSpaceReservationMode: .fixed(labelsWidthY)) + + let coordsSpace = ChartCoordsSpaceLeftBottomSingleAxis(chartSettings: chartSettings, chartFrame: frame, xModel: xAxisModel, yModel: yAxisModel) + + let (xAxisLayer, yAxisLayer, innerFrame) = (coordsSpace.xAxisLayer, coordsSpace.yAxisLayer, coordsSpace.chartInnerFrame) + + let carbFillColor = UIColor.COBTintColor.withAlphaComponent(0.8) + + // Carb effect + let effectsLayer = ChartPointsFillsLayer( + xAxis: xAxisLayer.axis, + yAxis: yAxisLayer.axis, + fills: [ + ChartPointsFill(chartPoints: carbEffectPoints, fillColor: UIColor.secondaryLabelColor.withAlphaComponent(0.5)), + ChartPointsFill(chartPoints: insulinCounteractionEffectPoints, fillColor: carbFillColor, blendMode: .colorBurn) + ] + ) + + // Grid lines + let gridLayer = ChartGuideLinesForValuesLayer( + xAxis: xAxisLayer.axis, + yAxis: yAxisLayer.axis, + settings: guideLinesLayerSettings, + axisValuesX: Array(xAxisValues.dropFirst().dropLast()), + axisValuesY: yAxisValues + ) + + // 0-line + let dummyZeroChartPoint = ChartPoint(x: ChartAxisValueDouble(0), y: ChartAxisValueDouble(0)) + let zeroGuidelineLayer = ChartPointsViewsLayer(xAxis: xAxisLayer.axis, yAxis: yAxisLayer.axis, chartPoints: [dummyZeroChartPoint], viewGenerator: {(chartPointModel, layer, chart) -> UIView? in + let width: CGFloat = 1 + let viewFrame = CGRect(x: chart.contentView.bounds.minX, y: chartPointModel.screenLoc.y - width / 2, width: chart.contentView.bounds.size.width, height: width) + + let v = UIView(frame: viewFrame) + v.backgroundColor = carbFillColor + return v + }) + + if gestureRecognizer != nil { + carbEffectChartCache = ChartPointsTouchHighlightLayerViewCache( + xAxisLayer: xAxisLayer, + yAxisLayer: yAxisLayer, + axisLabelSettings: self.axisLabelSettings, + chartPoints: allCarbEffectPoints, + tintColor: UIColor.COBTintColor, + gestureRecognizer: gestureRecognizer + ) + } + + let layers: [ChartLayer?] = [ + gridLayer, + xAxisLayer, + yAxisLayer, + zeroGuidelineLayer, + carbEffectChartCache?.highlightLayer, + effectsLayer + ] + + return Chart( + frame: frame, + innerFrame: innerFrame, + settings: chartSettings, + layers: layers.flatMap { $0 } + ) + } + + // MARK: - Shared Axis + private func generateXAxisValues() { let timeFormatter = DateFormatter() timeFormatter.dateFormat = "h a" diff --git a/LoopUI/Views/ChartContentView.swift b/LoopUI/Views/ChartContainerView.swift similarity index 94% rename from LoopUI/Views/ChartContentView.swift rename to LoopUI/Views/ChartContainerView.swift index dcc3d7d3ec..a0b322b7eb 100644 --- a/LoopUI/Views/ChartContentView.swift +++ b/LoopUI/Views/ChartContainerView.swift @@ -1,5 +1,5 @@ // -// ChartContentView.swift +// ChartContainerView.swift // Loop // // Created by Nate Racklyeft on 9/14/16. @@ -8,7 +8,7 @@ import UIKit -public class ChartContentView: UIView { +public class ChartContainerView: UIView { override public func layoutSubviews() { super.layoutSubviews() diff --git a/LoopUI/Views/ChartPointsContextFillLayer.swift b/LoopUI/Views/ChartPointsContextFillLayer.swift new file mode 100644 index 0000000000..a2824336ce --- /dev/null +++ b/LoopUI/Views/ChartPointsContextFillLayer.swift @@ -0,0 +1,121 @@ +// +// ChartPointsContextFillLayer.swift +// Loop +// +// Copyright © 2017 LoopKit Authors. All rights reserved. +// + +import SwiftCharts + + +struct ChartPointsFill { + let chartPoints: [ChartPoint] + let fillColor: UIColor + let createContainerPoints: Bool + let blendMode: CGBlendMode + fileprivate var screenPoints: [CGPoint] = [] + + init?(chartPoints: [ChartPoint], fillColor: UIColor, createContainerPoints: Bool = true, blendMode: CGBlendMode = .normal) { + guard chartPoints.count > 1 else { + return nil; + } + + var chartPoints = chartPoints + + if createContainerPoints { + // Create a container line at value position 0 + if let first = chartPoints.first { + chartPoints.insert(ChartPoint(x: first.x, y: ChartAxisValueInt(0)), at: 0) + } + + if let last = chartPoints.last { + chartPoints.append(ChartPoint(x: last.x, y: ChartAxisValueInt(0))) + } + } + + self.chartPoints = chartPoints + self.fillColor = fillColor + self.createContainerPoints = createContainerPoints + self.blendMode = blendMode + } + + var areaPath: UIBezierPath { + let path = UIBezierPath() + + if let point = screenPoints.first { + path.move(to: point) + } + + for point in screenPoints.dropFirst() { + path.addLine(to: point) + } + + return path + } +} + + +final class ChartPointsFillsLayer: ChartCoordsSpaceLayer { + let fills: [ChartPointsFill] + + init?(xAxis: ChartAxis, yAxis: ChartAxis, fills: [ChartPointsFill?]) { + self.fills = fills.flatMap({ $0 }) + + guard fills.count > 0 else { + return nil + } + + super.init(xAxis: xAxis, yAxis: yAxis) + } + + override func chartInitialized(chart: Chart) { + super.chartInitialized(chart: chart) + + let view = ChartPointsFillsView( + frame: chart.bounds, + chartPointsFills: fills.map { (fill) -> ChartPointsFill in + var fill = fill + + fill.screenPoints = fill.chartPoints.map { (point) -> CGPoint in + return modelLocToScreenLoc(x: point.x.scalar, y: point.y.scalar) + } + + return fill + } + ) + + chart.addSubview(view) + } +} + + +class ChartPointsFillsView: UIView { + let chartPointsFills: [ChartPointsFill] + var allowsAntialiasing = false + + init(frame: CGRect, chartPointsFills: [ChartPointsFill]) { + self.chartPointsFills = chartPointsFills + + super.init(frame: frame) + + backgroundColor = .clear + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { return } + + context.saveGState() + context.setAllowsAntialiasing(allowsAntialiasing) + + for fill in chartPointsFills { + context.setFillColor(fill.fillColor.cgColor) + fill.areaPath.fill(with: fill.blendMode, alpha: 1) + } + + context.restoreGState() + } +} diff --git a/LoopUI/Views/ChartPointsTouchHighlightLayerViewCache.swift b/LoopUI/Views/ChartPointsTouchHighlightLayerViewCache.swift index 3dfd55d40e..7da757f9fb 100644 --- a/LoopUI/Views/ChartPointsTouchHighlightLayerViewCache.swift +++ b/LoopUI/Views/ChartPointsTouchHighlightLayerViewCache.swift @@ -19,7 +19,11 @@ final class ChartPointsTouchHighlightLayerViewCache { private lazy var labelY: UILabel = { let label = UILabel() - label.font = UIFont.monospacedDigitSystemFont(ofSize: 15, weight: UIFontWeightBold) + #if swift(>=4) + label.font = UIFont.monospacedDigitSystemFont(ofSize: 15, weight: UIFont.Weight.bold) + #else + label.font = UIFont.monospacedDigitSystemFont(ofSize: 15, weight: UIFontWeightBold) + #endif return label }() diff --git a/README.md b/README.md index fc98685c7d..b7680a9698 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Loop for iOS -![App Icon](/Loop/Assets.xcassets/AppIcon.appiconset/Icon-40%402x.png?raw=true) ![WatchApp Icon](/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-40%402x.png?raw=true) +![App Icon](/Loop/Assets.xcassets/AppIcon.appiconset/Icon-Small-40%402x.png?raw=true) [![Build Status](https://travis-ci.org/LoopKit/Loop.svg?branch=master)](https://travis-ci.org/LoopKit/Loop) [![Join the chat at https://gitter.im/LoopKit/Loop](https://badges.gitter.im/LoopKit/Loop.svg)](https://gitter.im/LoopKit/Loop?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift index 1ac3c89faa..18a0ccf1b0 100644 --- a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift +++ b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift @@ -30,17 +30,17 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas private var absorptionTime = AbsorptionTimeType.medium { didSet { - absorptionButtonA.setBackgroundColor(UIColor.darkTintColor) - absorptionButtonB.setBackgroundColor(UIColor.darkTintColor) - absorptionButtonC.setBackgroundColor(UIColor.darkTintColor) + absorptionButtonA.setBackgroundColor(UIColor.darkCarbsColor) + absorptionButtonB.setBackgroundColor(UIColor.darkCarbsColor) + absorptionButtonC.setBackgroundColor(UIColor.darkCarbsColor) switch absorptionTime { case .fast: - absorptionButtonA.setBackgroundColor(UIColor.tintColor) + absorptionButtonA.setBackgroundColor(UIColor.carbsColor) case .medium: - absorptionButtonB.setBackgroundColor(UIColor.tintColor) + absorptionButtonB.setBackgroundColor(UIColor.carbsColor) case .slow: - absorptionButtonC.setBackgroundColor(UIColor.tintColor) + absorptionButtonC.setBackgroundColor(UIColor.carbsColor) } } } @@ -134,7 +134,7 @@ extension AddCarbsInterfaceController: WKCrownDelegate { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerCarb) - carbValue += Int((accumulatedRotation - remainder).divided(by: rotationsPerCarb)) + carbValue += Int((accumulatedRotation - remainder) / rotationsPerCarb) accumulatedRotation = remainder } } diff --git a/WatchApp Extension/Controllers/BolusInterfaceController.swift b/WatchApp Extension/Controllers/BolusInterfaceController.swift index 0dfd6a2444..23c46fc12d 100644 --- a/WatchApp Extension/Controllers/BolusInterfaceController.swift +++ b/WatchApp Extension/Controllers/BolusInterfaceController.swift @@ -165,7 +165,7 @@ extension BolusInterfaceController: WKCrownDelegate { accumulatedRotation += rotationalDelta let remainder = accumulatedRotation.truncatingRemainder(dividingBy: rotationsPerValue) - pickerValue += Int((accumulatedRotation - remainder).divided(by: rotationsPerValue)) + pickerValue += Int((accumulatedRotation - remainder) / rotationsPerValue) accumulatedRotation = remainder } } diff --git a/WatchApp Extension/Controllers/StatusInterfaceController.swift b/WatchApp Extension/Controllers/StatusInterfaceController.swift index 8aac1f18b1..641be0d72f 100644 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ b/WatchApp Extension/Controllers/StatusInterfaceController.swift @@ -12,7 +12,6 @@ import Foundation final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { - @IBOutlet weak var graphImage: WKInterfaceImage! @IBOutlet weak var loopHUDImage: WKInterfaceImage! @IBOutlet weak var loopTimer: WKInterfaceTimer! @IBOutlet weak var glucoseLabel: WKInterfaceLabel! @@ -74,7 +73,6 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { // TODO: Other elements statusLabel.setHidden(true) - graphImage.setHidden(true) } // MARK: - Menu Items @@ -87,4 +85,5 @@ final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { presentController(withName: BolusInterfaceController.className, context: lastContext?.bolusSuggestion) } + } diff --git a/WatchApp Extension/Extensions/UIColor.swift b/WatchApp Extension/Extensions/UIColor.swift index 8f21edbcee..3995e6e60a 100644 --- a/WatchApp Extension/Extensions/UIColor.swift +++ b/WatchApp Extension/Extensions/UIColor.swift @@ -10,9 +10,11 @@ import UIKit extension UIColor { - @nonobjc static let tintColor = UIColor.HIGOrangeColor() + @nonobjc static let tintColor = UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1) - @nonobjc static let darkTintColor = UIColor.HIGOrangeColorDark() + @nonobjc static let carbsColor = UIColor(red: 99 / 255, green: 218 / 255, blue: 56 / 255, alpha: 1) + + @nonobjc static let darkCarbsColor = UIColor.carbsColor.withAlphaComponent(0.14) // MARK: - HIG colors // See: https://developer.apple.com/watch/human-interface-guidelines/visual-design/#color diff --git a/WatchApp Extension/Info.plist b/WatchApp Extension/Info.plist index 5d2fdc31ed..20d35fdbcf 100644 --- a/WatchApp Extension/Info.plist +++ b/WatchApp Extension/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleSignature ???? CFBundleVersion diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Home Screen.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Home Screen.png new file mode 100644 index 0000000000..b9b45b2865 Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Home Screen.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Notification.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Notification.png new file mode 100644 index 0000000000..5027b37e16 Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Notification.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Short Look.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Short Look.png new file mode 100644 index 0000000000..042d05fdc2 Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/38mm Short Look.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Notification.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Notification.png new file mode 100644 index 0000000000..651a4530db Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Notification.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Short Look.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Short Look.png new file mode 100644 index 0000000000..9b7cd90921 Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/42mm Short Look.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json index fb7e4851fa..7c1352bce2 100644 --- a/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/WatchApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -3,7 +3,7 @@ { "size" : "24x24", "idiom" : "watch", - "filename" : "watch-24@2x.png", + "filename" : "38mm Notification.png", "scale" : "2x", "role" : "notificationCenter", "subtype" : "38mm" @@ -11,7 +11,7 @@ { "size" : "27.5x27.5", "idiom" : "watch", - "filename" : "watch-27.5@2x.png", + "filename" : "42mm Notification.png", "scale" : "2x", "role" : "notificationCenter", "subtype" : "42mm" @@ -19,21 +19,21 @@ { "size" : "29x29", "idiom" : "watch", - "filename" : "watch-29@2x.png", + "filename" : "Icon-Small@2x.png", "role" : "companionSettings", "scale" : "2x" }, { "size" : "29x29", "idiom" : "watch", - "filename" : "watch-29@3x.png", + "filename" : "Icon-Small@3x.png", "role" : "companionSettings", "scale" : "3x" }, { "size" : "40x40", "idiom" : "watch", - "filename" : "watch-40@2x.png", + "filename" : "38mm Home Screen.png", "scale" : "2x", "role" : "appLauncher", "subtype" : "38mm" @@ -41,7 +41,7 @@ { "size" : "86x86", "idiom" : "watch", - "filename" : "watch-86@2x.png", + "filename" : "38mm Short Look.png", "scale" : "2x", "role" : "quickLook", "subtype" : "38mm" @@ -49,7 +49,7 @@ { "size" : "98x98", "idiom" : "watch", - "filename" : "watch-98@2x.png", + "filename" : "42mm Short Look.png", "scale" : "2x", "role" : "quickLook", "subtype" : "42mm" diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 0000000000..02e28324c5 Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 0000000000..3659954b6e Binary files /dev/null and b/WatchApp/Assets.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-24@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-24@2x.png deleted file mode 100644 index 69885337ae..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-24@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-27.5@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-27.5@2x.png deleted file mode 100644 index a9660e4a04..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-27.5@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@2x.png deleted file mode 100644 index a933fd9d42..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@3x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@3x.png deleted file mode 100644 index 0e006e094d..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-29@3x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-40@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-40@2x.png deleted file mode 100644 index 45b19baed7..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-40@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-86@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-86@2x.png deleted file mode 100644 index f2f80b5460..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-86@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-98@2x.png b/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-98@2x.png deleted file mode 100644 index 2064ccdf8c..0000000000 Binary files a/WatchApp/Assets.xcassets/AppIcon.appiconset/watch-98@2x.png and /dev/null differ diff --git a/WatchApp/Assets.xcassets/bolus.imageset/Contents.json b/WatchApp/Assets.xcassets/bolus.imageset/Contents.json index 72322c7055..de9ac511db 100644 --- a/WatchApp/Assets.xcassets/bolus.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/bolus.imageset/Contents.json @@ -20,5 +20,8 @@ "info" : { "version" : 1, "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" } } \ No newline at end of file diff --git a/WatchApp/Assets.xcassets/fork.imageset/Contents.json b/WatchApp/Assets.xcassets/fork.imageset/Contents.json index e24ede6f2c..6888857814 100644 --- a/WatchApp/Assets.xcassets/fork.imageset/Contents.json +++ b/WatchApp/Assets.xcassets/fork.imageset/Contents.json @@ -20,5 +20,8 @@ "info" : { "version" : 1, "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" } } \ No newline at end of file diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index 6cccfe1bb8..c37f64fa8b 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,6 +1,6 @@ - - + + @@ -21,8 +21,8 @@ + + + + + + + + + + + - - - - - - - - - - - - - - - @@ -256,5 +286,5 @@ - + diff --git a/WatchApp/Info.plist b/WatchApp/Info.plist index 96ae3c6900..f40aa2dd5e 100644 --- a/WatchApp/Info.plist +++ b/WatchApp/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.3 + 1.4.0 CFBundleSignature ???? CFBundleVersion