Ivan Valkou 4 лет назад
Родитель
Сommit
2690722b07

+ 6 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -200,6 +200,8 @@
 		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 */; };
 		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 */; };
@@ -583,6 +585,7 @@
 		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>"; };
 		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>"; };
@@ -1369,6 +1372,7 @@
 				38E8752B27554D5700975559 /* MainView.swift */,
 				38E8754B2755548F00975559 /* WatchStateModel.swift */,
 				38E87549275550BB00975559 /* CarbsView.swift */,
+				38E8755A27568A6700975559 /* ConfirmationView.swift */,
 			);
 			path = Views;
 			sourceTree = "<group>";
@@ -2209,6 +2213,8 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				38E8757927579D9200975559 /* Publisher.swift in Sources */,
+				38E8755B27568A6800975559 /* ConfirmationView.swift in Sources */,
 				38E8752E27554D5700975559 /* NotificationController.swift in Sources */,
 				38E8754C2755548F00975559 /* WatchStateModel.swift in Sources */,
 				38E8754A275550BB00975559 /* CarbsView.swift in Sources */,

+ 17 - 8
FreeAPS/Sources/Services/WatchManager/WatchManager.swift

@@ -14,6 +14,7 @@ final class BaseWatchManager: NSObject, WatchManager, Injectable {
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var apsManager: APSManager!
     @Injected() private var storage: FileStorage!
+    @Injected() private var carbsStorage: CarbsStorage!
 
     init(resolver: Resolver, session: WCSession = .default) {
         self.session = session
@@ -144,20 +145,28 @@ extension BaseWatchManager: WCSessionDelegate {
     }
 
     func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        guard let commandRaw = message[WatchCommandKey.command.rawValue] as? String,
-              let command = WatchCommand(rawValue: commandRaw) else { return }
-        switch command {
-        case .stateRequest:
+        debug(.service, "WCSession got message: \(message)")
+
+        if let stateRequest = message["stateRequest"] as? Bool, stateRequest {
             processQueue.async {
                 self.sendState()
             }
-        case .carbs: break
         }
     }
 
-    func session(_: WCSession, didReceiveMessage _: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
-        print("WCSession got message with reply handler")
-        replyHandler(["message": "ok"])
+    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)
+            ])
+
+            replyHandler(["confirmation": true])
+            return
+        }
+
+        replyHandler(["confirmation": false])
     }
 
     func session(_: WCSession, didReceiveMessageData _: Data) {}

+ 0 - 14
FreeAPSWatch WatchKit Extension/DataFlow.swift

@@ -1,14 +1,5 @@
 import Foundation
 
-enum WatchCommandKey: String {
-    case command
-}
-
-enum WatchCommand: String {
-    case stateRequest
-    case carbs
-}
-
 struct WatchState: Codable {
     var glucose: String?
     var trend: String?
@@ -23,8 +14,3 @@ struct WatchState: Codable {
     var iob: Decimal?
     var cob: Decimal?
 }
-
-struct WatchCommandConfitmation: Codable {
-    let confirmed: Bool
-    let reason: String?
-}

+ 1 - 1
FreeAPSWatch WatchKit Extension/FreeAPSApp.swift

@@ -10,6 +10,6 @@ import SwiftUI
             }.environmentObject(state)
         }
 
-        WKNotificationScene(controller: NotificationController.self, category: "myCategory")
+        WKNotificationScene(controller: NotificationController.self, category: "FreeAPSCategory")
     }
 }

+ 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()
+    }
+}
+
+private 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)
+    }
+}

+ 14 - 7
FreeAPSWatch WatchKit Extension/Views/MainView.swift

@@ -12,10 +12,17 @@ struct MainView: View {
     @State var isTargetsActive = false
     @State var isBolusActive = false
     var body: some View {
-        VStack {
-            header
-            Spacer()
-            buttons
+        ZStack {
+            VStack {
+                header
+                Spacer()
+                buttons
+            }
+
+            if state.isConfirmationViewActive {
+                ConfirmationView(success: $state.confirmationSuccess)
+                    .background(Rectangle().fill(.black))
+            }
         }
         .frame(maxHeight: .infinity)
         .padding()
@@ -57,7 +64,7 @@ struct MainView: View {
 
     var buttons: some View {
         HStack {
-            NavigationLink {
+            NavigationLink(isActive: $state.isCarbsViewActive) {
                 CarbsView()
                     .environmentObject(state)
             } label: {
@@ -70,7 +77,7 @@ struct MainView: View {
                 }
             }
 
-            NavigationLink {
+            NavigationLink(isActive: $state.isTempTargetViewActive) {
                 EmptyView()
             } label: {
                 HStack {
@@ -82,7 +89,7 @@ struct MainView: View {
                 }
             }
 
-            NavigationLink {
+            NavigationLink(isActive: $state.isBolusViewActive) {
                 EmptyView()
             } label: {
                 HStack {

+ 42 - 12
FreeAPSWatch WatchKit Extension/Views/WatchStateModel.swift

@@ -1,10 +1,10 @@
+import Combine
 import Foundation
 import SwiftUI
 import WatchConnectivity
 
 class WatchStateModel: NSObject, ObservableObject {
     var session: WCSession
-    @Published var result = ""
 
     @Published var glucose = "00"
     @Published var trend = "→"
@@ -19,6 +19,14 @@ class WatchStateModel: NSObject, ObservableObject {
     @Published var iob: Decimal?
     @Published var cob: Decimal?
 
+    @Published var isCarbsViewActive = false
+    @Published var isTempTargetViewActive = false
+    @Published var isBolusViewActive = false
+    @Published var isConfirmationViewActive = false
+    @Published var confirmationSuccess: Bool?
+
+    private var lifetime = Set<AnyCancellable>()
+
     init(session: WCSession = .default) {
         self.session = session
         super.init()
@@ -28,10 +36,38 @@ class WatchStateModel: NSObject, ObservableObject {
     }
 
     func addCarbs(_ carbs: Int) {
-        session.sendMessage(["addCarbs": carbs], replyHandler: { _ in
-            WKInterfaceDevice.current().play(.success)
+        confirmationSuccess = nil
+        isConfirmationViewActive = true
+        isCarbsViewActive = false
+        session.sendMessage(["carbs": carbs], replyHandler: { replay in
+            if let ok = replay["confirmation"] as? Bool {
+                DispatchQueue.main.async {
+                    self.confirmation(ok)
+                }
+            } else {
+                DispatchQueue.main.async {
+                    self.confirmation(false)
+                }
+            }
+
         }) { error in
-            print("ASDF: " + error.localizedDescription)
+            print(error.localizedDescription)
+            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
+            }
         }
     }
 
@@ -53,18 +89,12 @@ class WatchStateModel: NSObject, ObservableObject {
 extension WatchStateModel: WCSessionDelegate {
     func session(_: WCSession, activationDidCompleteWith state: WCSessionActivationState, error _: Error?) {
         print("WCSession activated: \(state == .activated)")
-        session.sendMessage([WatchCommandKey.command.rawValue: WatchCommand.stateRequest.rawValue], replyHandler: nil) { error in
+        session.sendMessage(["stateRequest": true], replyHandler: nil) { error in
             print("WatchStateModel error: " + error.localizedDescription)
         }
     }
 
-    func session(_: WCSession, didReceiveMessage message: [String: Any]) {
-        if let text = message["message"] as? String {
-            DispatchQueue.main.async {
-                self.result = text
-            }
-        }
-    }
+    func session(_: WCSession, didReceiveMessage _: [String: Any]) {}
 
     func sessionReachabilityDidChange(_ session: WCSession) {
         print("WCSession Reachability: \(session.isReachable)")