Jon B Mårtensson пре 4 година
родитељ
комит
16df551ed5
46 измењених фајлова са 2167 додато и 1 уклоњено
  1. 409 1
      FreeAPS.xcodeproj/project.pbxproj
  2. 15 0
      FreeAPS.xcodeproj/xcuserdata/i.valkou.xcuserdatad/xcschemes/xcschememanagement.plist
  3. 1 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  4. 1 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  5. 12 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  6. 12 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  7. 7 0
      FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift
  8. 338 0
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  9. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
  10. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/FreeAPS-X.svg
  11. 53 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
  12. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
  13. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/FreeAPS-X-2.svg
  14. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
  15. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/FreeAPS-X-3.svg
  16. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
  17. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/FreeAPS-X-3.svg
  18. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
  19. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/FreeAPS-X-4.svg
  20. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
  21. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/FreeAPS-X-5.svg
  22. 20 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
  23. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
  24. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/FreeAPS-X-6.svg
  25. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
  26. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/FreeAPS-X-7.svg
  27. 6 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Contents.json
  28. 59 0
      FreeAPSWatch WatchKit Extension/ComplicationController.swift
  29. 26 0
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  30. 15 0
      FreeAPSWatch WatchKit Extension/FreeAPSApp.swift
  31. 10 0
      FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements
  32. 16 0
      FreeAPSWatch WatchKit Extension/Info.plist
  33. 25 0
      FreeAPSWatch WatchKit Extension/NotificationController.swift
  34. 13 0
      FreeAPSWatch WatchKit Extension/NotificationView.swift
  35. 6 0
      FreeAPSWatch WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json
  36. 20 0
      FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns
  37. 86 0
      FreeAPSWatch WatchKit Extension/Views/BolusView.swift
  38. 72 0
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift
  39. 95 0
      FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift
  40. 209 0
      FreeAPSWatch WatchKit Extension/Views/MainView.swift
  41. 54 0
      FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift
  42. 162 0
      FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift
  43. 11 0
      FreeAPSWatch/Assets.xcassets/AccentColor.colorset/Contents.json
  44. 109 0
      FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
  45. 6 0
      FreeAPSWatch/Assets.xcassets/Contents.json
  46. 10 0
      FreeAPSWatch/FreeAPSWatch.entitlements

+ 409 - 1
FreeAPS.xcodeproj/project.pbxproj

@@ -180,6 +180,31 @@
 		38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E873FD274F761800975559 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		38E87403274F78C000975559 /* libswiftCoreNFC.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E87402274F78C000975559 /* libswiftCoreNFC.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
 		38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E87407274F9AD000975559 /* UserNotificationsManager.swift */; };
+		38E8751F27554D5700975559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38E8751E27554D5700975559 /* Assets.xcassets */; };
+		38E8752527554D5700975559 /* FreeAPSWatch WatchKit Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		38E8752A27554D5700975559 /* FreeAPSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8752927554D5700975559 /* FreeAPSApp.swift */; };
+		38E8752C27554D5700975559 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8752B27554D5700975559 /* MainView.swift */; };
+		38E8752E27554D5700975559 /* NotificationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8752D27554D5700975559 /* NotificationController.swift */; };
+		38E8753027554D5700975559 /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8752F27554D5700975559 /* NotificationView.swift */; };
+		38E8753227554D5700975559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8753127554D5700975559 /* ComplicationController.swift */; };
+		38E8753427554D5800975559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38E8753327554D5800975559 /* Assets.xcassets */; };
+		38E8753727554D5900975559 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 38E8753627554D5800975559 /* Preview Assets.xcassets */; };
+		38E8753C27554D5900975559 /* FreeAPSWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 38E8751C27554D5500975559 /* FreeAPSWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
+		38E8754527554D8800975559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388E595F25AD948E0019842D /* Assets.xcassets */; };
+		38E8754627554D8A00975559 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 388E595F25AD948E0019842D /* Assets.xcassets */; };
+		38E8754727554DF100975559 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F37827261260DC009DB701 /* Color+Extensions.swift */; };
+		38E8754A275550BB00975559 /* CarbsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E87549275550BB00975559 /* CarbsView.swift */; };
+		38E8754C2755548F00975559 /* WatchStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8754B2755548F00975559 /* WatchStateModel.swift */; };
+		38E8754F275556FA00975559 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8754E275556FA00975559 /* WatchManager.swift */; };
+		38E8755127555D0500975559 /* DataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8755027555D0500975559 /* DataFlow.swift */; };
+		38E8755427561E9800975559 /* DataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8755027555D0500975559 /* DataFlow.swift */; };
+		38E8755827567AE400975559 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 38E8755727567AE400975559 /* SwiftDate */; };
+		38E8755927567CA600975559 /* Decimal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */; };
+		38E8755B27568A6800975559 /* ConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8755A27568A6700975559 /* ConfirmationView.swift */; };
+		38E8757927579D9200975559 /* Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE5525C9D4D500A708ED /* Publisher.swift */; };
+		38E8757B2757B1C300975559 /* TempTargetsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8757A2757B1C300975559 /* TempTargetsView.swift */; };
+		38E8757D2757C45D00975559 /* BolusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E8757C2757C45D00975559 /* BolusView.swift */; };
+		38E8757E2758C86A00975559 /* ConvenienceExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38192E0C261BAF980094D973 /* ConvenienceExtensions.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
 		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
 		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
@@ -276,6 +301,20 @@
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
+		38E8752627554D5700975559 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 388E595025AD948C0019842D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 38E8752327554D5700975559;
+			remoteInfo = "FreeAPSWatch WatchKit Extension";
+		};
+		38E8753A27554D5900975559 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 388E595025AD948C0019842D /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = 38E8751B27554D5500975559;
+			remoteInfo = FreeAPSWatch;
+		};
 		38FCF3F225E9028E0078B0D1 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = 388E595025AD948C0019842D /* Project object */;
@@ -310,6 +349,28 @@
 			name = "Embed Frameworks";
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		38E8753D27554D5900975559 /* Embed Watch Content */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
+			dstSubfolderSpec = 16;
+			files = (
+				38E8753C27554D5900975559 /* FreeAPSWatch.app in Embed Watch Content */,
+			);
+			name = "Embed Watch Content";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		38E8754027554D5900975559 /* Embed App Extensions */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 13;
+			files = (
+				38E8752527554D5700975559 /* FreeAPSWatch WatchKit Extension.appex in Embed App Extensions */,
+			);
+			name = "Embed App Extensions";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXCopyFilesBuildPhase section */
 
 /* Begin PBXFileReference section */
@@ -511,6 +572,27 @@
 		38E873FD274F761800975559 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; };
 		38E87402274F78C000975559 /* libswiftCoreNFC.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreNFC.tbd; path = usr/lib/swift/libswiftCoreNFC.tbd; sourceTree = SDKROOT; };
 		38E87407274F9AD000975559 /* UserNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsManager.swift; sourceTree = "<group>"; };
+		38E8751C27554D5500975559 /* FreeAPSWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FreeAPSWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		38E8751E27554D5700975559 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FreeAPSWatch WatchKit Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
+		38E8752927554D5700975559 /* FreeAPSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FreeAPSApp.swift; sourceTree = "<group>"; };
+		38E8752B27554D5700975559 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
+		38E8752D27554D5700975559 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = "<group>"; };
+		38E8752F27554D5700975559 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
+		38E8753127554D5700975559 /* ComplicationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationController.swift; sourceTree = "<group>"; };
+		38E8753327554D5800975559 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		38E8753627554D5800975559 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		38E8753827554D5900975559 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		38E8753927554D5900975559 /* PushNotificationPayload.apns */ = {isa = PBXFileReference; lastKnownFileType = text; path = PushNotificationPayload.apns; sourceTree = "<group>"; };
+		38E87549275550BB00975559 /* CarbsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbsView.swift; sourceTree = "<group>"; };
+		38E8754B2755548F00975559 /* WatchStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchStateModel.swift; sourceTree = "<group>"; };
+		38E8754E275556FA00975559 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = "<group>"; };
+		38E8755027555D0500975559 /* DataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataFlow.swift; sourceTree = "<group>"; };
+		38E8755527564B5000975559 /* FreeAPSWatch WatchKit Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "FreeAPSWatch WatchKit Extension.entitlements"; sourceTree = "<group>"; };
+		38E8755627564B6100975559 /* FreeAPSWatch.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FreeAPSWatch.entitlements; sourceTree = "<group>"; };
+		38E8755A27568A6700975559 /* ConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationView.swift; sourceTree = "<group>"; };
+		38E8757A2757B1C300975559 /* TempTargetsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetsView.swift; sourceTree = "<group>"; };
+		38E8757C2757C45D00975559 /* BolusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusView.swift; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
 		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
 		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@@ -639,6 +721,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		38E8752127554D5700975559 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38E8755827567AE400975559 /* SwiftDate in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		38FCF3EA25E9028E0078B0D1 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -858,6 +948,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				38E8754D275556E100975559 /* WatchManager */,
 				38E87406274F9AA500975559 /* UserNotifiactions */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
 				38AEE75025F021F10013F05B /* SettingsManager */,
@@ -1078,6 +1169,8 @@
 				388E595A25AD948C0019842D /* FreeAPS */,
 				38FCF3EE25E9028E0078B0D1 /* FreeAPSTests */,
 				3818AA44274C229000843DB3 /* Packages */,
+				38E8751D27554D5500975559 /* FreeAPSWatch */,
+				38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */,
 				388E595925AD948C0019842D /* Products */,
 				3818AA48274C267000843DB3 /* Frameworks */,
 			);
@@ -1088,6 +1181,8 @@
 			children = (
 				388E595825AD948C0019842D /* FreeAPS.app */,
 				38FCF3ED25E9028E0078B0D1 /* FreeAPSTests.xctest */,
+				38E8751C27554D5500975559 /* FreeAPSWatch.app */,
+				38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -1243,6 +1338,62 @@
 			path = UserNotifiactions;
 			sourceTree = "<group>";
 		};
+		38E8751D27554D5500975559 /* FreeAPSWatch */ = {
+			isa = PBXGroup;
+			children = (
+				38E8755627564B6100975559 /* FreeAPSWatch.entitlements */,
+				38E8751E27554D5700975559 /* Assets.xcassets */,
+			);
+			path = FreeAPSWatch;
+			sourceTree = "<group>";
+		};
+		38E8752827554D5700975559 /* FreeAPSWatch WatchKit Extension */ = {
+			isa = PBXGroup;
+			children = (
+				38E8755527564B5000975559 /* FreeAPSWatch WatchKit Extension.entitlements */,
+				38E8755027555D0500975559 /* DataFlow.swift */,
+				38E875482755505800975559 /* Views */,
+				38E8752927554D5700975559 /* FreeAPSApp.swift */,
+				38E8752D27554D5700975559 /* NotificationController.swift */,
+				38E8752F27554D5700975559 /* NotificationView.swift */,
+				38E8753127554D5700975559 /* ComplicationController.swift */,
+				38E8753327554D5800975559 /* Assets.xcassets */,
+				38E8753827554D5900975559 /* Info.plist */,
+				38E8753927554D5900975559 /* PushNotificationPayload.apns */,
+				38E8753527554D5800975559 /* Preview Content */,
+			);
+			path = "FreeAPSWatch WatchKit Extension";
+			sourceTree = "<group>";
+		};
+		38E8753527554D5800975559 /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				38E8753627554D5800975559 /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+		38E875482755505800975559 /* Views */ = {
+			isa = PBXGroup;
+			children = (
+				38E8752B27554D5700975559 /* MainView.swift */,
+				38E8754B2755548F00975559 /* WatchStateModel.swift */,
+				38E87549275550BB00975559 /* CarbsView.swift */,
+				38E8755A27568A6700975559 /* ConfirmationView.swift */,
+				38E8757A2757B1C300975559 /* TempTargetsView.swift */,
+				38E8757C2757C45D00975559 /* BolusView.swift */,
+			);
+			path = Views;
+			sourceTree = "<group>";
+		};
+		38E8754D275556E100975559 /* WatchManager */ = {
+			isa = PBXGroup;
+			children = (
+				38E8754E275556FA00975559 /* WatchManager.swift */,
+			);
+			path = WatchManager;
+			sourceTree = "<group>";
+		};
 		38E98A1A25F52C9300C0CED0 /* Logger */ = {
 			isa = PBXGroup;
 			children = (
@@ -1627,10 +1778,12 @@
 				388E595525AD948C0019842D /* Frameworks */,
 				388E595625AD948C0019842D /* Resources */,
 				3821ECD025DC703C00BC42AD /* Embed Frameworks */,
+				38E8753D27554D5900975559 /* Embed Watch Content */,
 			);
 			buildRules = (
 			);
 			dependencies = (
+				38E8753B27554D5900975559 /* PBXTargetDependency */,
 			);
 			name = FreeAPS;
 			packageProductDependencies = (
@@ -1643,6 +1796,43 @@
 			productReference = 388E595825AD948C0019842D /* FreeAPS.app */;
 			productType = "com.apple.product-type.application";
 		};
+		38E8751B27554D5500975559 /* FreeAPSWatch */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 38E8754427554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch" */;
+			buildPhases = (
+				38E8751A27554D5500975559 /* Resources */,
+				38E8754027554D5900975559 /* Embed App Extensions */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				38E8752727554D5700975559 /* PBXTargetDependency */,
+			);
+			name = FreeAPSWatch;
+			productName = FreeAPSWatch;
+			productReference = 38E8751C27554D5500975559 /* FreeAPSWatch.app */;
+			productType = "com.apple.product-type.application.watchapp2";
+		};
+		38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 38E8754327554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch WatchKit Extension" */;
+			buildPhases = (
+				38E8752027554D5700975559 /* Sources */,
+				38E8752127554D5700975559 /* Frameworks */,
+				38E8752227554D5700975559 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "FreeAPSWatch WatchKit Extension";
+			packageProductDependencies = (
+				38E8755727567AE400975559 /* SwiftDate */,
+			);
+			productName = "FreeAPSWatch WatchKit Extension";
+			productReference = 38E8752427554D5700975559 /* FreeAPSWatch WatchKit Extension.appex */;
+			productType = "com.apple.product-type.watchkit2-extension";
+		};
 		38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = 38FCF3F425E9028E0078B0D1 /* Build configuration list for PBXNativeTarget "FreeAPSTests" */;
@@ -1667,12 +1857,18 @@
 		388E595025AD948C0019842D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastSwiftUpdateCheck = 1240;
+				LastSwiftUpdateCheck = 1310;
 				LastUpgradeCheck = 1240;
 				TargetAttributes = {
 					388E595725AD948C0019842D = {
 						CreatedOnToolsVersion = 12.3;
 					};
+					38E8751B27554D5500975559 = {
+						CreatedOnToolsVersion = 13.1;
+					};
+					38E8752327554D5700975559 = {
+						CreatedOnToolsVersion = 13.1;
+					};
 					38FCF3EC25E9028E0078B0D1 = {
 						CreatedOnToolsVersion = 12.4;
 						TestTargetID = 388E595725AD948C0019842D;
@@ -1719,6 +1915,8 @@
 			targets = (
 				388E595725AD948C0019842D /* FreeAPS */,
 				38FCF3EC25E9028E0078B0D1 /* FreeAPSTests */,
+				38E8751B27554D5500975559 /* FreeAPSWatch */,
+				38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */,
 			);
 		};
 /* End PBXProject section */
@@ -1736,6 +1934,25 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		38E8751A27554D5500975559 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38E8754527554D8800975559 /* Assets.xcassets in Resources */,
+				38E8751F27554D5700975559 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		38E8752227554D5700975559 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38E8753727554D5900975559 /* Preview Assets.xcassets in Resources */,
+				38E8754627554D8A00975559 /* Assets.xcassets in Resources */,
+				38E8753427554D5800975559 /* Assets.xcassets in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		38FCF3EB25E9028E0078B0D1 /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -1939,12 +2156,14 @@
 				5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */,
 				E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */,
 				9702FF92A09C53942F20D7EA /* TargetsEditorRootView.swift in Sources */,
+				38E8754F275556FA00975559 /* WatchManager.swift in Sources */,
 				A228DF96647338139F152B15 /* PreferencesEditorDataFlow.swift in Sources */,
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */,
 				DD399FB31EACB9343C944C4C /* PreferencesEditorStateModel.swift in Sources */,
 				44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */,
 				E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */,
+				38E8755427561E9800975559 /* DataFlow.swift in Sources */,
 				38E44522274E3DDC00EC9A94 /* NetworkReachabilityManager.swift in Sources */,
 				A6F097A14CAAE0CE0D11BE1B /* AddCarbsProvider.swift in Sources */,
 				33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */,
@@ -2000,6 +2219,28 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		38E8752027554D5700975559 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				38E8757927579D9200975559 /* Publisher.swift in Sources */,
+				38E8755B27568A6800975559 /* ConfirmationView.swift in Sources */,
+				38E8757D2757C45D00975559 /* BolusView.swift in Sources */,
+				38E8752E27554D5700975559 /* NotificationController.swift in Sources */,
+				38E8754C2755548F00975559 /* WatchStateModel.swift in Sources */,
+				38E8754A275550BB00975559 /* CarbsView.swift in Sources */,
+				38E8752C27554D5700975559 /* MainView.swift in Sources */,
+				38E8755127555D0500975559 /* DataFlow.swift in Sources */,
+				38E8753227554D5700975559 /* ComplicationController.swift in Sources */,
+				38E8752A27554D5700975559 /* FreeAPSApp.swift in Sources */,
+				38E8757B2757B1C300975559 /* TempTargetsView.swift in Sources */,
+				38E8753027554D5700975559 /* NotificationView.swift in Sources */,
+				38E8757E2758C86A00975559 /* ConvenienceExtensions.swift in Sources */,
+				38E8754727554DF100975559 /* Color+Extensions.swift in Sources */,
+				38E8755927567CA600975559 /* Decimal+Extensions.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		38FCF3E925E9028E0078B0D1 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -2011,6 +2252,16 @@
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
+		38E8752727554D5700975559 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 38E8752327554D5700975559 /* FreeAPSWatch WatchKit Extension */;
+			targetProxy = 38E8752627554D5700975559 /* PBXContainerItemProxy */;
+		};
+		38E8753B27554D5900975559 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = 38E8751B27554D5500975559 /* FreeAPSWatch */;
+			targetProxy = 38E8753A27554D5900975559 /* PBXContainerItemProxy */;
+		};
 		38FCF3F325E9028E0078B0D1 /* PBXTargetDependency */ = {
 			isa = PBXTargetDependency;
 			target = 388E595725AD948C0019842D /* FreeAPS */;
@@ -2203,9 +2454,11 @@
 		388E596825AD948E0019842D /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
 				APP_GROUP_ID = "$(APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
@@ -2237,9 +2490,11 @@
 		388E596925AD948E0019842D /* Release */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
 				APP_GROUP_ID = "$(APP_GROUP_ID)";
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
 				CODE_SIGN_ENTITLEMENTS = FreeAPS/Resources/FreeAPS.entitlements;
 				CODE_SIGN_STYLE = Automatic;
 				CURRENT_PROJECT_VERSION = 1;
@@ -2268,6 +2523,136 @@
 			};
 			name = Release;
 		};
+		38E8753E27554D5900975559 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)";
+				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
+				GENERATE_INFOPLIST_FILE = YES;
+				IBSC_MODULE = FreeAPSWatch_WatchKit_Extension;
+				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(BUNDLE_IDENTIFIER)";
+				MARKETING_VERSION = 1;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+			};
+			name = Debug;
+		};
+		38E8753F27554D5900975559 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = FreeAPSWatch/FreeAPSWatch.entitlements;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)";
+				DEVELOPMENT_TEAM = "${DEVELOPER_TEAM}";
+				GENERATE_INFOPLIST_FILE = YES;
+				IBSC_MODULE = FreeAPSWatch_WatchKit_Extension;
+				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME)";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
+				INFOPLIST_KEY_WKCompanionAppBundleIdentifier = "$(BUNDLE_IDENTIFIER)";
+				MARKETING_VERSION = 1;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+			};
+			name = Release;
+		};
+		38E8754127554D5900975559 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
+				ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = "FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)";
+				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist";
+				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME) WatchKit Extension";
+				INFOPLIST_KEY_CLKComplicationPrincipalClass = ComplicationController;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				MARKETING_VERSION = 1;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp.watchkitextension";
+				PRODUCT_NAME = "${TARGET_NAME}";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+			};
+			name = Debug;
+		};
+		38E8754227554D5900975559 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APP_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
+				ASSETCATALOG_COMPILER_COMPLICATION_NAME = Complication;
+				BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER)";
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CODE_SIGN_ENTITLEMENTS = "FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = "$(BUILD_VERSION)";
+				DEVELOPMENT_ASSET_PATHS = "\"FreeAPSWatch WatchKit Extension/Preview Content\"";
+				DEVELOPMENT_TEAM = BA7ZHP4963;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "FreeAPSWatch WatchKit Extension/Info.plist";
+				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME) WatchKit Extension";
+				INFOPLIST_KEY_CLKComplicationPrincipalClass = ComplicationController;
+				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+					"@executable_path/../../Frameworks",
+				);
+				MARKETING_VERSION = 1;
+				PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER).watchkitapp.watchkitextension";
+				PRODUCT_NAME = "${TARGET_NAME}";
+				SDKROOT = watchos;
+				SKIP_INSTALL = YES;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = 4;
+				WATCHOS_DEPLOYMENT_TARGET = 8.0;
+			};
+			name = Release;
+		};
 		38FCF3F525E9028E0078B0D1 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -2331,6 +2716,24 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		38E8754327554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch WatchKit Extension" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				38E8754127554D5900975559 /* Debug */,
+				38E8754227554D5900975559 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		38E8754427554D5900975559 /* Build configuration list for PBXNativeTarget "FreeAPSWatch" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				38E8753E27554D5900975559 /* Debug */,
+				38E8753F27554D5900975559 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		38FCF3F425E9028E0078B0D1 /* Build configuration list for PBXNativeTarget "FreeAPSTests" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (
@@ -2389,6 +2792,11 @@
 			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
 			productName = SwiftDate;
 		};
+		38E8755727567AE400975559 /* SwiftDate */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 38B17B6425DD90E0005CAE3D /* XCRemoteSwiftPackageReference "SwiftDate" */;
+			productName = SwiftDate;
+		};
 /* End XCSwiftPackageProductDependency section */
 	};
 	rootObject = 388E595025AD948C0019842D /* Project object */;

+ 15 - 0
FreeAPS.xcodeproj/xcuserdata/i.valkou.xcuserdatad/xcschemes/xcschememanagement.plist

@@ -30,6 +30,21 @@
 			<key>orderHint</key>
 			<integer>0</integer>
 		</dict>
+		<key>FreeAPSWatch (Complication).xcscheme_^#shared#^_</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>132</integer>
+		</dict>
+		<key>FreeAPSWatch (Notification).xcscheme_^#shared#^_</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>131</integer>
+		</dict>
+		<key>FreeAPSWatch.xcscheme_^#shared#^_</key>
+		<dict>
+			<key>orderHint</key>
+			<integer>130</integer>
+		</dict>
 		<key>ReactiveSwift (Playground) 1.xcscheme</key>
 		<dict>
 			<key>isShown</key>

+ 1 - 0
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -37,6 +37,7 @@ import Swinject
         _ = resolver.resolve(FetchAnnouncementsManager.self)!
         _ = resolver.resolve(CalendarManager.self)!
         _ = resolver.resolve(UserNotificationsManager.self)!
+        _ = resolver.resolve(WatchManager.self)!
     }
 
     init() {

+ 1 - 0
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -15,5 +15,6 @@ final class ServiceAssembly: Assembly {
         }
         container.register(CalendarManager.self) { r in BaseCalendarManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
+        container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
     }
 }

+ 12 - 0
FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Add Carbs";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Amount Carbs";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Other";
 
+/* Whatch app alert */
+"Set temp targets presets on iPhone first" = "Set temp targets presets on iPhone first";
+
+/* Updating Watch app */
+"Updating..." = "Updating...";
+
+/* Header for Temp targets in Watch app */
+"Temp Targets" = "Temp Targets";
+
 
 /* Calendar and Libre transmitter settings --------------*/
 

+ 12 - 0
FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Lägg till kolhydrater";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentation */
+"Add Carbs " = "Kolhydrater";
+
 /*  */
 "Amount Carbs" = "Mängd kolhydrater";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Annan";
 
+/* Whatch app alert */
+"Set temp targets presets on iPhone first" = "Spara ett tillfälligt målvärde på din iPhone först";
+
+/* Updating Watch app */
+"Updating..." = "Uppdaterar...";
+
+/* Header for Temp targets in Watch app */
+"Temp Targets" = "Målvärden";
+
 
 /* Calendar and Libre transmitter settings --------------*/
 /* */

+ 7 - 0
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -4,6 +4,7 @@ import Swinject
 protocol SettingsManager: AnyObject {
     var settings: FreeAPSSettings { get set }
     var preferences: Preferences { get }
+    var pumpSettings: PumpSettings { get }
 }
 
 protocol SettingsObserver {
@@ -45,4 +46,10 @@ final class BaseSettingsManager: SettingsManager, Injectable {
             ?? Preferences(from: OpenAPS.defaults(for: OpenAPS.Settings.preferences))
             ?? Preferences()
     }
+
+    var pumpSettings: PumpSettings {
+        storage.retrieve(OpenAPS.Settings.settings, as: PumpSettings.self)
+            ?? PumpSettings(from: OpenAPS.defaults(for: OpenAPS.Settings.settings))
+            ?? PumpSettings(insulinActionCurve: 5, maxBolus: 10, maxBasal: 2)
+    }
 }

+ 338 - 0
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -0,0 +1,338 @@
+import Foundation
+import Swinject
+import WatchConnectivity
+
+protocol WatchManager {}
+
+final class BaseWatchManager: NSObject, WatchManager, Injectable {
+    private let session: WCSession
+    private var state = WatchState()
+    private let processQueue = DispatchQueue(label: "BaseWatchManager.processQueue")
+
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var apsManager: APSManager!
+    @Injected() private var storage: FileStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
+    @Injected() private var tempTargetsStorage: TempTargetsStorage!
+
+    private var lifetime = Lifetime()
+
+    init(resolver: Resolver, session: WCSession = .default) {
+        self.session = session
+        super.init()
+        injectServices(resolver)
+
+        if WCSession.isSupported() {
+            session.delegate = self
+            session.activate()
+        }
+
+        broadcaster.register(GlucoseObserver.self, observer: self)
+        broadcaster.register(SuggestionObserver.self, observer: self)
+        broadcaster.register(SettingsObserver.self, observer: self)
+        broadcaster.register(PumpHistoryObserver.self, observer: self)
+        broadcaster.register(PumpSettingsObserver.self, observer: self)
+        broadcaster.register(BasalProfileObserver.self, observer: self)
+        broadcaster.register(TempTargetsObserver.self, observer: self)
+        broadcaster.register(CarbsObserver.self, observer: self)
+        broadcaster.register(EnactedSuggestionObserver.self, observer: self)
+        broadcaster.register(PumpBatteryObserver.self, observer: self)
+        broadcaster.register(PumpReservoirObserver.self, observer: self)
+
+        configureState()
+    }
+
+    private func configureState() {
+        processQueue.async {
+            let glucoseValues = self.glucoseText()
+            self.state.glucose = glucoseValues.glucose
+            self.state.trend = glucoseValues.trend
+            self.state.delta = glucoseValues.delta
+            self.state.glucoseDate = self.glucoseStorage.recent().last?.dateString
+            self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self
+                .apsManager.lastLoopDate
+            self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement
+            self.state.maxCOB = self.settingsManager.preferences.maxCOB
+            self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus
+            self.state.carbsRequired = self.suggestion?.carbsReq
+
+            let inslinRequired = self.suggestion?.insulinReq ?? 0
+            self.state.bolusRecommended = self.apsManager
+                .roundBolus(amount: max(inslinRequired * self.settingsManager.settings.insulinReqFraction, 0))
+
+            self.state.iob = self.suggestion?.iob
+            self.state.cob = self.suggestion?.cob
+            self.state.tempTargets = self.tempTargetsStorage.presets()
+                .map { target -> TempTargetWatchPreset in
+                    let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in
+                        guard currentTarget.id == target.id else { return nil }
+                        let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60))
+                        return date > Date() ? date : nil
+                    }
+                    return TempTargetWatchPreset(
+                        name: target.displayName,
+                        id: target.id,
+                        description: self.descriptionForTarget(target),
+                        until: untilDate
+                    )
+                }
+            self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs
+            self.state.eventualBG = self.evetualBGStraing()
+
+            self.sendState()
+        }
+    }
+
+    private func sendState() {
+        dispatchPrecondition(condition: .onQueue(processQueue))
+        guard let data = try? JSONEncoder().encode(state) else {
+            warning(.service, "Cannot encode watch state")
+            return
+        }
+        guard session.isReachable else {
+            warning(.service, "WCSession is not reachable")
+            return
+        }
+        session.sendMessageData(data, replyHandler: nil) { error in
+            warning(.service, "Cannot send message to watch", error: error)
+        }
+    }
+
+    private func glucoseText() -> (glucose: String, trend: String, delta: String) {
+        let glucose = glucoseStorage.recent()
+
+        guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return ("--", "--", "--") }
+
+        let delta = glucose.count >= 2 ? glucoseValue - (glucose[glucose.count - 2].glucose ?? 0) : nil
+
+        let units = settingsManager.settings.units
+        let glucoseText = glucoseFormatter
+            .string(from: Double(
+                units == .mmolL ? glucoseValue
+                    .asMmolL : Decimal(glucoseValue)
+            ) as NSNumber)!
+        let directionText = lastGlucose.direction?.symbol ?? "↔︎"
+        let deltaText = delta
+            .map {
+                self.deltaFormatter
+                    .string(from: Double(
+                        units == .mmolL ? $0
+                            .asMmolL : Decimal($0)
+                    ) as NSNumber)!
+            } ?? "--"
+
+        return (glucoseText, directionText, deltaText)
+    }
+
+    private func descriptionForTarget(_ target: TempTarget) -> String {
+        let units = settingsManager.settings.units
+
+        var low = target.targetBottom
+        var high = target.targetTop
+        if units == .mmolL {
+            low = low?.asMmolL
+            high = high?.asMmolL
+        }
+
+        let description =
+            "\(targetFormatter.string(from: (low ?? 0) as NSNumber)!) - \(targetFormatter.string(from: (high ?? 0) as NSNumber)!)" +
+            " for \(targetFormatter.string(from: target.duration as NSNumber)!) min"
+
+        return description
+    }
+
+    private func evetualBGStraing() -> String? {
+        guard let eventualBG = suggestion?.eventualBG else {
+            return nil
+        }
+        let units = settingsManager.settings.units
+        return "⇢ " + eventualFormatter.string(
+            from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber
+        )!
+    }
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+        return formatter
+    }
+
+    private var eventualFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+
+    private var targetFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 1
+        return formatter
+    }
+
+    private var suggestion: Suggestion? {
+        storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self)
+    }
+
+    private var enactedSuggestion: Suggestion? {
+        storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self)
+    }
+}
+
+extension BaseWatchManager: WCSessionDelegate {
+    func sessionDidBecomeInactive(_: WCSession) {}
+
+    func sessionDidDeactivate(_: WCSession) {}
+
+    func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
+        debug(.service, "WCSession is activated: \(state == .activated)")
+    }
+
+    func session(_: WCSession, didReceiveMessage message: [String: Any]) {
+        debug(.service, "WCSession got message: \(message)")
+
+        if let stateRequest = message["stateRequest"] as? Bool, stateRequest {
+            processQueue.async {
+                self.sendState()
+            }
+        }
+    }
+
+    func session(_: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
+        debug(.service, "WCSession got message with reply handler: \(message)")
+
+        if let carbs = message["carbs"] as? Double, carbs > 0 {
+            carbsStorage.storeCarbs([
+                CarbsEntry(createdAt: Date(), carbs: Decimal(carbs), enteredBy: CarbsEntry.manual)
+            ])
+
+            if settingsManager.settings.skipBolusScreenAfterCarbs {
+                apsManager.determineBasalSync()
+                replyHandler(["confirmation": true])
+                return
+            } else {
+                apsManager.determineBasal()
+                    .sink { _ in
+                        replyHandler(["confirmation": true])
+                    }
+                    .store(in: &lifetime)
+                return
+            }
+        }
+
+        if let tempTargetID = message["tempTarget"] as? String {
+            if var preset = tempTargetsStorage.presets().first(where: { $0.id == tempTargetID }) {
+                preset.createdAt = Date()
+                tempTargetsStorage.storeTempTargets([preset])
+                replyHandler(["confirmation": true])
+                return
+            } else if tempTargetID == "cancel" {
+                let entry = TempTarget(
+                    name: TempTarget.cancel,
+                    createdAt: Date(),
+                    targetTop: 0,
+                    targetBottom: 0,
+                    duration: 0,
+                    enteredBy: TempTarget.manual,
+                    reason: TempTarget.cancel
+                )
+                tempTargetsStorage.storeTempTargets([entry])
+                replyHandler(["confirmation": true])
+                return
+            }
+        }
+
+        if let bolus = message["bolus"] as? Double, bolus > 0 {
+            apsManager.enactBolus(amount: bolus, isSMB: false)
+            replyHandler(["confirmation": true])
+            return
+        }
+
+        replyHandler(["confirmation": false])
+    }
+
+    func session(_: WCSession, didReceiveMessageData _: Data) {}
+
+    func sessionReachabilityDidChange(_ session: WCSession) {
+        if session.isReachable {
+            processQueue.async {
+                self.sendState()
+            }
+        }
+    }
+}
+
+extension BaseWatchManager:
+    GlucoseObserver,
+    SuggestionObserver,
+    SettingsObserver,
+    PumpHistoryObserver,
+    PumpSettingsObserver,
+    BasalProfileObserver,
+    TempTargetsObserver,
+    CarbsObserver,
+    EnactedSuggestionObserver,
+    PumpBatteryObserver,
+    PumpReservoirObserver
+{
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        configureState()
+    }
+
+    func suggestionDidUpdate(_: Suggestion) {
+        configureState()
+    }
+
+    func settingsDidChange(_: FreeAPSSettings) {
+        configureState()
+    }
+
+    func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) {
+        // TODO:
+    }
+
+    func pumpSettingsDidChange(_: PumpSettings) {
+        configureState()
+    }
+
+    func basalProfileDidChange(_: [BasalProfileEntry]) {
+        // TODO:
+    }
+
+    func tempTargetsDidUpdate(_: [TempTarget]) {
+        configureState()
+    }
+
+    func carbsDidUpdate(_: [CarbsEntry]) {
+        // TODO:
+    }
+
+    func enactedSuggestionDidUpdate(_: Suggestion) {
+        configureState()
+    }
+
+    func pumpBatteryDidChange(_: Battery) {
+        // TODO:
+    }
+
+    func pumpReservoirDidChange(_: Decimal) {
+        // TODO:
+    }
+}

+ 26 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/FreeAPS-X.svg


+ 53 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json

@@ -0,0 +1,53 @@
+{
+  "assets" : [
+    {
+      "filename" : "Circular.imageset",
+      "idiom" : "watch",
+      "role" : "circular"
+    },
+    {
+      "filename" : "Extra Large.imageset",
+      "idiom" : "watch",
+      "role" : "extra-large"
+    },
+    {
+      "filename" : "Graphic Bezel.imageset",
+      "idiom" : "watch",
+      "role" : "graphic-bezel"
+    },
+    {
+      "filename" : "Graphic Circular.imageset",
+      "idiom" : "watch",
+      "role" : "graphic-circular"
+    },
+    {
+      "filename" : "Graphic Corner.imageset",
+      "idiom" : "watch",
+      "role" : "graphic-corner"
+    },
+    {
+      "filename" : "Graphic Extra Large.imageset",
+      "idiom" : "watch",
+      "role" : "graphic-extra-large"
+    },
+    {
+      "filename" : "Graphic Large Rectangular.imageset",
+      "idiom" : "watch",
+      "role" : "graphic-large-rectangular"
+    },
+    {
+      "filename" : "Modular.imageset",
+      "idiom" : "watch",
+      "role" : "modular"
+    },
+    {
+      "filename" : "Utilitarian.imageset",
+      "idiom" : "watch",
+      "role" : "utilitarian"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 26 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-2.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/FreeAPS-X-2.svg


+ 21 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-3.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/FreeAPS-X-3.svg


+ 21 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-3.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/FreeAPS-X-3.svg


+ 21 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-4.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/FreeAPS-X-4.svg


+ 26 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-5.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/FreeAPS-X-5.svg


+ 20 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json

@@ -0,0 +1,20 @@
+{
+  "images" : [
+    {
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

+ 26 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-6.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/FreeAPS-X-6.svg


+ 26 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json

@@ -0,0 +1,26 @@
+{
+  "images" : [
+    {
+      "filename" : "FreeAPS-X-7.svg",
+      "idiom" : "watch",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : "<=145"
+    },
+    {
+      "idiom" : "watch",
+      "scale" : "2x",
+      "screen-width" : ">183"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  },
+  "properties" : {
+    "auto-scaling" : "auto"
+  }
+}

Разлика између датотеке није приказан због своје велике величине
+ 12 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/FreeAPS-X-7.svg


+ 6 - 0
FreeAPSWatch WatchKit Extension/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 59 - 0
FreeAPSWatch WatchKit Extension/ComplicationController.swift

@@ -0,0 +1,59 @@
+import ClockKit
+
+class ComplicationController: NSObject, CLKComplicationDataSource {
+    // MARK: - Complication Configuration
+
+    func getComplicationDescriptors(handler: @escaping ([CLKComplicationDescriptor]) -> Void) {
+        let descriptors = [
+            CLKComplicationDescriptor(
+                identifier: "complication",
+                displayName: "FreeAPS X",
+                supportedFamilies: CLKComplicationFamily.allCases
+            )
+            // Multiple complication support can be added here with more descriptors
+        ]
+
+        // Call the handler with the currently supported complication descriptors
+        handler(descriptors)
+    }
+
+    func handleSharedComplicationDescriptors(_: [CLKComplicationDescriptor]) {
+        // Do any necessary work to support these newly shared complication descriptors
+    }
+
+    // MARK: - Timeline Configuration
+
+    func getTimelineEndDate(for _: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
+        // Call the handler with the last entry date you can currently provide or nil if you can't support future timelines
+        handler(nil)
+    }
+
+    func getPrivacyBehavior(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationPrivacyBehavior) -> Void) {
+        // Call the handler with your desired behavior when the device is locked
+        handler(.showOnLockScreen)
+    }
+
+    // MARK: - Timeline Population
+
+    func getCurrentTimelineEntry(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
+        // Call the handler with the current timeline entry
+        handler(nil)
+    }
+
+    func getTimelineEntries(
+        for _: CLKComplication,
+        after _: Date,
+        limit _: Int,
+        withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void
+    ) {
+        // Call the handler with the timeline entries after the given date
+        handler(nil)
+    }
+
+    // MARK: - Sample Templates
+
+    func getLocalizableSampleTemplate(for _: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
+        // This method will be called once per supported complication, and the results will be cached
+        handler(nil)
+    }
+}

+ 26 - 0
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -0,0 +1,26 @@
+import Foundation
+
+struct WatchState: Codable {
+    var glucose: String?
+    var trend: String?
+    var delta: String?
+    var glucoseDate: Date?
+    var lastLoopDate: Date?
+    var bolusIncrement: Decimal?
+    var maxCOB: Decimal?
+    var maxBolus: Decimal?
+    var carbsRequired: Decimal?
+    var bolusRecommended: Decimal?
+    var iob: Decimal?
+    var cob: Decimal?
+    var tempTargets: [TempTargetWatchPreset] = []
+    var bolusAfterCarbs: Bool?
+    var eventualBG: String?
+}
+
+struct TempTargetWatchPreset: Codable, Identifiable {
+    let name: String
+    let id: String
+    let description: String
+    let until: Date?
+}

+ 15 - 0
FreeAPSWatch WatchKit Extension/FreeAPSApp.swift

@@ -0,0 +1,15 @@
+import SwiftUI
+
+@main struct FreeAPSApp: App {
+    @StateObject var state = WatchStateModel()
+
+    @SceneBuilder var body: some Scene {
+        WindowGroup {
+            NavigationView {
+                MainView()
+            }.environmentObject(state)
+        }
+
+//        WKNotificationScene(controller: NotificationController.self, category: "FreeAPSCategory")
+    }
+}

+ 10 - 0
FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>$(APP_GROUP_ID)</string>
+	</array>
+</dict>
+</plist>

+ 16 - 0
FreeAPSWatch WatchKit Extension/Info.plist

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>NSExtension</key>
+	<dict>
+		<key>NSExtensionAttributes</key>
+		<dict>
+			<key>WKAppBundleIdentifier</key>
+			<string>$(BUNDLE_IDENTIFIER).watchkitapp</string>
+		</dict>
+		<key>NSExtensionPointIdentifier</key>
+		<string>com.apple.watchkit</string>
+	</dict>
+</dict>
+</plist>

+ 25 - 0
FreeAPSWatch WatchKit Extension/NotificationController.swift

@@ -0,0 +1,25 @@
+import SwiftUI
+import UserNotifications
+import WatchKit
+
+class NotificationController: WKUserNotificationHostingController<NotificationView> {
+    override var body: NotificationView {
+        NotificationView()
+    }
+
+    override func willActivate() {
+        // This method is called when watch view controller is about to be visible to user
+        super.willActivate()
+    }
+
+    override func didDeactivate() {
+        // This method is called when watch view controller is no longer visible
+        super.didDeactivate()
+    }
+
+    override func didReceive(_: UNNotification) {
+        // This method is called when a notification needs to be presented.
+        // Implement it if you use a dynamic notification interface.
+        // Populate your dynamic notification interface as quickly as possible.
+    }
+}

+ 13 - 0
FreeAPSWatch WatchKit Extension/NotificationView.swift

@@ -0,0 +1,13 @@
+import SwiftUI
+
+struct NotificationView: View {
+    var body: some View {
+        Text("Hello, World!")
+    }
+}
+
+struct NotificationView_Previews: PreviewProvider {
+    static var previews: some View {
+        NotificationView()
+    }
+}

+ 6 - 0
FreeAPSWatch WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 20 - 0
FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns

@@ -0,0 +1,20 @@
+{
+    "aps": {
+        "alert": {
+            "body": "Test message",
+            "title": "Optional title",
+            "subtitle": "Optional subtitle"
+        },
+        "category": "myCategory",
+        "thread-id": "5280"
+    },
+    
+    "WatchKit Simulator Actions": [
+        {
+            "title": "First Button",
+            "identifier": "firstButtonAction"
+        }
+    ],
+    
+    "customKey": "Use this file to define a testing payload for your notifications. The aps dictionary specifies the category, alert text and title. The WatchKit Simulator Actions array can provide info for one or more action buttons in addition to the standard Dismiss button. Any other top level keys are custom payload. If you have multiple such JSON files in your project, you'll be able to select them when choosing to debug the notification interface of your Watch App."
+}

+ 86 - 0
FreeAPSWatch WatchKit Extension/Views/BolusView.swift

@@ -0,0 +1,86 @@
+import SwiftUI
+
+struct BolusView: View {
+    @EnvironmentObject var state: WatchStateModel
+
+    @State var steps = 0.0
+
+    var numberFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimum = 0
+        formatter.maximum = Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)) as NSNumber
+        formatter.maximumFractionDigits = 2
+        formatter.minimumFractionDigits = 2
+        formatter.allowsFloats = true
+        return formatter
+    }
+
+    var body: some View {
+        VStack(spacing: 16) {
+            HStack {
+                Button {
+                    let newValue = steps - 1
+                    steps = max(newValue, 0)
+                } label: { Image(systemName: "minus") }
+                    .frame(width: 50)
+                Spacer()
+                Text(numberFormatter.string(from: (steps * Double(state.bolusIncrement ?? 0.1)) as NSNumber)! + NSLocalizedString(" U", comment: "Abbreviation for insulin unit"))
+                    .font(.headline)
+                    .focusable(true)
+                    .digitalCrownRotation(
+                    $steps,
+                    from: 0,
+                    through: Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)),
+                    by: 1,
+                    sensitivity: .medium,
+                    isContinuous: false,
+                    isHapticFeedbackEnabled: true
+                )
+                Spacer()
+                Button {
+                    let newValue = steps + 1
+                    steps = min(newValue, Double((state.maxBolus ?? 5) / (state.bolusIncrement ?? 0.1)))
+                } label: { Image(systemName: "plus") }
+                    .frame(width: 50)
+            }
+
+            HStack {
+                Button {
+                    state.isBolusViewActive = false
+                }
+                label: {
+                    Image(systemName: "xmark.circle.fill")
+                        .resizable()
+                        .foregroundColor(.loopRed)
+                        .frame(width: 30, height: 30)
+                }
+                Button {
+                    enactBolus()
+                }
+                label: {
+                    Image(systemName: "checkmark.circle.fill")
+                        .resizable()
+                        .foregroundColor(.loopGreen)
+                        .frame(width: 30, height: 30)
+                }
+                .disabled(steps <= 0)
+            }
+        }
+        .navigationTitle("Enact Bolus")
+        .onAppear {
+            steps = Double((state.bolusRecommended ?? 0) / (state.bolusIncrement ?? 0.1))
+        }
+    }
+
+    private func enactBolus() {
+        let amount = steps * Double(state.bolusIncrement ?? 0.1)
+        state.enactBolus(amount: amount)
+    }
+}
+
+struct BolusView_Previews: PreviewProvider {
+    static var previews: some View {
+        BolusView().environmentObject(WatchStateModel())
+    }
+}

+ 72 - 0
FreeAPSWatch WatchKit Extension/Views/CarbsView.swift

@@ -0,0 +1,72 @@
+import SwiftUI
+
+struct CarbsView: View {
+    @EnvironmentObject var state: WatchStateModel
+
+    @State var amount = 0.0
+
+    var numberFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.minimum = 0
+        formatter.maximum = (state.maxCOB ?? 120) as NSNumber
+        formatter.maximumFractionDigits = 0
+        formatter.allowsFloats = false
+        return formatter
+    }
+
+    var body: some View {
+        VStack(spacing: 16) {
+            HStack {
+                Button {
+                    let newValue = amount - 5
+                    amount = max(newValue, 0)
+                } label: { Image(systemName: "minus") }
+                    .frame(width: 50)
+                Spacer()
+                Text(numberFormatter.string(from: amount as NSNumber)! + " g")
+                    .font(.title2)
+                    .focusable(true)
+                    .digitalCrownRotation(
+                        $amount,
+                        from: 0,
+                        through: Double(state.maxCOB ?? 120),
+                        by: 1,
+                        sensitivity: .medium,
+                        isContinuous: false,
+                        isHapticFeedbackEnabled: true
+                    )
+                Spacer()
+                Button {
+                    let newValue = amount + 5
+                    amount = min(newValue, Double(state.maxCOB ?? 120))
+                } label: { Image(systemName: "plus") }
+                    .frame(width: 50)
+            }
+            Button {
+                state.addCarbs(Int(amount))
+            }
+            label: {
+                HStack {
+                    Image("carbs", bundle: nil)
+                        .renderingMode(.template)
+                        .resizable()
+                        .frame(width: 24, height: 24)
+                        .foregroundColor(.loopGreen)
+                    Text("Add Carbs ")
+                }
+            }
+            .disabled(amount <= 0)
+        }
+        .navigationTitle("Add Carbs ")
+        .onAppear {
+            amount = Double(state.carbsRequired ?? 0)
+        }
+    }
+}
+
+struct CarbsView_Previews: PreviewProvider {
+    static var previews: some View {
+        CarbsView().environmentObject(WatchStateModel())
+    }
+}

+ 95 - 0
FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift

@@ -0,0 +1,95 @@
+import SwiftUI
+
+struct ConfirmationView: View {
+    @Binding var success: Bool?
+
+    var body: some View {
+        ZStack {
+            Group {
+                Image(systemName: "checkmark.circle.fill")
+                    .resizable()
+                    .foregroundColor(.loopGreen)
+                    .opacity(success == true ? 1.0 : 0.0)
+                    .scaleEffect(success == true ? 1.0 : 0.0)
+
+                Image(systemName: "xmark.circle.fill")
+                    .resizable()
+                    .foregroundColor(.loopRed)
+                    .opacity(success == false ? 1.0 : 0.0)
+                    .scaleEffect(success == false ? 1.0 : 0.0)
+
+                BlinkingView(count: 10, size: 10)
+                    .opacity(success == nil ? 1.0 : 0.0)
+                    .scaleEffect(success == nil ? 1.0 : 0.0)
+            }
+            .frame(width: 50, height: 50)
+        }
+        .frame(maxWidth: .infinity, maxHeight: .infinity)
+        .onTapGesture {
+            toggleState()
+        }
+    }
+
+    func toggleState() {
+        withAnimation(.easeIn.speed(1)) {
+            success = success == nil ? true : success == true ? false : nil
+        }
+    }
+}
+
+struct ConfirmationView_Previews: PreviewProvider {
+    struct Container: View {
+        @State var success: Bool?
+
+        var body: some View {
+            ConfirmationView(success: $success)
+        }
+    }
+
+    static var previews: some View {
+        Container()
+    }
+}
+
+struct BlinkingView: View {
+    let count: UInt
+    let size: CGFloat
+
+    var body: some View {
+        GeometryReader { geometry in
+            ForEach(0 ..< Int(count)) { index in
+                item(forIndex: index, in: geometry.size)
+                    .frame(width: geometry.size.width, height: geometry.size.height)
+            }
+        }
+        .animation(.none, value: false)
+        .aspectRatio(contentMode: .fit)
+        .onAppear {
+            scale = 1
+            opacity = 1
+        }
+    }
+
+    @State var scale = 0.5
+    @State var opacity = 0.25
+
+    func animation(index: Int) -> Animation {
+        Animation
+            .default
+            .repeatCount(.max, autoreverses: true)
+            .delay(Double(index) / Double(count) / 2)
+    }
+
+    private func item(forIndex index: Int, in geometrySize: CGSize) -> some View {
+        let angle = 2 * CGFloat.pi / CGFloat(count) * CGFloat(index)
+        let x = (geometrySize.width / 2 - size / 2) * cos(angle)
+        let y = (geometrySize.height / 2 - size / 2) * sin(angle)
+        return Circle()
+            .frame(width: size, height: size)
+            .scaleEffect(scale)
+            .opacity(opacity)
+            .animation(animation(index: index), value: scale)
+            .animation(animation(index: index), value: opacity)
+            .offset(x: x, y: y)
+    }
+}

+ 209 - 0
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -0,0 +1,209 @@
+import SwiftDate
+import SwiftUI
+
+struct MainView: View {
+    private enum Config {
+        static let lag: TimeInterval = 30
+    }
+
+    @EnvironmentObject var state: WatchStateModel
+
+    @State var isCarbsActive = false
+    @State var isTargetsActive = false
+    @State var isBolusActive = false
+
+    var body: some View {
+        ZStack(alignment: .topLeading) {
+            if state.timerDate.timeIntervalSince(state.lastUpdate) > 10 {
+                HStack {
+                    withAnimation {
+                        BlinkingView(count: 5, size: 3)
+                            .frame(width: 14, height: 14)
+                            .padding(2)
+                    }
+                    Text("Updating...").font(.caption2).foregroundColor(.secondary)
+                }
+            }
+            VStack {
+                header
+                Spacer()
+                buttons
+            }
+
+            if state.isConfirmationViewActive {
+                ConfirmationView(success: $state.confirmationSuccess)
+                    .background(Rectangle().fill(.black))
+            }
+        }
+        .frame(maxHeight: .infinity)
+        .padding()
+        .onReceive(state.timer) { date in
+            state.timerDate = date
+            state.requestState()
+        }
+        .onAppear {
+            state.requestState()
+        }
+    }
+
+    var header: some View {
+        VStack {
+            HStack(alignment: .top) {
+                VStack(alignment: .leading) {
+                    HStack {
+                        Text(state.glucose).font(.largeTitle)
+                            .scaledToFill()
+                            .minimumScaleFactor(0.5)
+                        Text(state.trend)
+                    }
+                    Text(state.delta).font(.caption2)
+                        .scaledToFill()
+                        .minimumScaleFactor(0.5)
+                        .foregroundColor(.secondary)
+                }
+                Spacer()
+
+                VStack(spacing: 0) {
+                    HStack {
+                        Circle().stroke(color, lineWidth: 6).frame(width: 30, height: 30).padding(10)
+                    }
+
+                    if state.lastLoopDate != nil {
+                        Text(timeString).font(.caption2)
+                            .scaledToFill()
+                            .minimumScaleFactor(0.5)
+                            .foregroundColor(.secondary)
+                    } else {
+                        Text("--").font(.caption2)
+                    }
+                }
+            }
+            Spacer()
+            HStack(alignment: .firstTextBaseline) {
+                HStack {
+                    Text(iobFormatter.string(from: (state.iob ?? 0) as NSNumber)! + " U")
+                        .font(.caption2)
+                        .scaledToFill()
+                        .foregroundColor(.insulin)
+                        .minimumScaleFactor(0.5)
+
+                }.minimumScaleFactor(0.5)
+                Spacer()
+                HStack {
+                    Text(iobFormatter.string(from: (state.cob ?? 0) as NSNumber)! + " g")
+                        .font(.caption2)
+                        .scaledToFill()
+                        .foregroundColor(.loopGreen)
+                        .minimumScaleFactor(0.5)
+                }
+
+                if let eventualBG = state.eventualBG.nonEmpty {
+                    Spacer()
+                    HStack {
+                        Text(eventualBG)
+                            .font(.caption2)
+                            .scaledToFill()
+                            .foregroundColor(.secondary)
+                            .minimumScaleFactor(0.5)
+                    }
+                }
+            }
+            Spacer()
+        }.padding()
+    }
+
+    var buttons: some View {
+        HStack(alignment: .center) {
+            NavigationLink(isActive: $state.isCarbsViewActive) {
+                CarbsView()
+                    .environmentObject(state)
+            } label: {
+                Image("carbs", bundle: nil)
+                    .renderingMode(.template)
+                    .resizable()
+                    .frame(width: 24, height: 24)
+                    .foregroundColor(.loopGreen)
+            }
+
+            NavigationLink(isActive: $state.isTempTargetViewActive) {
+                TempTargetsView()
+                    .environmentObject(state)
+            } label: {
+                VStack {
+                    Image("target", bundle: nil)
+                        .renderingMode(.template)
+                        .resizable()
+                        .frame(width: 24, height: 24)
+                        .foregroundColor(.loopYellow)
+                    if let until = state.tempTargets.compactMap(\.until).first, until > Date() {
+                        Text(until, style: .timer)
+                            .scaledToFill()
+                            .font(.system(size: 8))
+                    }
+                }
+            }
+
+            NavigationLink(isActive: $state.isBolusViewActive) {
+                BolusView()
+                    .environmentObject(state)
+            } label: {
+                Image("bolus", bundle: nil)
+                    .renderingMode(.template)
+                    .resizable()
+                    .frame(width: 24, height: 24)
+                    .foregroundColor(.insulin)
+            }
+        }
+    }
+
+    private var iobFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.maximumFractionDigits = 2
+        formatter.numberStyle = .decimal
+        return formatter
+    }
+
+    private var timeString: String {
+        let minAgo = Int((Date().timeIntervalSince(state.lastLoopDate ?? .distantPast) - Config.lag) / 60) + 1
+        if minAgo > 1440 {
+            return "--"
+        }
+        return "\(minAgo) " + NSLocalizedString("min", comment: "Minutes ago since last loop")
+    }
+
+    private var color: Color {
+        guard let lastLoopDate = state.lastLoopDate else {
+            return .loopGray
+        }
+        let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag
+
+        if delta <= 5.minutes.timeInterval {
+            return .loopGreen
+        } else if delta <= 10.minutes.timeInterval {
+            return .loopYellow
+        } else {
+            return .loopRed
+        }
+    }
+}
+
+struct ContentView_Previews: PreviewProvider {
+    static var previews: some View {
+        let state = WatchStateModel()
+
+        state.glucose = "888"
+        state.delta = "+888"
+        state.iob = 100.38
+        state.cob = 112.123
+        state.eventualBG = "⇢ 8,888"
+        state.lastLoopDate = Date().addingTimeInterval(-200)
+        state
+            .tempTargets =
+            [TempTargetWatchPreset(name: "Test", id: "test", description: "", until: Date().addingTimeInterval(3600 * 3))]
+
+        return Group {
+            MainView()
+            MainView().previewDevice("Apple Watch Series 5 - 40mm")
+        }.environmentObject(state)
+    }
+}

+ 54 - 0
FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift

@@ -0,0 +1,54 @@
+import SwiftUI
+
+struct TempTargetsView: View {
+    @EnvironmentObject var state: WatchStateModel
+
+    var body: some View {
+        List {
+            if state.tempTargets.isEmpty {
+                Text("Set temp targets presets on iPhone first").padding()
+            } else {
+                ForEach(state.tempTargets) { target in
+                    Button {
+                        state.enactTempTarget(id: target.id)
+                    } label: {
+                        VStack(alignment: .leading) {
+                            HStack {
+                                Text(target.name)
+                                if let until = target.until, until > Date() {
+                                    Spacer()
+                                    Text(until, style: .timer).foregroundColor(.loopGreen)
+                                }
+                            }
+                            Text(target.description).font(.caption2).foregroundColor(.secondary)
+                        }
+                    }
+                }
+            }
+
+            Button {
+                state.enactTempTarget(id: "cancel")
+            } label: {
+                Text("Cancel Temp Target")
+            }
+        }
+        .navigationTitle("Temp Targets")
+    }
+}
+
+struct TempTargetsView_Previews: PreviewProvider {
+    static var previews: some View {
+        let model = WatchStateModel()
+        model.tempTargets = [
+            TempTargetWatchPreset(
+                name: "Target 0",
+                id: UUID().uuidString,
+                description: "blablabla",
+                until: Date().addingTimeInterval(60 * 60)
+            ),
+            TempTargetWatchPreset(name: "target1", id: UUID().uuidString, description: "blablabla", until: nil),
+            TempTargetWatchPreset(name: "🤖 Target 2", id: UUID().uuidString, description: "blablabla", until: nil)
+        ]
+        return TempTargetsView().environmentObject(model)
+    }
+}

+ 162 - 0
FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift

@@ -0,0 +1,162 @@
+import Combine
+import Foundation
+import SwiftUI
+import WatchConnectivity
+
+class WatchStateModel: NSObject, ObservableObject {
+    var session: WCSession
+
+    @Published var glucose = "00"
+    @Published var trend = "→"
+    @Published var delta = "+00"
+    @Published var eventualBG = ""
+    @Published var lastLoopDate: Date?
+    @Published var glucoseDate: Date?
+    @Published var bolusIncrement: Decimal?
+    @Published var maxCOB: Decimal?
+    @Published var maxBolus: Decimal?
+    @Published var bolusRecommended: Decimal?
+    @Published var carbsRequired: Decimal?
+    @Published var iob: Decimal?
+    @Published var cob: Decimal?
+    @Published var tempTargets: [TempTargetWatchPreset] = []
+    @Published var bolusAfterCarbs = true
+
+    @Published var isCarbsViewActive = false
+    @Published var isTempTargetViewActive = false
+    @Published var isBolusViewActive = false
+    @Published var isConfirmationViewActive = false
+    @Published var confirmationSuccess: Bool?
+    @Published var lastUpdate: Date = .distantPast
+    @Published var timerDate = Date()
+
+    private var lifetime = Set<AnyCancellable>()
+    let timer = Timer.publish(every: 10, on: .main, in: .common).autoconnect()
+
+    init(session: WCSession = .default) {
+        self.session = session
+        super.init()
+
+        session.delegate = self
+        session.activate()
+    }
+
+    func addCarbs(_ carbs: Int) {
+        confirmationSuccess = nil
+        isConfirmationViewActive = true
+        isCarbsViewActive = false
+        session.sendMessage(["carbs": carbs], replyHandler: { reply in
+            self.completionHandler(reply)
+            if let ok = reply["confirmation"] as? Bool, ok, self.bolusAfterCarbs {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+                    self.isBolusViewActive = true
+                }
+            }
+        }) { error in
+            print(error.localizedDescription)
+            DispatchQueue.main.async {
+                self.confirmation(false)
+            }
+        }
+    }
+
+    func enactTempTarget(id: String) {
+        confirmationSuccess = nil
+        isConfirmationViewActive = true
+        isTempTargetViewActive = false
+        session.sendMessage(["tempTarget": id], replyHandler: completionHandler) { error in
+            print(error.localizedDescription)
+            DispatchQueue.main.async {
+                self.confirmation(false)
+            }
+        }
+    }
+
+    func enactBolus(amount: Double) {
+        confirmationSuccess = nil
+        isConfirmationViewActive = true
+        isBolusViewActive = false
+        session.sendMessage(["bolus": amount], replyHandler: completionHandler) { error in
+            print(error.localizedDescription)
+            DispatchQueue.main.async {
+                self.confirmation(false)
+            }
+        }
+    }
+
+    func requestState() {
+        guard session.activationState == .activated else {
+            session.activate()
+            return
+        }
+        session.sendMessage(["stateRequest": true], replyHandler: nil) { error in
+            print("WatchStateModel error: " + error.localizedDescription)
+        }
+    }
+
+    private func completionHandler(_ reply: [String: Any]) {
+        if let ok = reply["confirmation"] as? Bool {
+            DispatchQueue.main.async {
+                self.confirmation(ok)
+            }
+        } else {
+            DispatchQueue.main.async {
+                self.confirmation(false)
+            }
+        }
+    }
+
+    private func confirmation(_ ok: Bool) {
+        WKInterfaceDevice.current().play(ok ? .success : .failure)
+        withAnimation {
+            confirmationSuccess = ok
+        }
+
+        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
+            withAnimation {
+                self.isConfirmationViewActive = false
+            }
+        }
+    }
+
+    private func processState(_ state: WatchState) {
+        glucose = state.glucose ?? "?"
+        trend = state.trend ?? "?"
+        delta = state.delta ?? "?"
+        glucoseDate = state.glucoseDate
+        lastLoopDate = state.lastLoopDate
+        bolusIncrement = state.bolusIncrement
+        maxCOB = state.maxCOB
+        maxBolus = state.maxBolus
+        bolusRecommended = state.bolusRecommended
+        carbsRequired = state.carbsRequired
+        iob = state.iob
+        cob = state.cob
+        tempTargets = state.tempTargets
+        bolusAfterCarbs = state.bolusAfterCarbs ?? true
+        eventualBG = state.eventualBG ?? ""
+        lastUpdate = Date()
+    }
+}
+
+extension WatchStateModel: WCSessionDelegate {
+    func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
+        print("WCSession activated: \(state == .activated)")
+        requestState()
+    }
+
+    func session(_: WCSession, didReceiveMessage _: [String: Any]) {}
+
+    func sessionReachabilityDidChange(_ session: WCSession) {
+        print("WCSession Reachability: \(session.isReachable)")
+    }
+
+    func session(_: WCSession, didReceiveMessageData messageData: Data) {
+        if let state = try? JSONDecoder().decode(WatchState.self, from: messageData) {
+            DispatchQueue.main.async {
+//                WKInterfaceDevice.current().play(.click)
+                self.processState(state)
+            }
+        }
+    }
+}

+ 11 - 0
FreeAPSWatch/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 109 - 0
FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,109 @@
+{
+  "images" : [
+    {
+      "idiom" : "watch",
+      "role" : "notificationCenter",
+      "scale" : "2x",
+      "size" : "24x24",
+      "subtype" : "38mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "notificationCenter",
+      "scale" : "2x",
+      "size" : "27.5x27.5",
+      "subtype" : "42mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "companionSettings",
+      "scale" : "2x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "companionSettings",
+      "scale" : "3x",
+      "size" : "29x29"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "notificationCenter",
+      "scale" : "2x",
+      "size" : "33x33",
+      "subtype" : "45mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "appLauncher",
+      "scale" : "2x",
+      "size" : "40x40",
+      "subtype" : "38mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "appLauncher",
+      "scale" : "2x",
+      "size" : "44x44",
+      "subtype" : "40mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "appLauncher",
+      "scale" : "2x",
+      "size" : "46x46",
+      "subtype" : "41mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "appLauncher",
+      "scale" : "2x",
+      "size" : "50x50",
+      "subtype" : "44mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "appLauncher",
+      "scale" : "2x",
+      "size" : "51x51",
+      "subtype" : "45mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "quickLook",
+      "scale" : "2x",
+      "size" : "86x86",
+      "subtype" : "38mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "quickLook",
+      "scale" : "2x",
+      "size" : "98x98",
+      "subtype" : "42mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "quickLook",
+      "scale" : "2x",
+      "size" : "108x108",
+      "subtype" : "44mm"
+    },
+    {
+      "idiom" : "watch",
+      "role" : "quickLook",
+      "scale" : "2x",
+      "size" : "117x117",
+      "subtype" : "45mm"
+    },
+    {
+      "idiom" : "watch-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
FreeAPSWatch/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 10 - 0
FreeAPSWatch/FreeAPSWatch.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>com.apple.security.application-groups</key>
+	<array>
+		<string>$(APP_GROUP_ID)</string>
+	</array>
+</dict>
+</plist>