Browse Source

Merge remote-tracking branch 'origin/Crowdin' into Crowdin

Jon B.M 4 years ago
parent
commit
64ee4bdf8e
64 changed files with 2383 additions and 1 deletions
  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/ar.lproj/Localizable.strings
  6. 12 0
      FreeAPS/Sources/Localizations/Main/ca.lproj/Localizable.strings
  7. 12 0
      FreeAPS/Sources/Localizations/Main/da.lproj/Localizable.strings
  8. 12 0
      FreeAPS/Sources/Localizations/Main/de.lproj/Localizable.strings
  9. 12 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  10. 12 0
      FreeAPS/Sources/Localizations/Main/es.lproj/Localizable.strings
  11. 12 0
      FreeAPS/Sources/Localizations/Main/fi.lproj/Localizable.strings
  12. 12 0
      FreeAPS/Sources/Localizations/Main/fr.lproj/Localizable.strings
  13. 12 0
      FreeAPS/Sources/Localizations/Main/he.lproj/Localizable.strings
  14. 12 0
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  15. 12 0
      FreeAPS/Sources/Localizations/Main/nb.lproj/Localizable.strings
  16. 12 0
      FreeAPS/Sources/Localizations/Main/nl.lproj/Localizable.strings
  17. 12 0
      FreeAPS/Sources/Localizations/Main/pl.lproj/Localizable.strings
  18. 12 0
      FreeAPS/Sources/Localizations/Main/pt-BR.lproj/Localizable.strings
  19. 12 0
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  20. 12 0
      FreeAPS/Sources/Localizations/Main/sk.lproj/Localizable.strings
  21. 12 0
      FreeAPS/Sources/Localizations/Main/sv.lproj/Localizable.strings
  22. 12 0
      FreeAPS/Sources/Localizations/Main/tr.lproj/Localizable.strings
  23. 12 0
      FreeAPS/Sources/Localizations/Main/uk.lproj/Localizable.strings
  24. 12 0
      FreeAPS/Sources/Localizations/Main/zh-Hans.lproj/Localizable.strings
  25. 7 0
      FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift
  26. 338 0
      FreeAPS/Sources/Services/WatchManager/WatchManager.swift
  27. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/Contents.json
  28. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Circular.imageset/FreeAPS-X.svg
  29. 53 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Contents.json
  30. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/Contents.json
  31. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Extra Large.imageset/FreeAPS-X-2.svg
  32. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/Contents.json
  33. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Bezel.imageset/FreeAPS-X-3.svg
  34. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/Contents.json
  35. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Circular.imageset/FreeAPS-X-3.svg
  36. 21 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/Contents.json
  37. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Corner.imageset/FreeAPS-X-4.svg
  38. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/Contents.json
  39. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Extra Large.imageset/FreeAPS-X-5.svg
  40. 20 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Graphic Large Rectangular.imageset/Contents.json
  41. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/Contents.json
  42. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Modular.imageset/FreeAPS-X-6.svg
  43. 26 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/Contents.json
  44. 12 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Complication.complicationset/Utilitarian.imageset/FreeAPS-X-7.svg
  45. 6 0
      FreeAPSWatch WatchKit Extension/Assets.xcassets/Contents.json
  46. 59 0
      FreeAPSWatch WatchKit Extension/ComplicationController.swift
  47. 26 0
      FreeAPSWatch WatchKit Extension/DataFlow.swift
  48. 15 0
      FreeAPSWatch WatchKit Extension/FreeAPSApp.swift
  49. 10 0
      FreeAPSWatch WatchKit Extension/FreeAPSWatch WatchKit Extension.entitlements
  50. 16 0
      FreeAPSWatch WatchKit Extension/Info.plist
  51. 25 0
      FreeAPSWatch WatchKit Extension/NotificationController.swift
  52. 13 0
      FreeAPSWatch WatchKit Extension/NotificationView.swift
  53. 6 0
      FreeAPSWatch WatchKit Extension/Preview Content/Preview Assets.xcassets/Contents.json
  54. 20 0
      FreeAPSWatch WatchKit Extension/PushNotificationPayload.apns
  55. 86 0
      FreeAPSWatch WatchKit Extension/Views/BolusView.swift
  56. 72 0
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift
  57. 95 0
      FreeAPSWatch WatchKit Extension/Views/ConfirmationView.swift
  58. 209 0
      FreeAPSWatch WatchKit Extension/Views/MainView.swift
  59. 54 0
      FreeAPSWatch WatchKit Extension/Views/TempTargetsView.swift
  60. 162 0
      FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift
  61. 11 0
      FreeAPSWatch/Assets.xcassets/AccentColor.colorset/Contents.json
  62. 109 0
      FreeAPSWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
  63. 6 0
      FreeAPSWatch/Assets.xcassets/Contents.json
  64. 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/ar.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 differentiation */
+"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/ca.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 differentiation */
+"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/da.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 differentiation */
+"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/de.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Kohlenhydrate hinzufügen";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Kohlenhydratmenge";
 
@@ -399,6 +402,15 @@ Einheitenzeichen g, für Gramm";
 /* Other CGM setting */
 "Other" = "Sonstiges";
 
+/* 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/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 differentiation */
+"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/es.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Añadir Carbohidratos";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Cantidad de Carbohidratos";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Otros";
 
+/* 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/fi.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 differentiation */
+"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/fr.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Ajouter des glucides";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Quantité de Glucides";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Autre";
 
+/* 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/he.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 differentiation */
+"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/it.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Aggiungi carboidrati";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Quantità di carboidrati";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Altro";
 
+/* 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/nb.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Legg til karbo";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Legg til karbo ";
+
 /*  */
 "Amount Carbs" = "Mengde karbo";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Annet";
 
+/* Whatch app alert */
+"Set temp targets presets on iPhone first" = "Angi midlertidig mål forhåndsinnstillinger på iPhone først";
+
+/* Updating Watch app */
+"Updating..." = "Oppdaterer...";
+
+/* Header for Temp targets in Watch app */
+"Temp Targets" = "Midlertidige mål";
+
 
 /* Calendar and Libre transmitter settings --------------*/
 /* */

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

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Koolhydraten toevoegen";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Hoeveelheid koolhydraten";
 
@@ -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/pl.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Dodaj węglowodany";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Amount Carbs";
 
@@ -400,6 +403,15 @@ Połączono z Nightscout!";
 /* 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/pt-BR.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Adicionar carboidratos";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Quantidade de Carboidratos";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Outro";
 
+/* 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/ru.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Ввод углеводов";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Добавить";
+
 /*  */
 "Amount Carbs" = "Кол-во углеводов";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Прочее";
 
+/* Whatch app alert */
+"Set temp targets presets on iPhone first" = "Сначала установите временные цели в приложении на iPhone";
+
+/* Updating Watch app */
+"Updating..." = "Обновление...";
+
+/* Header for Temp targets in Watch app */
+"Temp Targets" = "Временные цели";
+
 
 /* Calendar and Libre transmitter settings --------------*/
 /* */

+ 12 - 0
FreeAPS/Sources/Localizations/Main/sk.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 differentiation */
+"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 differentiation */
+"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 --------------*/
 /* */

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

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Karbonhidrat Ekle";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Miktar Karbonhidrat";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "Other" = "Diğeri";
 
+/* 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/uk.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "Додати Вуглеводи";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add Carbs ";
+
 /*  */
 "Amount Carbs" = "Кількість Вуглеводи";
 
@@ -398,6 +401,15 @@ Enact a temp Basal or a temp target */
 /* Other CGM setting */
 "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/zh-Hans.lproj/Localizable.strings

@@ -67,6 +67,9 @@
 /* Add carbs screen */
 "Add Carbs" = "添加碳水化合物";
 
+/* Add carbs header and button in Watch app. You can skip the last " " space. It's just for differentiation */
+"Add Carbs " = "Add 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 --------------*/
 /* */

+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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"
+  }
+}

File diff suppressed because it is too large
+ 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>