Ver código fonte

Merge pull request #140 from nightscout/swipe-to-delete

Rework History Sheet
Mike Plante 2 anos atrás
pai
commit
74fd65f6bd

+ 2 - 0
FreeAPS/Sources/Models/NightscoutTreatment.swift

@@ -3,6 +3,8 @@ import Foundation
 func determineBolusEventType(for event: PumpHistoryEvent) -> EventType {
     if event.isExternalInsulin ?? false {
         return .nsExternalInsulin
+    } else if event.isSMB ?? false {
+        return .smb
     }
     return event.type
 }

+ 1 - 0
FreeAPS/Sources/Models/PumpHistoryEvent.swift

@@ -46,6 +46,7 @@ struct PumpHistoryEvent: JSON, Equatable {
 
 enum EventType: String, JSON {
     case bolus = "Bolus"
+    case smb = "SMB"
     case mealBolus = "Meal Bolus"
     case correctionBolus = "Correction Bolus"
     case snackBolus = "Snack Bolus"

+ 0 - 2
FreeAPS/Sources/Modules/DataTable/DataTableDataFlow.swift

@@ -141,8 +141,6 @@ enum DataTable {
 
                 if isExternal ?? false {
                     bolusText += " " + NSLocalizedString("External", comment: "External Insulin")
-                } else if isSMB ?? false {
-                    bolusText += " " + NSLocalizedString("SMB", comment: "SMB")
                 }
 
                 return numberFormatter

+ 6 - 6
FreeAPS/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -13,7 +13,7 @@ extension DataTable {
         @Published var mode: Mode = .treatments
         @Published var treatments: [Treatment] = []
         @Published var glucose: [Glucose] = []
-        @Published var manualGlcuose: Decimal = 0
+        @Published var manualGlucose: Decimal = 0
         @Published var maxBolus: Decimal = 0
         @Published var externalInsulinAmount: Decimal = 0
         @Published var externalInsulinDate = Date()
@@ -156,8 +156,8 @@ extension DataTable {
                 .store(in: &lifetime)
         }
 
-        func deleteGlucose(at index: Int) {
-            let id = glucose[index].id
+        func deleteGlucose(_ glucose: Glucose) {
+            let id = glucose.id
             provider.deleteGlucose(id: id)
 
             let fetchRequest: NSFetchRequest<NSFetchRequestResult>
@@ -181,8 +181,8 @@ extension DataTable {
             // try? coredataContext.save()
         }
 
-        func addManualGlucose() {
-            let glucose = units == .mmolL ? manualGlcuose.asMgdL : manualGlcuose
+        func logManualGlucose() {
+            let glucose = units == .mmolL ? manualGlucose.asMgdL : manualGlucose
             let now = Date()
             let id = UUID().uuidString
 
@@ -201,7 +201,7 @@ extension DataTable {
             debug(.default, "Manual Glucose saved to glucose.json")
         }
 
-        func addExternalInsulin() {
+        func logExternalInsulin() {
             guard externalInsulinAmount > 0 else {
                 showModal(for: nil)
                 return

+ 190 - 144
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -7,11 +7,12 @@ extension DataTable {
         let resolver: Resolver
         @StateObject var state = StateModel()
 
-        @State private var isRemoveCarbsAlertPresented = false
-        @State private var removeCarbsAlert: Alert?
-        @State private var isRemoveInsulinAlertPresented = false
-        @State private var removeInsulinAlert: Alert?
-        @State private var newGlucose = false
+        @State private var isRemoveHistoryItemAlertPresented: Bool = false
+        @State private var alertTitle: String = ""
+        @State private var alertMessage: String = ""
+        @State private var alertTreatmentToDelete: Treatment?
+        @State private var alertGlucoseToDelete: Glucose?
+        @State private var showManualGlucose = false
         @State private var showExternalInsulin = false
         @State private var isAmountUnconfirmed = true
 
@@ -64,7 +65,7 @@ extension DataTable {
             .navigationBarTitleDisplayMode(.automatic)
             .navigationBarItems(
                 leading: Button("Close", action: state.hideModal),
-                trailing: state.mode == .glucose ? EditButton().asAny() : EmptyView().asAny()
+                trailing: state.mode == .glucose ? logGlucoseButton.asAny() : logInsulinButton.asAny()
             )
             .sheet(isPresented: $showExternalInsulin, onDismiss: {
                 if isAmountUnconfirmed {
@@ -72,57 +73,40 @@ extension DataTable {
                     state.externalInsulinDate = Date()
                 }
             }) {
-                addExternalInsulinView
+                logExternalInsulinView
             }
-            .popup(isPresented: newGlucose, alignment: .top, direction: .bottom) {
-                VStack(spacing: 20) {
-                    HStack {
-                        Text("New Glucose")
-                        DecimalTextField(" ... ", value: $state.manualGlcuose, formatter: glucoseFormatter)
-                        Text(state.units.rawValue)
-                    }.padding(.horizontal, 20)
-                    HStack {
-                        let limitLow: Decimal = state.units == .mmolL ? 2.2 : 40
-                        let limitHigh: Decimal = state.units == .mmolL ? 21 : 380
-                        Button { newGlucose = false }
-                        label: { Text("Cancel") }.frame(maxWidth: .infinity, alignment: .leading)
+            .sheet(isPresented: $showManualGlucose) {
+                logGlucoseView
+            }
+        }
 
-                        Button {
-                            state.addManualGlucose()
-                            newGlucose = false
-                        }
-                        label: { Text("Save") }
-                            .frame(maxWidth: .infinity, alignment: .trailing)
-                        // .disabled(state.manualGlcuose < limitLow || state.manualGlcuose > limitHigh)
+        private var logInsulinButton: some View {
+            Button(action: { showExternalInsulin = true
+                state.externalInsulinDate = Date() }, label: {
+                Text("Log Insulin")
+                    .foregroundColor(Color.accentColor)
+                Image(systemName: "plus")
+                    .foregroundColor(Color.accentColor)
+            }).buttonStyle(.borderless)
+        }
 
-                    }.padding(20)
+        private var logGlucoseButton: some View {
+            Button(
+                action: {
+                    showManualGlucose = true
+                    state.manualGlucose = 0
+                },
+                label: {
+                    Text("Log Glucose")
+                        .foregroundColor(Color.accentColor)
+                    Image(systemName: "plus")
+                        .foregroundColor(Color.accentColor)
                 }
-                .frame(maxHeight: 140)
-                .background(
-                    RoundedRectangle(cornerRadius: 8, style: .continuous)
-                        .fill(Color(colorScheme == .dark ? UIColor.systemGray2 : UIColor.systemGray6))
-                )
-            }
+            ).buttonStyle(.borderless)
         }
 
         private var treatmentsList: some View {
             List {
-                HStack {
-                    Spacer()
-                    Button(action: { showExternalInsulin = true
-                        state.externalInsulinDate = Date() }, label: {
-                        HStack {
-                            Text("Add")
-                                .foregroundColor(Color.secondary)
-                                .font(.caption)
-
-                            Image(systemName: "syringe")
-                                .foregroundColor(Color.accentColor)
-                        }.frame(maxWidth: .infinity, alignment: .trailing)
-
-                    }).buttonStyle(.borderless)
-                }
-
                 if !state.treatments.isEmpty {
                     ForEach(state.treatments) { item in
                         treatmentView(item)
@@ -137,100 +121,131 @@ extension DataTable {
 
         private var glucoseList: some View {
             List {
-                Button { newGlucose = true }
-                label: { Text("Add") }.frame(maxWidth: .infinity, alignment: .trailing)
-                    .padding(.trailing, 20)
+                if !state.glucose.isEmpty {
+                    ForEach(state.glucose) { item in
+                        glucoseView(item)
+                    }
+                } else {
+                    HStack {
+                        Text(NSLocalizedString("No data.", comment: "No data text when no entries in history list"))
+                    }
+                }
+            }
+        }
 
-                ForEach(state.glucose) { item in
-                    glucoseView(item)
-                }.onDelete(perform: deleteGlucose)
+        private var logGlucoseView: some View {
+            NavigationView {
+                VStack {
+                    Form {
+                        Section {
+                            HStack {
+                                Text("New Glucose")
+                                DecimalTextField(
+                                    " ... ",
+                                    value: $state.manualGlucose,
+                                    formatter: glucoseFormatter,
+                                    autofocus: true,
+                                    cleanInput: true
+                                )
+                                Text(state.units.rawValue).foregroundStyle(.secondary)
+                            }
+                        }
+
+                        Section {
+                            HStack {
+                                let limitLow: Decimal = state.units == .mmolL ? 0.8 : 40
+                                let limitHigh: Decimal = state.units == .mmolL ? 14 : 720
+
+                                Button {
+                                    state.logManualGlucose()
+                                    isAmountUnconfirmed = false
+                                    showManualGlucose = false
+                                }
+                                label: { Text("Save") }
+                                    .frame(maxWidth: .infinity, alignment: .center)
+                                    .disabled(state.manualGlucose < limitLow || state.manualGlucose > limitHigh)
+                            }
+                        }
+                    }
+                }
+                .onAppear(perform: configureView)
+                .navigationTitle("Log Glucose")
+                .navigationBarTitleDisplayMode(.automatic)
+                .navigationBarItems(leading: Button("Close", action: { showManualGlucose = false }))
             }
         }
 
         @ViewBuilder private func treatmentView(_ item: Treatment) -> some View {
             HStack {
-                Image(systemName: "circle.fill").foregroundColor(item.color)
-                Text(dateFormatter.string(from: item.date))
-                    .moveDisabled(true)
-                Text(item.type.name)
+                if item.type == .bolus || item.type == .carbs {
+                    Image(systemName: "circle.fill").foregroundColor(item.color).padding(.vertical)
+                } else {
+                    Image(systemName: "circle.fill").foregroundColor(item.color)
+                }
+                Text((item.isSMB ?? false) ? "SMB" : item.type.name)
                 Text(item.amountText).foregroundColor(.secondary)
                 if let duration = item.durationText {
                     Text(duration).foregroundColor(.secondary)
                 }
+                Spacer()
+                Text(dateFormatter.string(from: item.date))
+                    .moveDisabled(true)
+            }
+            .swipeActions {
+                // Only allow swipe to delete if a carb, fpu, or bolus entry.
+                if item.type == .carbs || item.type == .fpus || item.type == .bolus {
+                    Button(
+                        "Delete",
+                        systemImage: "trash.fill",
+                        role: .none,
+                        action: {
+                            alertTreatmentToDelete = item
 
-                if item.type == .carbs {
-                    if item.note != "" {
-                        Spacer()
-                        Text(item.note ?? "").foregroundColor(.brown)
-                    }
-                    Spacer()
-                    Image(systemName: "xmark.circle").foregroundColor(.secondary)
-                        .contentShape(Rectangle())
-                        .padding(.vertical)
-                        .onTapGesture {
-                            removeCarbsAlert = Alert(
-                                title: Text("Delete carbs?"),
-                                message: Text(item.amountText),
-                                primaryButton: .destructive(
-                                    Text("Delete"),
-                                    action: { state.deleteCarbs(item) }
-                                ),
-                                secondaryButton: .cancel()
-                            )
-                            isRemoveCarbsAlertPresented = true
-                        }
-                        .alert(isPresented: $isRemoveCarbsAlertPresented) {
-                            removeCarbsAlert!
-                        }
-                }
+                            if item.type == .carbs {
+                                alertTitle = "Delete Carbs?"
+                                alertMessage = dateFormatter.string(from: item.date) + ", " + item.amountText
+                            } else if item.type == .fpus {
+                                alertTitle = "Delete Carb Equivalents?"
+                                alertMessage = "All FPUs of the meal will be deleted."
+                            } else {
+                                // item is insulin treatment; item.type == .bolus
+                                alertTitle = "Delete Insulin?"
+                                alertMessage = dateFormatter.string(from: item.date) + ", " + item.amountText
 
-                if item.type == .fpus {
-                    Spacer()
-                    Image(systemName: "xmark.circle").foregroundColor(.secondary)
-                        .contentShape(Rectangle())
-                        .padding(.vertical)
-                        .onTapGesture {
-                            removeCarbsAlert = Alert(
-                                title: Text("Delete carb equivalents?"),
-                                message: Text(""), // Temporary fix. New to fix real amount of carb equivalents later
-                                primaryButton: .destructive(
-                                    Text("Delete"),
-                                    action: { state.deleteCarbs(item) }
-                                ),
-                                secondaryButton: .cancel()
-                            )
-                            isRemoveCarbsAlertPresented = true
-                        }
-                        .alert(isPresented: $isRemoveCarbsAlertPresented) {
-                            removeCarbsAlert!
+                                if item.isSMB ?? false {
+                                    // Add text snippet, so that alert message is more descriptive for SMBs
+                                    alertMessage += " SMB"
+                                }
+                            }
+
+                            isRemoveHistoryItemAlertPresented = true
                         }
+                    ).tint(.red)
                 }
+            }
+            .alert(
+                Text(NSLocalizedString(alertTitle, comment: "")),
+                isPresented: $isRemoveHistoryItemAlertPresented
+            ) {
+                Button("Cancel", role: .cancel) {}
+                Button("Delete", role: .destructive) {
+                    guard let treatmentToDelete = alertTreatmentToDelete else {
+                        debug(.default, "Cannot gracefully unwrap alertTreatmentToDelete!")
+                        return
+                    }
 
-                if item.type == .bolus {
-                    Spacer()
-                    Image(systemName: "xmark.circle").foregroundColor(.secondary)
-                        .contentShape(Rectangle())
-                        .padding(.vertical)
-                        .onTapGesture {
-                            removeInsulinAlert = Alert(
-                                title: Text("Delete insulin?"),
-                                message: Text(item.amountText),
-                                primaryButton: .destructive(
-                                    Text("Delete"),
-                                    action: { state.deleteInsulin(item) }
-                                ),
-                                secondaryButton: .cancel()
-                            )
-                            isRemoveInsulinAlertPresented = true
-                        }
-                        .alert(isPresented: $isRemoveInsulinAlertPresented) {
-                            removeInsulinAlert!
-                        }
+                    if treatmentToDelete.type == .carbs || treatmentToDelete.type == .fpus {
+                        state.deleteCarbs(treatmentToDelete)
+                    } else {
+                        state.deleteInsulin(treatmentToDelete)
+                    }
                 }
+            } message: {
+                Text("\n" + NSLocalizedString(alertMessage, comment: ""))
             }
         }
 
-        var addExternalInsulinView: some View {
+        var logExternalInsulinView: some View {
             NavigationView {
                 VStack {
                     Form {
@@ -259,12 +274,12 @@ extension DataTable {
                         Section {
                             HStack {
                                 Button {
-                                    state.addExternalInsulin()
+                                    state.logExternalInsulin()
                                     isAmountUnconfirmed = false
                                     showExternalInsulin = false
                                 }
                                 label: {
-                                    Text("Log external insulin")
+                                    Text("Save")
                                 }
                                 .foregroundColor(amountWarningCondition ? Color.white : Color.accentColor)
                                 .frame(maxWidth: .infinity, alignment: .center)
@@ -287,32 +302,63 @@ extension DataTable {
                     }
                 }
                 .onAppear(perform: configureView)
-                .navigationTitle("External Insulin")
-                .navigationBarTitleDisplayMode(.inline)
+                .navigationTitle("Log External Insulin")
+                .navigationBarTitleDisplayMode(.automatic)
                 .navigationBarItems(leading: Button("Close", action: { showExternalInsulin = false
                     state.externalInsulinAmount = 0 }))
             }
         }
 
         @ViewBuilder private func glucoseView(_ item: Glucose) -> some View {
-            VStack(alignment: .leading, spacing: 4) {
-                HStack {
-                    Text(dateFormatter.string(from: item.glucose.dateString))
-                    Spacer()
-                    Text(item.glucose.glucose.map {
-                        glucoseFormatter.string(from: Double(
-                            state.units == .mmolL ? $0.asMmolL : Decimal($0)
-                        ) as NSNumber)!
-                    } ?? "--")
-                    Text(state.units.rawValue)
-                    Text(item.glucose.direction?.symbol ?? "--")
-                }
-                Text("ID: " + item.glucose.id).font(.caption2).foregroundColor(.secondary)
+            HStack {
+                Text(item.glucose.glucose.map {
+                    glucoseFormatter.string(from: Double(
+                        state.units == .mmolL ? $0.asMmolL : Decimal($0)
+                    ) as NSNumber)!
+                } ?? "--")
+                Text(item.glucose.direction?.symbol ?? "--")
+                Spacer()
+
+                Text(dateFormatter.string(from: item.glucose.dateString))
             }
-        }
+            .swipeActions {
+                Button(
+                    "Delete",
+                    systemImage: "trash.fill",
+                    role: .none,
+                    action: {
+                        alertGlucoseToDelete = item
+
+                        let valueText = glucoseFormatter.string(from: Double(
+                            state.units == .mmolL ? Double(item.glucose.value.asMmolL) : item.glucose.value
+                        ) as NSNumber)! + " " + state.units.rawValue
+
+                        alertTitle = "Delete Glucose?"
+                        alertMessage = dateFormatter.string(from: item.glucose.dateString) + ", " + valueText
 
-        private func deleteGlucose(at offsets: IndexSet) {
-            state.deleteGlucose(at: offsets[offsets.startIndex])
+                        isRemoveHistoryItemAlertPresented = true
+                    }
+                ).tint(.red)
+            }
+            .alert(
+                Text(NSLocalizedString(alertTitle, comment: "")),
+                isPresented: $isRemoveHistoryItemAlertPresented
+            ) {
+                Button("Cancel", role: .cancel) {}
+                Button("Delete", role: .destructive) {
+                    // gracefully unwrap value here.
+                    // value cannot ever really be nil because it is an existing(!) table entry
+                    // but just to be sure.
+                    guard let glucoseToDelete = alertGlucoseToDelete else {
+                        print("Cannot gracefully unwrap alertTreatmentToDelete!")
+                        return
+                    }
+
+                    state.deleteGlucose(glucoseToDelete)
+                }
+            } message: {
+                Text("\n" + NSLocalizedString(alertMessage, comment: ""))
+            }
         }
     }
 }