From 1c8b1c6c6a3cc23190e71034d79aa392267a3067 Mon Sep 17 00:00:00 2001 From: Nathan Racklyeft Date: Thu, 22 Sep 2016 22:17:41 -0700 Subject: [PATCH 01/14] Migrate to UserNotifications.framework for iOS 10 (#175) * Migrate to UserNotifications.framework (WIP) * iOS 10 UserNotifications.framework Fixes #170 --- Loop.xcodeproj/project.pbxproj | 4 +- Loop/AppDelegate.swift | 35 ++-- Loop/Managers/DeviceDataManager.swift | 19 +-- Loop/Managers/NotificationManager.swift | 161 ++++++++++-------- Loop/Managers/WatchDataManager.swift | 2 +- .../StatusTableViewController.swift | 4 +- 6 files changed, 122 insertions(+), 103 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index d82c739941..915a7a62c5 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -1233,7 +1233,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MAIN_APP_BUNDLE_IDENTIFIER = "$(inherited).Loop"; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -1279,7 +1279,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MAIN_APP_BUNDLE_IDENTIFIER = "$(inherited).Loop"; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index c0610119a1..f2f0868f3c 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -7,6 +7,7 @@ // import UIKit +import UserNotifications import CarbKit import InsulinKit @@ -20,7 +21,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { window?.tintColor = UIColor.tintColor - NotificationManager.authorize() + NotificationManager.authorize(delegate: self) AnalyticsManager.sharedManager.application(application, didFinishLaunchingWithOptions: launchOptions) @@ -60,27 +61,25 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } - // MARK: - Notifications + // MARK: - 3D Touch - func application(_ application: UIApplication, didReceive notification: UILocalNotification) { - if application.applicationState == .active { - if let message = notification.alertBody { - window?.rootViewController?.presentAlertController(withTitle: notification.alertTitle, message: message, animated: true, completion: nil) - } - } + func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { + completionHandler(false) } +} - func application(_ application: UIApplication, handleActionWithIdentifier identifier: String?, for notification: UILocalNotification, withResponseInfo responseInfo: [AnyHashable: Any], completionHandler: @escaping () -> Void) { - switch identifier { - case NotificationManager.Action.RetryBolus.rawValue?: - if let units = notification.userInfo?[NotificationManager.UserInfoKey.BolusAmount.rawValue] as? Double, - let startDate = notification.userInfo?[NotificationManager.UserInfoKey.BolusStartDate.rawValue] as? Date, +extension AppDelegate: UNUserNotificationCenterDelegate { + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + switch response.actionIdentifier { + case NotificationManager.Action.RetryBolus.rawValue: + if let units = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusAmount.rawValue] as? Double, + let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusStartDate.rawValue] as? Date, startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) { AnalyticsManager.sharedManager.didRetryBolus() - dataManager.enactBolus(units) { (error) in + dataManager.enactBolus(units: units) { (error) in if error != nil { NotificationManager.sendBolusFailureNotificationForAmount(units, atStartDate: startDate) } @@ -92,13 +91,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { default: break } - + completionHandler() } - // MARK: - 3D Touch - - func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { - completionHandler(false) + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.badge, .sound, .alert]) } } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 673bb7496a..91c8fe62fb 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -114,7 +114,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter AnalyticsManager.sharedManager.didChangeRileyLinkConnectionState() if connectedPeripheralIDs.count == 0 { - NotificationManager.clearLoopNotRunningNotifications() + NotificationManager.clearPendingNotificationRequests() } } @@ -360,13 +360,12 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } } - /** - Send a bolus command and handle the result - - - parameter completion: A closure called after the command is complete. This closure takes a single argument: - - error: An error describing why the command failed - */ - func enactBolus(_ units: Double, completion: @escaping (_ error: Error?) -> Void) { + /// Send a bolus command and handle the result + /// + /// - parameter units: The number of units to deliver + /// - parameter completion: A clsure called after the command is complete. This closure takes a single argument: + /// - error: An error describing why the command failed + func enactBolus(units: Double, completion: @escaping (_ error: Error?) -> Void) { guard units > 0 else { completion(nil) return @@ -442,9 +441,7 @@ final class DeviceDataManager: CarbStoreDelegate, DoseStoreDelegate, Transmitter } // MARK: - G5 Transmitter - /** - The G5 transmitter is a reliable heartbeat by which we can assert the loop state. - */ + /// The G5 transmitter is a reliable heartbeat by which we can assert the loop state. // MARK: TransmitterDelegate diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index 47c729385c..6667183cd7 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -7,6 +7,7 @@ // import UIKit +import UserNotifications struct NotificationManager { @@ -27,43 +28,44 @@ struct NotificationManager { case BolusStartDate } - static var userNotificationSettings: UIUserNotificationSettings { - let retryBolusAction = UIMutableUserNotificationAction() - retryBolusAction.title = NSLocalizedString("Retry", comment: "The title of the notification action to retry a bolus command") - retryBolusAction.identifier = Action.RetryBolus.rawValue - retryBolusAction.activationMode = .background - - let bolusFailureCategory = UIMutableUserNotificationCategory() - bolusFailureCategory.identifier = Category.BolusFailure.rawValue - bolusFailureCategory.setActions([ - retryBolusAction - ], - for: .default - ) + private static var notificationCategories: Set { + var categories = [UNNotificationCategory]() - return UIUserNotificationSettings( - types: [.badge, .sound, .alert], - categories: [ - bolusFailureCategory - ] + let retryBolusAction = UNNotificationAction( + identifier: Action.RetryBolus.rawValue, + title: NSLocalizedString("Retry", comment: "The title of the notification action to retry a bolus command"), + options: [] ) + + categories.append(UNNotificationCategory( + identifier: Category.BolusFailure.rawValue, + actions: [retryBolusAction], + intentIdentifiers: [], + options: [] + )) + + return Set(categories) } - static func authorize() { - UIApplication.shared.registerUserNotificationSettings(userNotificationSettings) + static func authorize(delegate: UNUserNotificationCenterDelegate) { + let center = UNUserNotificationCenter.current() + + center.delegate = delegate + center.requestAuthorization(options: [.badge, .sound, .alert], completionHandler: { _, _ in }) + center.setNotificationCategories(notificationCategories) } // MARK: - Notifications static func sendBolusFailureNotificationForAmount(_ units: Double, atStartDate startDate: Date) { - let notification = UILocalNotification() + let notification = UNMutableNotificationContent() - notification.alertTitle = NSLocalizedString("Bolus", comment: "The notification title for a bolus failure") - notification.alertBody = String(format: NSLocalizedString("%@ U bolus may have failed.", comment: "The notification alert describing a possible bolus failure. The substitution parameter is the size of the bolus in units."), NumberFormatter.localizedString(from: NSNumber(value: units), number: .decimal)) - notification.soundName = UILocalNotificationDefaultSoundName + notification.title = NSLocalizedString("Bolus", comment: "The notification title for a bolus failure") + notification.body = String(format: NSLocalizedString("%@ U bolus may have failed.", comment: "The notification alert describing a possible bolus failure. The substitution parameter is the size of the bolus in units."), NumberFormatter.localizedString(from: NSNumber(value: units), number: .decimal)) + notification.sound = UNNotificationSound.default() if startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) { - notification.category = Category.BolusFailure.rawValue + notification.categoryIdentifier = Category.BolusFailure.rawValue } notification.userInfo = [ @@ -71,30 +73,27 @@ struct NotificationManager { UserInfoKey.BolusStartDate.rawValue: startDate ] - UIApplication.shared.presentLocalNotificationNow(notification) + let request = UNNotificationRequest( + // Only support 1 bolus notification at once + identifier: Category.BolusFailure.rawValue, + content: notification, + trigger: nil + ) + + UNUserNotificationCenter.current().add(request) } // Cancel any previous scheduled notifications in the Loop Not Running category - static func clearLoopNotRunningNotifications() { - let app = UIApplication.shared - - app.scheduledLocalNotifications?.filter({ - $0.category == Category.LoopNotRunning.rawValue - }).forEach({ - app.cancelLocalNotification($0) - }) + static func clearPendingNotificationRequests() { + UNUserNotificationCenter.current().removeAllPendingNotificationRequests() } static func scheduleLoopNotRunningNotifications() { - let app = UIApplication.shared - - clearLoopNotRunningNotifications() - // Give a little extra time for a loop-in-progress to complete let gracePeriod = TimeInterval(minutes: 0.5) for minutes: Double in [20, 40, 60, 120] { - let notification = UILocalNotification() + let notification = UNMutableNotificationContent() let failureInterval = TimeInterval(minutes: minutes) let formatter = DateComponentsFormatter() @@ -103,46 +102,66 @@ struct NotificationManager { formatter.unitsStyle = .full if let failueIntervalString = formatter.string(from: failureInterval)?.localizedLowercase { - notification.alertBody = String(format: NSLocalizedString("Loop has not completed successfully in %@", comment: "The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop"), failueIntervalString) + notification.body = String(format: NSLocalizedString("Loop has not completed successfully in %@", comment: "The notification alert describing a long-lasting loop failure. The substitution parameter is the time interval since the last loop"), failueIntervalString) } - notification.alertTitle = NSLocalizedString("Loop Failure", comment: "The notification title for a loop failure") - notification.fireDate = Date(timeIntervalSinceNow: failureInterval + gracePeriod) - notification.soundName = UILocalNotificationDefaultSoundName - notification.category = Category.LoopNotRunning.rawValue - - app.scheduleLocalNotification(notification) + notification.title = NSLocalizedString("Loop Failure", comment: "The notification title for a loop failure") + notification.sound = UNNotificationSound.default() + notification.categoryIdentifier = Category.LoopNotRunning.rawValue + notification.threadIdentifier = Category.LoopNotRunning.rawValue + + let request = UNNotificationRequest( + identifier: "\(Category.LoopNotRunning.rawValue)\(failureInterval)", + content: notification, + trigger: UNTimeIntervalNotificationTrigger( + timeInterval: failureInterval + gracePeriod, + repeats: false + ) + ) + + UNUserNotificationCenter.current().add(request) } } static func sendPumpBatteryLowNotification() { - let notification = UILocalNotification() + let notification = UNMutableNotificationContent() + + notification.title = NSLocalizedString("Pump Battery Low", comment: "The notification title for a low pump battery") + notification.body = NSLocalizedString("Change the pump battery immediately", comment: "The notification alert describing a low pump battery") + notification.sound = UNNotificationSound.default() + notification.categoryIdentifier = Category.PumpBatteryLow.rawValue - notification.alertTitle = NSLocalizedString("Pump Battery Low", comment: "The notification title for a low pump battery") - notification.alertBody = NSLocalizedString("Change the pump battery immediately", comment: "The notification alert describing a low pump battery") - notification.soundName = UILocalNotificationDefaultSoundName - notification.category = Category.PumpBatteryLow.rawValue + let request = UNNotificationRequest( + identifier: Category.PumpBatteryLow.rawValue, + content: notification, + trigger: nil + ) - UIApplication.shared.presentLocalNotificationNow(notification) + UNUserNotificationCenter.current().add(request) } static func sendPumpReservoirEmptyNotification() { - let notification = UILocalNotification() - - notification.alertTitle = NSLocalizedString("Pump Reservoir Empty", comment: "The notification title for an empty pump reservoir") - notification.alertBody = NSLocalizedString("Change the pump reservoir now", comment: "The notification alert describing an empty pump reservoir") - notification.soundName = UILocalNotificationDefaultSoundName - notification.category = Category.PumpReservoirEmpty.rawValue - - // TODO: Add an action to Suspend the pump + let notification = UNMutableNotificationContent() + + notification.title = NSLocalizedString("Pump Reservoir Empty", comment: "The notification title for an empty pump reservoir") + notification.body = NSLocalizedString("Change the pump reservoir now", comment: "The notification alert describing an empty pump reservoir") + notification.sound = UNNotificationSound.default() + notification.categoryIdentifier = Category.PumpReservoirEmpty.rawValue + + let request = UNNotificationRequest( + // Not a typo: this should replace any pump reservoir low notifications + identifier: Category.PumpReservoirLow.rawValue, + content: notification, + trigger: nil + ) - UIApplication.shared.presentLocalNotificationNow(notification) + UNUserNotificationCenter.current().add(request) } static func sendPumpReservoirLowNotificationForAmount(_ units: Double, andTimeRemaining remaining: TimeInterval?) { - let notification = UILocalNotification() + let notification = UNMutableNotificationContent() - notification.alertTitle = NSLocalizedString("Pump Reservoir Low", comment: "The notification title for a low pump reservoir") + notification.title = NSLocalizedString("Pump Reservoir Low", comment: "The notification title for a low pump reservoir") let unitsString = NumberFormatter.localizedString(from: NSNumber(value: units), number: .decimal) @@ -154,14 +173,20 @@ struct NotificationManager { intervalFormatter.includesTimeRemainingPhrase = true if let remaining = remaining, let timeString = intervalFormatter.string(from: remaining) { - notification.alertBody = String(format: NSLocalizedString("%1$@ U left: %2$@", comment: "Low reservoir alert with time remaining format string. (1: Number of units remaining)(2: approximate time remaining)"), unitsString, timeString) + notification.body = String(format: NSLocalizedString("%1$@ U left: %2$@", comment: "Low reservoir alert with time remaining format string. (1: Number of units remaining)(2: approximate time remaining)"), unitsString, timeString) } else { - notification.alertBody = String(format: NSLocalizedString("%1$@ U left", comment: "Low reservoir alert format string. (1: Number of units remaining)"), unitsString) + notification.body = String(format: NSLocalizedString("%1$@ U left", comment: "Low reservoir alert format string. (1: Number of units remaining)"), unitsString) } - notification.soundName = UILocalNotificationDefaultSoundName - notification.category = Category.PumpReservoirLow.rawValue + notification.sound = UNNotificationSound.default() + notification.categoryIdentifier = Category.PumpReservoirLow.rawValue + + let request = UNNotificationRequest( + identifier: Category.PumpReservoirLow.rawValue, + content: notification, + trigger: nil + ) - UIApplication.shared.presentLocalNotificationNow(notification) + UNUserNotificationCenter.current().add(request) } } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index 7ab5d36f82..14670c1364 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -160,7 +160,7 @@ final class WatchDataManager: NSObject, WCSessionDelegate { } case SetBolusUserInfo.name?: if let bolus = SetBolusUserInfo(rawValue: message as SetBolusUserInfo.RawValue) { - self.deviceDataManager.enactBolus(bolus.value) { (error) in + self.deviceDataManager.enactBolus(units: bolus.value) { (error) in if error != nil { NotificationManager.sendBolusFailureNotificationForAmount(bolus.value, atStartDate: bolus.startDate) } else { diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index b340c4e89a..d0ff9979c2 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -731,7 +731,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize if let bolusViewController = segue.source as? BolusViewController { if let bolus = bolusViewController.bolus, bolus > 0 { let startDate = Date() - dataManager.enactBolus(bolus) { (error) in + dataManager.enactBolus(units: bolus) { (error) in if error != nil { NotificationManager.sendBolusFailureNotificationForAmount(bolus, atStartDate: startDate) } @@ -798,7 +798,7 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize @objc private func openCGMApp(_: Any) { if let url = cgmAppURL { - UIApplication.shared.openURL(url) + UIApplication.shared.open(url) } } From 5806aceafc4a9ad56b1c289240f3ed2d67a42fc4 Mon Sep 17 00:00:00 2001 From: Nathan Racklyeft Date: Thu, 22 Sep 2016 22:47:49 -0700 Subject: [PATCH 02/14] watchOS 3 support (#185) * watchOS 3 background tasks WIP * Adding complication gallery bundle, and more background task comments * More rearchitecture of the watch app, WIP * Refactor bolus / carb sending * Changes to WCSession activation and observation * Fixing bolus, debugging complication * Additional comments, and a TODO * Removing unused code, and more comments --- Loop.xcodeproj/project.pbxproj | 39 +++-- Loop/Extensions/{NSDate.swift => Data.swift} | 0 Loop/Extensions/NSData.swift | 30 ---- .../A307227B-6EFF-4242-A538-2C9AC617E041.json | 15 ++ .../Base.lproj/ckcomplication.strings | 10 ++ .../complicationManifest.json | 6 + .../Base.lproj/ckcomplication.strings | 10 ++ .../ComplicationController.swift | 31 +--- .../AddCarbsInterfaceController.swift | 20 ++- .../BolusInterfaceController.swift | 12 +- .../ContextInterfaceController.swift | 56 ------ .../Controllers/ContextUpdatable.swift | 14 ++ .../Controllers/NotificationController.swift | 2 - .../StatusInterfaceController.swift | 76 ++++---- WatchApp Extension/DeviceDataManager.swift | 155 ----------------- WatchApp Extension/ExtensionDelegate.swift | 164 ++++++++++++++++++ .../Extensions/NSUserDefaults.swift | 26 --- WatchApp Extension/Extensions/WCSession.swift | 58 +++++++ WatchApp/Base.lproj/Interface.storyboard | 44 ++--- 19 files changed, 393 insertions(+), 375 deletions(-) rename Loop/Extensions/{NSDate.swift => Data.swift} (100%) delete mode 100644 Loop/Extensions/NSData.swift create mode 100644 Loop/gallery.ckcomplication/A307227B-6EFF-4242-A538-2C9AC617E041.json create mode 100644 Loop/gallery.ckcomplication/Base.lproj/ckcomplication.strings create mode 100644 Loop/gallery.ckcomplication/complicationManifest.json create mode 100644 WatchApp Extension/Base.lproj/ckcomplication.strings delete mode 100644 WatchApp Extension/Controllers/ContextInterfaceController.swift create mode 100644 WatchApp Extension/Controllers/ContextUpdatable.swift delete mode 100644 WatchApp Extension/DeviceDataManager.swift create mode 100644 WatchApp Extension/Extensions/WCSession.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 915a7a62c5..98c80319d0 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 4302F4DB1D4D6E9F00F0FCAF /* NSData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4DA1D4D6E9F00F0FCAF /* NSData.swift */; }; 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; 4302F4E51D4EA75100F0FCAF /* DoseStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */; }; @@ -16,7 +15,6 @@ 4313EDE01D8A6BF90060FA79 /* ChartContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4313EDDF1D8A6BF90060FA79 /* ChartContentView.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 */; }; - 4328E0181CFBE1DA00E199AA /* ContextInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4328E0131CFBE1DA00E199AA /* ContextInterfaceController.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 */; }; @@ -71,6 +69,9 @@ 437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437CEEE31CDE5C0A003C8C80 /* UIImage.swift */; }; 437D9BA31D7BC977007245E8 /* PredictionTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */; }; 43880F951D9CD54A009061A8 /* ChartPointsScatterDownTrianglesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43880F941D9CD54A009061A8 /* ChartPointsScatterDownTrianglesLayer.swift */; }; + 43846AD51D8FA67800799272 /* Base.lproj in Resources */ = {isa = PBXBuildFile; fileRef = 43846AD41D8FA67800799272 /* Base.lproj */; }; + 43846AD91D8FA84B00799272 /* gallery.ckcomplication in Resources */ = {isa = PBXBuildFile; fileRef = 43846AD81D8FA84B00799272 /* gallery.ckcomplication */; }; + 43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43846ADA1D91057000799272 /* ContextUpdatable.swift */; }; 438849EA1D297CB6003B3F23 /* NightscoutService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849E91D297CB6003B3F23 /* NightscoutService.swift */; }; 438849EC1D29EC34003B3F23 /* AmplitudeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */; }; 438849EE1D2A1EBB003B3F23 /* MLabService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438849ED1D2A1EBB003B3F23 /* MLabService.swift */; }; @@ -97,10 +98,10 @@ 43C246A81D89990F0031F8D1 /* Crypto.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43C246A71D89990F0031F8D1 /* Crypto.framework */; }; 43C418B51CE0575200405B6A /* ShareGlucose+GlucoseKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */; }; 43CA93371CB98079000026B5 /* MinimedKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43CA93361CB98079000026B5 /* MinimedKit.framework */; }; - 43CE7CDE1CA8B63E003CC1B0 /* NSDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CE7CDD1CA8B63E003CC1B0 /* NSDate.swift */; }; + 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CB2B2A1D924D450079823D /* WCSession.swift */; }; + 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */; }; 43DBF04C1C93B8D700B3C386 /* BolusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */; }; 43DBF0531C93EC8200B3C386 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */; }; - 43DBF0551C93ED3000B3C386 /* DeviceDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF0541C93ED3000B3C386 /* DeviceDataManager.swift */; }; 43DBF0591C93F73800B3C386 /* CarbEntryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */; }; 43DE92591C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */; }; 43DE925A1C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */; }; @@ -237,7 +238,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 4302F4DA1D4D6E9F00F0FCAF /* NSData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSData.swift; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 4302F4E41D4EA75100F0FCAF /* DoseStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoseStore.swift; sourceTree = ""; }; @@ -246,7 +246,6 @@ 4313EDDF1D8A6BF90060FA79 /* ChartContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartContentView.swift; sourceTree = ""; }; 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 = ""; }; - 4328E0131CFBE1DA00E199AA /* ContextInterfaceController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextInterfaceController.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 = ""; }; @@ -300,6 +299,9 @@ 437D9BA11D7B5203007245E8 /* Loop.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Loop.xcconfig; sourceTree = ""; }; 437D9BA21D7BC977007245E8 /* PredictionTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredictionTableViewController.swift; sourceTree = ""; }; 43880F941D9CD54A009061A8 /* ChartPointsScatterDownTrianglesLayer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartPointsScatterDownTrianglesLayer.swift; sourceTree = ""; }; + 43846AD41D8FA67800799272 /* Base.lproj */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Base.lproj; sourceTree = ""; }; + 43846AD81D8FA84B00799272 /* gallery.ckcomplication */ = {isa = PBXFileReference; lastKnownFileType = folder; path = gallery.ckcomplication; sourceTree = ""; }; + 43846ADA1D91057000799272 /* ContextUpdatable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContextUpdatable.swift; sourceTree = ""; }; 438849E91D297CB6003B3F23 /* NightscoutService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NightscoutService.swift; sourceTree = ""; }; 438849EB1D29EC34003B3F23 /* AmplitudeService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmplitudeService.swift; sourceTree = ""; }; 438849ED1D2A1EBB003B3F23 /* MLabService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MLabService.swift; sourceTree = ""; }; @@ -328,11 +330,11 @@ 43C246A71D89990F0031F8D1 /* Crypto.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crypto.framework; path = Carthage/Build/iOS/Crypto.framework; sourceTree = ""; }; 43C418B41CE0575200405B6A /* ShareGlucose+GlucoseKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ShareGlucose+GlucoseKit.swift"; sourceTree = ""; }; 43CA93361CB98079000026B5 /* MinimedKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MinimedKit.framework; path = Carthage/Build/iOS/MinimedKit.framework; sourceTree = ""; }; - 43CE7CDD1CA8B63E003CC1B0 /* NSDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDate.swift; sourceTree = ""; }; + 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 = ""; }; 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = "WatchApp Extension.entitlements"; sourceTree = ""; }; 43DBF04B1C93B8D700B3C386 /* BolusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = BolusViewController.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DeviceDataManager.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; - 43DBF0541C93ED3000B3C386 /* DeviceDataManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeviceDataManager.swift; sourceTree = ""; }; 43DBF0581C93F73800B3C386 /* CarbEntryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryTableViewController.swift; sourceTree = ""; }; 43DE92501C541832001FFDE1 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 43DE92581C5479E4001FFDE1 /* CarbEntryUserInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarbEntryUserInfo.swift; sourceTree = ""; }; @@ -447,7 +449,7 @@ children = ( 4328E01D1CFBE25F00E199AA /* AddCarbsInterfaceController.swift */, 4328E0161CFBE1DA00E199AA /* BolusInterfaceController.swift */, - 4328E0131CFBE1DA00E199AA /* ContextInterfaceController.swift */, + 43846ADA1D91057000799272 /* ContextUpdatable.swift */, 43A943891B926B7B0051FA24 /* NotificationController.swift */, 4328E0151CFBE1DA00E199AA /* StatusInterfaceController.swift */, ); @@ -461,6 +463,7 @@ 4328E0201CFBE2C500E199AA /* IdentifiableClass.swift */, 4328E0231CFBE2C500E199AA /* NSUserDefaults.swift */, 4328E0241CFBE2C500E199AA /* UIColor.swift */, + 43CB2B2A1D924D450079823D /* WCSession.swift */, 4328E0251CFBE2C500E199AA /* WKAlertAction.swift */, 4328E02E1CFBF81800E199AA /* WKInterfaceImage.swift */, ); @@ -516,6 +519,7 @@ 43776F8E1B8022E90074EA36 /* Loop */ = { isa = PBXGroup; children = ( + 43846AD81D8FA84B00799272 /* gallery.ckcomplication */, 43EDEE6B1CF2E12A00393BE3 /* Loop.entitlements */, 43F5C2D41B92A4A6003EB13D /* Info.plist */, 43776F8F1B8022E90074EA36 /* AppDelegate.swift */, @@ -558,9 +562,9 @@ isa = PBXGroup; children = ( 43D533BB1CFD1DD7009E3085 /* WatchApp Extension.entitlements */, + 43846AD41D8FA67800799272 /* Base.lproj */, 43A943911B926B7B0051FA24 /* Info.plist */, 43A9438D1B926B7B0051FA24 /* ComplicationController.swift */, - 43DBF0541C93ED3000B3C386 /* DeviceDataManager.swift */, 43A943871B926B7B0051FA24 /* ExtensionDelegate.swift */, 43A9438F1B926B7B0051FA24 /* Assets.xcassets */, 4328E0121CFBE1B700E199AA /* Controllers */, @@ -630,8 +634,7 @@ 434FF1E91CF26C29000DB779 /* IdentifiableClass.swift */, 434F54561D287FDB002A9274 /* NibLoadable.swift */, 430DA58D1D4AEC230097D1CA /* NSBundle.swift */, - 4302F4DA1D4D6E9F00F0FCAF /* NSData.swift */, - 43CE7CDD1CA8B63E003CC1B0 /* NSDate.swift */, + 43CE7CDD1CA8B63E003CC1B0 /* Data.swift */, 4398973A1CD2FC2000223065 /* NSDateFormatter.swift */, 436A0E7A1D7DE13400D6475D /* NSNumberFormatter.swift */, 439897341CD2F7DE00223065 /* NSTimeInterval.swift */, @@ -871,6 +874,9 @@ com.apple.ApplicationGroups.iOS = { enabled = 0; }; + com.apple.BackgroundModes.watchos.app = { + enabled = 0; + }; }; }; 43A9437D1B926B7B0051FA24 = { @@ -930,6 +936,7 @@ 43776F991B8022E90074EA36 /* Assets.xcassets in Resources */, 434F54591D28805E002A9274 /* ButtonTableViewCell.xib in Resources */, 43776F971B8022E90074EA36 /* Main.storyboard in Resources */, + 43846AD91D8FA84B00799272 /* gallery.ckcomplication in Resources */, 434F545B1D2880D4002A9274 /* AuthenticationTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -947,6 +954,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 43846AD51D8FA67800799272 /* Base.lproj in Resources */, 43A943901B926B7B0051FA24 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1019,7 +1027,7 @@ 430DA58E1D4AEC230097D1CA /* NSBundle.swift in Sources */, 43776F901B8022E90074EA36 /* AppDelegate.swift in Sources */, 437CCADA1D284ADF0075D2C3 /* AuthenticationTableViewCell.swift in Sources */, - 43CE7CDE1CA8B63E003CC1B0 /* NSDate.swift in Sources */, + 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, 43F41C331D3A17AA00C11ED6 /* ChartAxisValueDoubleUnit.swift in Sources */, 43F5C2DB1B92A5E1003EB13D /* SettingsTableViewController.swift in Sources */, 4313EDE01D8A6BF90060FA79 /* ChartContentView.swift in Sources */, @@ -1076,7 +1084,6 @@ 436A0DA51D236A2A00104B24 /* LoopError.swift in Sources */, 437CEEC01CD6FCD8003C8C80 /* BasalRateHUDView.swift in Sources */, 43E2D8C61D204678004DA55F /* KeychainManager.swift in Sources */, - 4302F4DB1D4D6E9F00F0FCAF /* NSData.swift in Sources */, 437CEECA1CD84DB7003C8C80 /* BatteryLevelHUDView.swift in Sources */, 43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */, 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */, @@ -1105,8 +1112,6 @@ buildActionMask = 2147483647; files = ( 435400311C9F744E00D5819C /* BolusSuggestionUserInfo.swift in Sources */, - 43DBF0551C93ED3000B3C386 /* DeviceDataManager.swift in Sources */, - 4328E0181CFBE1DA00E199AA /* ContextInterfaceController.swift in Sources */, 43A9438A1B926B7B0051FA24 /* NotificationController.swift in Sources */, 43A943881B926B7B0051FA24 /* ExtensionDelegate.swift in Sources */, 4328E0291CFBE2C500E199AA /* NSUserDefaults.swift in Sources */, @@ -1116,8 +1121,10 @@ 4328E02B1CFBE2C500E199AA /* WKAlertAction.swift in Sources */, 4328E0281CFBE2C500E199AA /* CLKComplicationTemplate.swift in Sources */, 4328E01E1CFBE25F00E199AA /* AddCarbsInterfaceController.swift in Sources */, + 43846ADB1D91057000799272 /* ContextUpdatable.swift in Sources */, 43EA285F1D50ED3D001BC233 /* GlucoseTrend.swift in Sources */, 4328E0261CFBE2C500E199AA /* IdentifiableClass.swift in Sources */, + 43CB2B2B1D924D450079823D /* WCSession.swift in Sources */, 43DE925A1C5479E4001FFDE1 /* CarbEntryUserInfo.swift in Sources */, 4328E0301CFBFAEB00E199AA /* NSTimeInterval.swift in Sources */, 43A9438E1B926B7B0051FA24 /* ComplicationController.swift in Sources */, diff --git a/Loop/Extensions/NSDate.swift b/Loop/Extensions/Data.swift similarity index 100% rename from Loop/Extensions/NSDate.swift rename to Loop/Extensions/Data.swift diff --git a/Loop/Extensions/NSData.swift b/Loop/Extensions/NSData.swift deleted file mode 100644 index 7ce1922d72..0000000000 --- a/Loop/Extensions/NSData.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// NSData.swift -// Loop -// -// Created by Nate Racklyeft on 7/30/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import Foundation - -/* -extension Data { - @nonobjc subscript(index: Int) -> UInt8 { - let bytes: [UInt8] = self[index...index] - - return bytes[0] - } - - subscript(range: Range) -> [UInt8] { - var dataArray = [UInt8](repeating: 0, count: range.count) - (self as NSData).getBytes(&dataArray, range: NSRange(range)) - - return dataArray - } - - subscript(range: Range) -> Data { - return subdata(in: NSRange(range)) - } -} -*/ diff --git a/Loop/gallery.ckcomplication/A307227B-6EFF-4242-A538-2C9AC617E041.json b/Loop/gallery.ckcomplication/A307227B-6EFF-4242-A538-2C9AC617E041.json new file mode 100644 index 0000000000..58580ff1b3 --- /dev/null +++ b/Loop/gallery.ckcomplication/A307227B-6EFF-4242-A538-2C9AC617E041.json @@ -0,0 +1,15 @@ +{ + "class" : "CLKComplicationTemplateModularSmallStackText", + "highlightLine2" : false, + "line2TextProvider" : { + "class" : "CLKLocalizableSimpleTextProvider", + "text" : "mg\/dL" + }, + "line1TextProvider" : { + "shortText" : "--", + "class" : "CLKSimpleTextProvider", + "text" : "--", + "accessibilityLabel" : "No glucose value available" + }, + "version" : 30000 +} \ No newline at end of file diff --git a/Loop/gallery.ckcomplication/Base.lproj/ckcomplication.strings b/Loop/gallery.ckcomplication/Base.lproj/ckcomplication.strings new file mode 100644 index 0000000000..63987e6900 --- /dev/null +++ b/Loop/gallery.ckcomplication/Base.lproj/ckcomplication.strings @@ -0,0 +1,10 @@ +/* + ckcomplication.strings + Loop + + Created by Nate Racklyeft on 9/18/16. + Copyright © 2016 Nathan Racklyeft. All rights reserved. +*/ + +/* The complication template example unit string */ +"mg/dL" = "mg/dL" diff --git a/Loop/gallery.ckcomplication/complicationManifest.json b/Loop/gallery.ckcomplication/complicationManifest.json new file mode 100644 index 0000000000..f7de58b5c8 --- /dev/null +++ b/Loop/gallery.ckcomplication/complicationManifest.json @@ -0,0 +1,6 @@ +{ + "supported complication families" : { + "0" : "A307227B-6EFF-4242-A538-2C9AC617E041.json" + }, + "client ID" : "com.loudnate.Loop.watchkitapp.watchkitextension" +} \ No newline at end of file diff --git a/WatchApp Extension/Base.lproj/ckcomplication.strings b/WatchApp Extension/Base.lproj/ckcomplication.strings new file mode 100644 index 0000000000..63987e6900 --- /dev/null +++ b/WatchApp Extension/Base.lproj/ckcomplication.strings @@ -0,0 +1,10 @@ +/* + ckcomplication.strings + Loop + + Created by Nate Racklyeft on 9/18/16. + Copyright © 2016 Nathan Racklyeft. All rights reserved. +*/ + +/* The complication template example unit string */ +"mg/dL" = "mg/dL" diff --git a/WatchApp Extension/ComplicationController.swift b/WatchApp Extension/ComplicationController.swift index 45192084b4..63884fbbf0 100644 --- a/WatchApp Extension/ComplicationController.swift +++ b/WatchApp Extension/ComplicationController.swift @@ -7,6 +7,7 @@ // import ClockKit +import WatchKit final class ComplicationController: NSObject, CLKComplicationDataSource { @@ -18,7 +19,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } func getTimelineStartDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { - if let date = DeviceDataManager.sharedManager.lastContextData?.glucoseDate { + if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { handler(date as Date) } else { handler(nil) @@ -26,7 +27,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { - if let date = DeviceDataManager.sharedManager.lastContextData?.glucoseDate { + if let date = ExtensionDelegate.shared().lastContext?.glucoseDate { handler(date as Date) } else { handler(nil) @@ -45,7 +46,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { switch complication.family { case .modularSmall: - if let context = DeviceDataManager.sharedManager.lastContextData, + if let context = ExtensionDelegate.shared().lastContext, let glucose = context.glucose, let unit = context.preferredGlucoseUnit, let glucoseString = formatter.string(from: NSNumber(value: glucose.doubleValue(for: unit))), @@ -68,7 +69,7 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: (@escaping ([CLKComplicationTimelineEntry]?) -> Void)) { // Call the handler with the timeline entries after to the given date - if let context = DeviceDataManager.sharedManager.lastContextData, + if let context = ExtensionDelegate.shared().lastContext, let glucose = context.glucose, let unit = context.preferredGlucoseUnit, let glucoseString = formatter.string(from: NSNumber(value: glucose.doubleValue(for: unit))), @@ -81,35 +82,19 @@ final class ComplicationController: NSObject, CLKComplicationDataSource { } } - func requestedUpdateDidBegin() { - DeviceDataManager.sharedManager.updateComplicationDataIfNeeded() - } - - func requestedUpdateBudgetExhausted() { - // TODO: os_log_info in iOS 10 - } - - // MARK: - Update Scheduling - - func getNextRequestedUpdateDate(handler: @escaping (Date?) -> Void) { - // Call the handler with the date when you would next like to be given the opportunity to update your complication content - handler(Date(timeIntervalSinceNow: TimeInterval(2 * 60 * 60))) - } - // MARK: - Placeholder Templates - - func getPlaceholderTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { + + func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { switch complication.family { case .modularSmall: let template = CLKComplicationTemplateModularSmallStackText() template.line1TextProvider = CLKSimpleTextProvider(text: "--", shortText: "--", accessibilityLabel: "No glucose value available") - template.line2TextProvider = CLKSimpleTextProvider(text: "mg/dL") + template.line2TextProvider = CLKSimpleTextProvider.localizableTextProvider(withStringsFileTextKey: "mg/dL") handler(template) default: handler(nil) } } - } diff --git a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift index 03cf417831..9f0ce8211f 100644 --- a/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift +++ b/WatchApp Extension/Controllers/AddCarbsInterfaceController.swift @@ -7,7 +7,7 @@ // import WatchKit -import Foundation +import WatchConnectivity final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClass { @@ -98,7 +98,23 @@ final class AddCarbsInterfaceController: WKInterfaceController, IdentifiableClas if carbValue > 0 { let entry = CarbEntryUserInfo(value: Double(carbValue), absorptionTimeType: absorptionTime, startDate: Date()) - DeviceDataManager.sharedManager.sendCarbEntry(entry) + do { + try WCSession.default().sendCarbEntryMessage(entry, + replyHandler: { (suggestion) in + WKExtension.shared().rootInterfaceController?.presentController(withName: BolusInterfaceController.className, context: suggestion) + }, + errorHandler: { (error) in + ExtensionDelegate.shared().present(error) + } + ) + } catch { + presentAlert(withTitle: NSLocalizedString("Send Failed", comment: "The title of the alert controller displayed after a carb entry send attempt fails"), + message: NSLocalizedString("Make sure your iPhone is nearby and try again", comment: "The recovery message displayed after a carb entry send attempt fails"), + preferredStyle: .alert, + actions: [WKAlertAction.dismissAction()] + ) + return + } } dismiss() diff --git a/WatchApp Extension/Controllers/BolusInterfaceController.swift b/WatchApp Extension/Controllers/BolusInterfaceController.swift index 4462a7fc9c..41006102e3 100644 --- a/WatchApp Extension/Controllers/BolusInterfaceController.swift +++ b/WatchApp Extension/Controllers/BolusInterfaceController.swift @@ -8,6 +8,7 @@ import WatchKit import Foundation +import WatchConnectivity final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { @@ -130,16 +131,19 @@ final class BolusInterfaceController: WKInterfaceController, IdentifiableClass { @IBAction func deliver() { if bolusValue > 0 { let bolus = SetBolusUserInfo(value: bolusValue, startDate: Date()) + do { - try DeviceDataManager.sharedManager.sendSetBolus(bolus) - } catch DeviceDataManagerError.reachabilityError { - presentAlert(withTitle: NSLocalizedString("Bolus Failed", comment: "The title of the alert controller displayed after a bolus attempt fails"), + try WCSession.default().sendBolusMessage(bolus) { (error) in + ExtensionDelegate.shared().present(error) + } + } catch { + presentAlert( + withTitle: NSLocalizedString("Bolus Failed", comment: "The title of the alert controller displayed after a bolus attempt fails"), message: NSLocalizedString("Make sure your iPhone is nearby and try again", comment: "The recovery message displayed after a bolus attempt fails"), preferredStyle: .alert, actions: [WKAlertAction.dismissAction()] ) return - } catch { } } diff --git a/WatchApp Extension/Controllers/ContextInterfaceController.swift b/WatchApp Extension/Controllers/ContextInterfaceController.swift deleted file mode 100644 index f9cb4e684a..0000000000 --- a/WatchApp Extension/Controllers/ContextInterfaceController.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// ContextInterfaceController.swift -// Loop -// -// Created by Nathan Racklyeft on 5/29/16. -// Copyright © 2016 Nathan Racklyeft. All rights reserved. -// - -import WatchKit -import Foundation - - -class ContextInterfaceController: WKInterfaceController { - - let dataManager = DeviceDataManager.sharedManager - - private var lastContextDataObserverContext = 0 - - override func awake(withContext context: Any?) { - super.awake(withContext: context) - - // Configure interface objects here. - } - - override func willActivate() { - super.willActivate() - - dataManager.addObserver(self, forKeyPath: "lastContextData", options: [], context: &lastContextDataObserverContext) - - updateFromContext(dataManager.lastContextData) - } - - override func didDeactivate() { - super.didDeactivate() - - dataManager.removeObserver(self, forKeyPath: "lastContextData", context: &lastContextDataObserverContext) - - } - - func updateFromContext(_ context: WatchContext?) { - DeviceDataManager.sharedManager.updateComplicationDataIfNeeded() - } - - // MARK: - KVO - - override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { - if context == &lastContextDataObserverContext { - if let context = dataManager.lastContextData { - updateFromContext(context) - } - } else { - super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) - } - } - -} diff --git a/WatchApp Extension/Controllers/ContextUpdatable.swift b/WatchApp Extension/Controllers/ContextUpdatable.swift new file mode 100644 index 0000000000..00cc2100d7 --- /dev/null +++ b/WatchApp Extension/Controllers/ContextUpdatable.swift @@ -0,0 +1,14 @@ +// +// ContextUpdatable.swift +// Loop +// +// Created by Nate Racklyeft on 9/19/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import Foundation + + +protocol ContextUpdatable { + func update(with context: WatchContext?) +} diff --git a/WatchApp Extension/Controllers/NotificationController.swift b/WatchApp Extension/Controllers/NotificationController.swift index 60226b4aef..0cc9b0011e 100644 --- a/WatchApp Extension/Controllers/NotificationController.swift +++ b/WatchApp Extension/Controllers/NotificationController.swift @@ -30,10 +30,8 @@ final class NotificationController: WKUserNotificationInterfaceController { super.didDeactivate() } - /* override func didReceive(_ notification: UNNotification, withCompletion completionHandler: @escaping (WKUserNotificationInterfaceType) -> Void) { completionHandler(.default) } - */ } diff --git a/WatchApp Extension/Controllers/StatusInterfaceController.swift b/WatchApp Extension/Controllers/StatusInterfaceController.swift index e2fc48d7fa..a5771c4bc6 100644 --- a/WatchApp Extension/Controllers/StatusInterfaceController.swift +++ b/WatchApp Extension/Controllers/StatusInterfaceController.swift @@ -10,7 +10,7 @@ import WatchKit import Foundation -final class StatusInterfaceController: ContextInterfaceController { +final class StatusInterfaceController: WKInterfaceController, ContextUpdatable { @IBOutlet var graphImage: WKInterfaceImage! @IBOutlet var loopHUDImage: WKInterfaceImage! @@ -19,59 +19,57 @@ final class StatusInterfaceController: ContextInterfaceController { @IBOutlet var eventualGlucoseLabel: WKInterfaceLabel! @IBOutlet var statusLabel: WKInterfaceLabel! - override func updateFromContext(_ context: WatchContext?) { - super.updateFromContext(context) + private var lastContext: WatchContext? - resetInterface() + func update(with context: WatchContext?) { + lastContext = context - DispatchQueue.main.async { - if let date = context?.loopLastRunDate { - self.loopTimer.setDate(date as Date) - self.loopTimer.setHidden(false) - self.loopTimer.start() + if let date = context?.loopLastRunDate { + self.loopTimer.setDate(date as Date) + self.loopTimer.setHidden(false) + self.loopTimer.start() - let loopImage: LoopImage + let loopImage: LoopImage - switch date.timeIntervalSinceNow { - case let t where t.minutes <= 5: - loopImage = .Fresh - case let t where t.minutes <= 15: - loopImage = .Aging - default: - loopImage = .Stale - } - - self.loopHUDImage.setLoopImage(loopImage) + switch date.timeIntervalSinceNow { + case let t where t.minutes <= 5: + loopImage = .Fresh + case let t where t.minutes <= 15: + loopImage = .Aging + default: + loopImage = .Stale } + + self.loopHUDImage.setLoopImage(loopImage) + } else { + loopTimer.setHidden(true) + loopHUDImage.setLoopImage(.Unknown) } let numberFormatter = NumberFormatter() - DispatchQueue.main.async { - if let glucose = context?.glucose, let unit = context?.preferredGlucoseUnit { - let glucoseValue = glucose.doubleValue(for: unit) - let trend = context?.glucoseTrend?.symbol ?? "" + if let glucose = context?.glucose, let unit = context?.preferredGlucoseUnit { + let glucoseValue = glucose.doubleValue(for: unit) + let trend = context?.glucoseTrend?.symbol ?? "" - self.glucoseLabel.setText((numberFormatter.string(from: NSNumber(value: glucoseValue)) ?? "") + trend) - self.glucoseLabel.setHidden(false) - } + self.glucoseLabel.setText((numberFormatter.string(from: NSNumber(value: glucoseValue)) ?? "") + trend) + self.glucoseLabel.setHidden(false) + } else { + glucoseLabel.setHidden(true) + } - if let eventualGlucose = context?.eventualGlucose, let unit = context?.preferredGlucoseUnit { - let glucoseValue = eventualGlucose.doubleValue(for: unit) + if let eventualGlucose = context?.eventualGlucose, let unit = context?.preferredGlucoseUnit { + let glucoseValue = eventualGlucose.doubleValue(for: unit) - self.eventualGlucoseLabel.setText(numberFormatter.string(from: NSNumber(value: glucoseValue))) - self.eventualGlucoseLabel.setHidden(false) - } + self.eventualGlucoseLabel.setText(numberFormatter.string(from: NSNumber(value: glucoseValue))) + self.eventualGlucoseLabel.setHidden(false) + } else { + eventualGlucoseLabel.setHidden(true) } - } - private func resetInterface() { - loopTimer.setHidden(true) + // TODO: Other elements statusLabel.setHidden(true) graphImage.setHidden(true) - glucoseLabel.setHidden(true) - eventualGlucoseLabel.setHidden(true) - loopHUDImage.setLoopImage(.Unknown) } // MARK: - Menu Items @@ -81,7 +79,7 @@ final class StatusInterfaceController: ContextInterfaceController { } @IBAction func setBolus() { - presentController(withName: BolusInterfaceController.className, context: dataManager.lastContextData?.bolusSuggestion) + presentController(withName: BolusInterfaceController.className, context: lastContext?.bolusSuggestion) } } diff --git a/WatchApp Extension/DeviceDataManager.swift b/WatchApp Extension/DeviceDataManager.swift deleted file mode 100644 index bc9d0fc0c3..0000000000 --- a/WatchApp Extension/DeviceDataManager.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// DeviceDataManager.swift -// Naterade -// -// Created by Nathan Racklyeft on 9/24/15. -// Copyright © 2015 Nathan Racklyeft. All rights reserved. -// - -import ClockKit -import Foundation -import WatchConnectivity -import WatchKit - - -enum DeviceDataManagerError: Error { - case reachabilityError -} - - -final class DeviceDataManager: NSObject, WCSessionDelegate { - - private var connectSession: WCSession? - - private func readContext() -> WatchContext? { - return UserDefaults.standard.watchContext - } - - private func saveContext(_ context: WatchContext) { - UserDefaults.standard.watchContext = context - } - - private var complicationDataLastRefreshed: Date { - get { - return UserDefaults.standard.complicationDataLastRefreshed - } - set { - UserDefaults.standard.complicationDataLastRefreshed = newValue - } - } - - private var hasNewComplicationData: Bool { - get { - return UserDefaults.standard.watchContextReadyForComplication - } - set { - UserDefaults.standard.watchContextReadyForComplication = newValue - } - } - - dynamic var lastContextData: WatchContext? { - didSet { - if let data = lastContextData { - saveContext(data) - } - } - } - - func updateComplicationDataIfNeeded() { - if DeviceDataManager.sharedManager.hasNewComplicationData { - DeviceDataManager.sharedManager.hasNewComplicationData = false - let server = CLKComplicationServer.sharedInstance() - for complication in server.activeComplications ?? [] { - if complicationDataLastRefreshed.timeIntervalSinceNow < TimeInterval(-8 * 60 * 60) { - complicationDataLastRefreshed = Date() - server.reloadTimeline(for: complication) - } else { - server.extendTimeline(for: complication) - } - } - } - } - - func sendCarbEntry(_ carbEntry: CarbEntryUserInfo) { - guard let session = connectSession else { return } - - if session.isReachable { - var replied = false - - session.sendMessage(carbEntry.rawValue, - replyHandler: { (reply) -> Void in - replied = true - - if let suggestion = BolusSuggestionUserInfo(rawValue: reply as BolusSuggestionUserInfo.RawValue), suggestion.recommendedBolus > 0 { - WKExtension.shared().rootInterfaceController?.presentController(withName: BolusInterfaceController.className, context: suggestion) - } - }, - errorHandler: { (error) -> Void in - if !replied { - WKExtension.shared().rootInterfaceController?.presentAlert(withTitle: #function, message: (error as NSError).localizedRecoverySuggestion, preferredStyle: .alert, actions: [WKAlertAction.dismissAction()]) - } - } - ) - } else { - session.transferUserInfo(carbEntry.rawValue) - } - } - - func sendSetBolus(_ userInfo: SetBolusUserInfo) throws { - guard let session = connectSession, session.isReachable else { - throw DeviceDataManagerError.reachabilityError - } - - var replied = false - - session.sendMessage(userInfo.rawValue, replyHandler: { (reply) -> Void in - replied = true - }, errorHandler: { (error) -> Void in - if !replied { - WKExtension.shared().rootInterfaceController?.presentAlert(withTitle: error.localizedDescription, message: (error as NSError).localizedRecoverySuggestion ?? (error as NSError).localizedFailureReason, preferredStyle: .alert, actions: [WKAlertAction.dismissAction()]) - } - }) - } - - // MARK: - WCSessionDelegate - - func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { -// if let error = error { - // TODO: os_log_info in iOS 10 -// } - } - - func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { - if let context = WatchContext(rawValue: applicationContext as WatchContext.RawValue) { - lastContextData = context - } - } - - func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) { - switch userInfo["name"] as? String { - case .some: - break - default: - if let context = WatchContext(rawValue: userInfo as WatchContext.RawValue) { - lastContextData = context - updateComplicationDataIfNeeded() - } - } - } - - // MARK: - Initialization - - static let sharedManager = DeviceDataManager() - - override init() { - super.init() - - connectSession = WCSession.default() - connectSession?.delegate = self - connectSession?.activate() - - if let context = readContext() { - self.lastContextData = context - } - } -} diff --git a/WatchApp Extension/ExtensionDelegate.swift b/WatchApp Extension/ExtensionDelegate.swift index 90266a3152..5e2ad95f4b 100644 --- a/WatchApp Extension/ExtensionDelegate.swift +++ b/WatchApp Extension/ExtensionDelegate.swift @@ -6,17 +6,48 @@ // Copyright © 2015 Nathan Racklyeft. All rights reserved. // +import WatchConnectivity import WatchKit +import os final class ExtensionDelegate: NSObject, WKExtensionDelegate { + static func shared() -> ExtensionDelegate { + return WKExtension.shared().extensionDelegate + } + + override init() { + super.init() + + let session = WCSession.default() + session.delegate = self + + // It seems, according to [this sample code](https://developer.apple.com/library/prerelease/content/samplecode/QuickSwitch/Listings/QuickSwitch_WatchKit_Extension_ExtensionDelegate_swift.html#//apple_ref/doc/uid/TP40016647-QuickSwitch_WatchKit_Extension_ExtensionDelegate_swift-DontLinkElementID_8) + // that WCSession activation and delegation and WKWatchConnectivityRefreshBackgroundTask don't have any determinism, + // and that KVO is the "recommended" way to deal with it. + session.addObserver(self, forKeyPath: "activationState", options: [], context: nil) + session.addObserver(self, forKeyPath: "hasContentPending", options: [], context: nil) + + session.activate() + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + DispatchQueue.main.async { + self.completePendingConnectivityTasksIfNeeded() + } + } + func applicationDidFinishLaunching() { // Perform any final initialization of your application. } func applicationDidBecomeActive() { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + + if WCSession.default().activationState != .activated { + WCSession.default().activate() + } } func applicationWillResignActive() { @@ -24,4 +55,137 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate { // Use this method to pause ongoing tasks, disable timers, etc. } + func handleUserActivity(_ userInfo: [AnyHashable : Any]?) { + // Use it to respond to Handoff–related activity. WatchKit calls this method when your app is launched as a result of a Handoff action. Use the information in the provided userInfo dictionary to determine how you want to respond to the action. For example, you might decide to display a specific interface controller. + } + + func handle(_ backgroundTasks: Set) { + for task in backgroundTasks { + switch task { + case is WKApplicationRefreshBackgroundTask: + os_log("Processing WKApplicationRefreshBackgroundTask") + // Use the WKApplicationRefreshBackgroundTask class to update your app’s state in the background. + // You often use a background app refresh task to drive other tasks. For example, you could use a background app refresh task to start an URLSession background transfer, or to schedule a background snapshot refresh task. + // Your app must schedule background app refresh tasks by calling your extension’s scheduleBackgroundRefresh(withPreferredDate:userInfo:scheduledCompletion:) method. The system never schedules these tasks. + // WKExtension.shared().scheduleBackgroundRefresh(withPreferredDate:userInfo: scheduledCompletion:) + // For more information, see [WKApplicationRefreshBackgroundTask] https://developer.apple.com/reference/watchkit/wkapplicationrefreshbackgroundtask + // Background app refresh tasks are budgeted. In general, the system performs approximately one task per hour for each app in the dock (including the most recently used app). This budget is shared among all apps on the dock. The system performs multiple tasks an hour for each app with a complication on the active watch face. This budget is shared among all complications on the watch face. After you exhaust the budget, the system delays your requests until more time becomes available. + break + case let task as WKSnapshotRefreshBackgroundTask: + os_log("Processing WKSnapshotRefreshBackgroundTask") + // Use the WKSnapshotRefreshBackgroundTask class to update your app’s user interface. You can push, pop, or present other interface controllers, and then update the content of the desired interface controller. The system automatically takes a snapshot of your user interface as soon as this task completes. + // Your app can invalidate its current snapshot and schedule a background snapshot refresh tasks by calling your extension’s scheduleSnapshotRefresh(withPreferredDate:userInfo:scheduledCompletion:) method. The system will also schedule background snapshot refresh tasks to periodically update your snapshot. + // For more information, see WKSnapshotRefreshBackgroundTask. + // For more information about snapshots, see Snapshots. + + task.setTaskCompleted(restoredDefaultState: false, estimatedSnapshotExpiration: Date(timeIntervalSinceNow: TimeInterval(minutes: 5)), userInfo: nil) + return // Don't call the standard setTaskCompleted handler + case is WKURLSessionRefreshBackgroundTask: + // Use the WKURLSessionRefreshBackgroundTask class to respond to URLSession background transfers. + break + case let task as WKWatchConnectivityRefreshBackgroundTask: + os_log("Processing WKWatchConnectivityRefreshBackgroundTask") + // Use the WKWatchConnectivityRefreshBackgroundTask class to receive background updates from the WatchConnectivity framework. + // For more information, see WKWatchConnectivityRefreshBackgroundTask. + + pendingConnectivityTasks.append(task) + + if WCSession.default().activationState != .activated { + WCSession.default().activate() + } + + completePendingConnectivityTasksIfNeeded() + return // Defer calls to the setTaskCompleted handler + default: + break + } + + task.setTaskCompleted() + } + } + + private var pendingConnectivityTasks: [WKWatchConnectivityRefreshBackgroundTask] = [] + + private func completePendingConnectivityTasksIfNeeded() { + if WCSession.default().activationState == .activated && !WCSession.default().hasContentPending { + pendingConnectivityTasks.forEach { $0.setTaskCompleted() } + pendingConnectivityTasks.removeAll() + } + } + + // Main queue only + private(set) var lastContext: WatchContext? { + didSet { + WKExtension.shared().rootUpdatableInterfaceController?.update(with: lastContext) + + if WKExtension.shared().applicationState != .active { + WKExtension.shared().scheduleSnapshotRefresh(withPreferredDate: Date(), userInfo: nil) { (_) in } + } + + // Update complication data if needed + let server = CLKComplicationServer.sharedInstance() + for complication in server.activeComplications ?? [] { + // In watchOS 2, we forced a timeline reload every 8 hours because attempting to extend it indefinitely seemed to lead to the complication "freezing". + if UserDefaults.standard.complicationDataLastRefreshed.timeIntervalSinceNow < TimeInterval(hours: -8) { + os_log("Reloading complication timeline") + server.reloadTimeline(for: complication) + } else { + os_log("Extending complication timeline") + // TODO: Switch this back to extendTimeline if things are working correctly. + // Time Travel appears to be disabled by default in watchOS 3 anyway + server.reloadTimeline(for: complication) + } + } + } + } + + fileprivate func updateContext(_ data: [String: Any]) { + if let context = WatchContext(rawValue: data as WatchContext.RawValue) { + DispatchQueue.main.async { + self.lastContext = context + } + } + } +} + + +extension ExtensionDelegate: WCSessionDelegate { + func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { + if activationState == .activated && lastContext == nil { + updateContext(session.receivedApplicationContext) + } + } + + func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { + updateContext(applicationContext) + } + + func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) { + // WatchContext is the only userInfo type without a "name" key. This isn't a great heuristic. + if !(userInfo["name"] is String) { + updateContext(userInfo) + } + } +} + + +extension ExtensionDelegate { + + /// Global shortcut to present an alert for a specific error out-of-context with a specific interface controller. + /// + /// - parameter error: The error whose contents to display + func present(_ error: Error) { + WKExtension.shared().rootInterfaceController?.presentAlert(withTitle: error.localizedDescription, message: (error as NSError).localizedRecoverySuggestion ?? (error as NSError).localizedFailureReason, preferredStyle: .alert, actions: [WKAlertAction.dismissAction()]) + } +} + + +fileprivate extension WKExtension { + var extensionDelegate: ExtensionDelegate! { + return delegate as? ExtensionDelegate + } + + var rootUpdatableInterfaceController: ContextUpdatable? { + return rootInterfaceController as? ContextUpdatable + } } diff --git a/WatchApp Extension/Extensions/NSUserDefaults.swift b/WatchApp Extension/Extensions/NSUserDefaults.swift index 99d4cb77b6..b45ba55feb 100644 --- a/WatchApp Extension/Extensions/NSUserDefaults.swift +++ b/WatchApp Extension/Extensions/NSUserDefaults.swift @@ -12,8 +12,6 @@ import Foundation extension UserDefaults { private enum Key: String { case ComplicationDataLastRefreshed = "com.loudnate.Naterade.ComplicationDataLastRefreshed" - case WatchContext = "com.loudnate.Naterade.WatchContext" - case WatchContextReadyForComplication = "com.loudnate.Naterade.WatchContextReadyForComplication" } var complicationDataLastRefreshed: Date { @@ -24,28 +22,4 @@ extension UserDefaults { set(newValue, forKey: Key.ComplicationDataLastRefreshed.rawValue) } } - - var watchContext: WatchContext? { - get { - if let rawValue = dictionary(forKey: Key.WatchContext.rawValue) { - return WatchContext(rawValue: rawValue as WatchContext.RawValue) - } else { - return nil - } - } - set { - set(newValue?.rawValue, forKey: Key.WatchContext.rawValue) - - watchContextReadyForComplication = newValue != nil - } - } - - var watchContextReadyForComplication: Bool { - get { - return bool(forKey: Key.WatchContextReadyForComplication.rawValue) - } - set { - set(newValue, forKey: Key.WatchContextReadyForComplication.rawValue) - } - } } diff --git a/WatchApp Extension/Extensions/WCSession.swift b/WatchApp Extension/Extensions/WCSession.swift new file mode 100644 index 0000000000..346b7ced61 --- /dev/null +++ b/WatchApp Extension/Extensions/WCSession.swift @@ -0,0 +1,58 @@ +// +// WCSession.swift +// Loop +// +// Created by Nate Racklyeft on 9/20/16. +// Copyright © 2016 Nathan Racklyeft. All rights reserved. +// + +import WatchConnectivity + + +enum MessageError: Error { + case activationError + case decodingError + case reachabilityError +} + + +extension WCSession { + func sendCarbEntryMessage(_ carbEntry: CarbEntryUserInfo, replyHandler: @escaping (BolusSuggestionUserInfo) -> Void, errorHandler: @escaping (Error) -> Void) throws { + guard activationState == .activated else { + throw MessageError.activationError + } + + guard isReachable else { + transferUserInfo(carbEntry.rawValue) + return + } + + sendMessage(carbEntry.rawValue, + replyHandler: { (reply) in + guard let suggestion = BolusSuggestionUserInfo(rawValue: reply as BolusSuggestionUserInfo.RawValue) else { + errorHandler(MessageError.decodingError) + return + } + + replyHandler(suggestion) + }, + errorHandler: errorHandler + ) + } + + func sendBolusMessage(_ userInfo: SetBolusUserInfo, errorHandler: @escaping (Error) -> Void) throws { + guard activationState == .activated else { + throw MessageError.activationError + } + + guard isReachable else { + throw MessageError.reachabilityError + } + + sendMessage(userInfo.rawValue, + replyHandler: { (reply) in + }, + errorHandler: errorHandler + ) + } +} diff --git a/WatchApp/Base.lproj/Interface.storyboard b/WatchApp/Base.lproj/Interface.storyboard index c9f0a77912..45440a50ce 100644 --- a/WatchApp/Base.lproj/Interface.storyboard +++ b/WatchApp/Base.lproj/Interface.storyboard @@ -1,8 +1,8 @@ - + - - + + @@ -22,8 +22,8 @@