Просмотр исходного кода

Merge remote-tracking branch 'ivalkou/dev' into Crowdin

Jon B.M 4 лет назад
Родитель
Сommit
c4264d7062
37 измененных файлов с 560 добавлено и 1103 удалено
  1. 0 226
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/GlucoseSchedules.swift
  2. 0 27
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Alarmsettings.swift
  3. 0 380
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/AlarmSettings/AlarmSettingsView.swift
  4. 0 270
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/AlarmSettings/CustomDataPickerView.swift
  5. 2 16
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SettingsView.swift
  6. 0 140
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SnoozeView.swift
  7. 0 21
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Observables/AlarmStatus.swift
  8. 58 0
      FreeAPS.xcodeproj/project.pbxproj
  9. 3 3
      FreeAPS.xcodeproj/xcuserdata/i.valkou.xcuserdatad/xcschemes/xcschememanagement.plist
  10. 6 0
      FreeAPS/Resources/FreeAPS.entitlements
  11. 4 0
      FreeAPS/Resources/Info.plist
  12. 1 1
      FreeAPS/Sources/APS/CGM/AppGroupSource.swift
  13. 2 0
      FreeAPS/Sources/APS/FetchGlucoseManager.swift
  14. 4 0
      FreeAPS/Sources/APS/OpenAPS/Constants.swift
  15. 36 0
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  16. 1 0
      FreeAPS/Sources/Application/FreeAPSApp.swift
  17. 3 0
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  18. 11 0
      FreeAPS/Sources/Helpers/Array+Extension.swift
  19. 3 1
      FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift
  20. 9 0
      FreeAPS/Sources/Localizations/Main/en.lproj/Localizable.strings
  21. 1 0
      FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings
  22. 9 0
      FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings
  23. 15 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  24. 19 0
      FreeAPS/Sources/Models/HealthKitSample.swift
  25. 5 0
      FreeAPS/Sources/Modules/HealthKit/HealthKitDataFlow.swift
  26. 3 0
      FreeAPS/Sources/Modules/HealthKit/HealthKitProvider.swift
  27. 44 0
      FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift
  28. 27 0
      FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift
  29. 2 0
      FreeAPS/Sources/Modules/Home/View/HomeRootView.swift
  30. 3 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  31. 1 3
      FreeAPS/Sources/Modules/Snooze/View/SnoozeRootView.swift
  32. 3 0
      FreeAPS/Sources/Router/Screen.swift
  33. 274 0
      FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift
  34. 2 2
      FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift
  35. 8 3
      FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift
  36. 1 0
      FreeAPSWatch WatchKit Extension/Views/BolusView.swift
  37. 0 10
      FreeAPSWatch WatchKit Extension/Views/CarbsView.swift

+ 0 - 226
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/GlucoseSchedules.swift

@@ -1,226 +0,0 @@
-//
-//  GlucoseSchedules.swift
-//  MiaomiaoClient
-//
-//  Created by Bjørn Inge Berg on 19/04/2019.
-//  Copyright © 2019 Bjørn Inge Berg. All rights reserved.
-//
-
-import Foundation
-import HealthKit
-//import MiaomiaoClient
-public enum GlucoseScheduleAlarmResult: Int, CaseIterable {
-    case none = 0
-    case low
-    case high
-
-    func isAlarming() -> Bool {
-        rawValue != GlucoseScheduleAlarmResult.none.rawValue
-    }
-}
-
-enum GlucoseSchedulesValidationStatus {
-    case success
-    case error(String)
-}
-
-class GlucoseScheduleList: Codable, CustomStringConvertible {
-    var description: String {
-        "(schedules: \(schedules) )"
-    }
-
-    public var schedules = [GlucoseSchedule]()
-
-    public var enabledSchedules: [GlucoseSchedule] {
-        schedules.compactMap({ $0.enabled == true ? $0 : nil })
-    }
-
-    //this is only used by the ui to count total number of schedules
-    public static let minimumSchedulesCount = 2
-
-    public var activeSchedules: [GlucoseSchedule] {
-        enabledSchedules.compactMap {
-            if let activeTime = $0.getScheduleActiveToFrom() {
-                let now = Date()
-                return activeTime.contains(now) ? $0 : nil
-            }
-            return nil
-        }
-    }
-
-    private func validateGlucoseThresholds() -> GlucoseSchedulesValidationStatus? {
-        // This is on purpose
-        // we check all chedules for valid thresholds
-        for schedule in self.schedules {
-            if let low = schedule.lowAlarm, let high = schedule.highAlarm {
-                if low == high {
-                    return .error("One of your glucose schedules had the same value for low and high thresholds")
-                }
-                if low > high {
-                    return .error("One of your glucose schedules had a low threshold set above your high threshold")
-                }
-                //just for completness sake, this would never be called
-                if high < low {
-                    return .error("One of your glucose schedules had a high threshold set below your low threshold")
-                }
-            }
-        }
-        return nil
-    }
-
-    public func validateGlucoseSchedules() -> GlucoseSchedulesValidationStatus {
-        if let errors = validateGlucoseThresholds() {
-            return errors
-        }
-
-        // if we have zero or 1 enabled schedules, overlapping would not be possible
-        // (there is nothing to overlap on), so we skip interval check
-        guard self.enabledSchedules.count > 1 else {
-            return .success
-        }
-
-        var sameStartEnd = false
-        let intervals: [DateInterval] = enabledSchedules.compactMap({
-            var schedule = $0.getScheduleActiveToFrom()
-            if let start = schedule?.start, let end = schedule?.end {
-                if start == end {
-                    sameStartEnd = true
-                    return nil
-                }
-            }
-            // This compensates for Datetimes being closed range in nature
-            // example,
-            // interval1start = 12:00, interval1end=14:00
-            // interval2start = 14:00, interval2end=24:00
-            // interval1end and interval2 would collide when .intersect()-ing,
-            // so we change interval1end to 13:59:59
-            // and interval2end to 23:59:59
-            // This function is only used in the gui for validation, so this is acceptable
-            //
-            if let end = schedule?.end {
-                if let newEnd = Calendar.current.date(byAdding: .second, value: -1, to: end) {
-                    schedule?.end = newEnd
-                }
-            }
-            return schedule
-        })
-        if sameStartEnd {
-            return .error("One interval had the same start and end!")
-        }
-        if let intersects = intervals.intersect() {
-            print("Glucose schedule collided, not valid! \(intersects)")
-            return .error("Glucose schedules had overlapping time intervals")
-        }
-        return .success
-    }
-    //for convenience
-    public static var snoozedUntil: Date? {
-        UserDefaults.standard.snoozedUntil
-    }
-
-    public static func isSnoozed() -> Bool {
-        let now = Date()
-
-        if let snoozedUntil = snoozedUntil {
-            return snoozedUntil >= now
-        }
-        return false
-    }
-
-    public func getActiveAlarms(_ currentGlucoseInMGDL: Double) -> GlucoseScheduleAlarmResult {
-        for schedule in self.activeSchedules {
-            if let lowAlarm = schedule.lowAlarm, currentGlucoseInMGDL <= lowAlarm {
-                return .low
-            }
-            if let highAlarm = schedule.highAlarm, currentGlucoseInMGDL >= highAlarm {
-                return .high
-            }
-        }
-        return .none
-    }
-}
-
-class GlucoseSchedule: Codable, CustomStringConvertible {
-    var from: DateComponents?
-    var to: DateComponents?
-    var lowAlarm: Double?
-    var highAlarm: Double?
-    var enabled: Bool?
-
-    init() {
-    }
-
-    //glucose schedules are stored as standalone datecomponents (i.e. offsets)
-    //this takes the current start of day and adds those offsets,
-    // and returns a Dateinterval with those offsets applied
-    public func getScheduleActiveToFrom() -> DateInterval? {
-        guard let fromComponents = from, let toComponents = to else {
-            return nil
-        }
-
-        let now = Date()
-        let previousMidnight = Calendar.current.startOfDay(for: now)
-        let helper = Calendar.current.date(byAdding: .day, value: 1, to: previousMidnight)!
-        let nextMidnight = Calendar.current.startOfDay(for: helper)
-
-        let fromDate: Date? =  Calendar.current.date(byAdding: fromComponents, to: previousMidnight)
-        var toDate: Date?
-        if  toComponents.minute == 0 && toComponents.hour == 0 {
-            toDate = nextMidnight
-        } else {
-            toDate = Calendar.current.date(byAdding: toComponents, to: previousMidnight)!
-        }
-
-        if let fromDate = fromDate, let toDate = toDate, toDate >= fromDate {
-            return DateInterval(start: fromDate, end: toDate)
-        }
-        return nil
-    }
-    //stores the alarm. It does not synhronize the value with the underlaying userdefaults
-    //that is up to the caller of this class
-    public func storeLowAlarm(forUnit unit: HKUnit, lowAlarm: Double) {
-        if unit == HKUnit.millimolesPerLiter {
-            self.lowAlarm = lowAlarm * 18
-            return
-        }
-
-        self.lowAlarm = lowAlarm
-    }
-
-    public func retrieveLowAlarm(forUnit unit: HKUnit) -> Double? {
-        if let lowAlarm = self.lowAlarm {
-            if unit == HKUnit.millimolesPerLiter {
-                return (lowAlarm / 18).roundTo(places: 1)
-            } else {
-                return lowAlarm
-            }
-        }
-
-        return nil
-    }
-
-    //stores the alarm. It does not synhronize the value with the underlaying userdefaults
-    //that is up to the caller of this class
-    public func storeHighAlarm(forUnit unit: HKUnit, highAlarm: Double) {
-        if unit == HKUnit.millimolesPerLiter {
-            self.highAlarm = highAlarm * 18
-            return
-        }
-
-        self.highAlarm = highAlarm
-    }
-    public func retrieveHighAlarm(forUnit unit: HKUnit) -> Double? {
-        if let highAlarm = self.highAlarm {
-            if unit == HKUnit.millimolesPerLiter {
-                return (highAlarm / 18).roundTo(places: 1)
-            }
-            return highAlarm
-        }
-
-        return nil
-    }
-
-    var description: String {
-        "(from: \(String(describing: from)), to: \(String(describing: to)), low: \(String(describing: lowAlarm)), high: \(String(describing: highAlarm)), enabled: \(String(describing: enabled)))"
-    }
-}

+ 0 - 27
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/Settings/UserDefaults+Alarmsettings.swift

@@ -11,7 +11,6 @@ import HealthKit
 
 extension UserDefaults {
     private enum Key: String {
-        case glucoseSchedules = "no.bjorninge.glucoseschedules"
         case mmAlertLowBatteryWarning = "no.bjorninge.mmLowBatteryWarning"
         case mmAlertInvalidSensorDetected = "no.bjorninge.mmInvalidSensorDetected"
         case mmAlertNewSensorDetected = "no.bjorninge.mmNewSensorDetected"
@@ -106,14 +105,6 @@ extension UserDefaults {
         }
     }
 
-    var enabledSchedules: [GlucoseSchedule]? {
-        glucoseSchedules?.schedules.compactMap({ schedule -> GlucoseSchedule? in
-            if schedule.enabled ?? false {
-                return schedule
-            }
-            return nil
-        })
-    }
     var snoozedUntil: Date? {
         get {
             object(forKey: Key.mmSnoozedUntil.rawValue) as? Date
@@ -122,22 +113,4 @@ extension UserDefaults {
             set(newValue, forKey: Key.mmSnoozedUntil.rawValue)
         }
     }
-    var glucoseSchedules: GlucoseScheduleList? {
-        get {
-            if let savedGlucoseSchedules = object(forKey: Key.glucoseSchedules.rawValue) as? Data {
-                let decoder = JSONDecoder()
-                if let loadedGlucoseSchedules = try? decoder.decode(GlucoseScheduleList.self, from: savedGlucoseSchedules) {
-                    return loadedGlucoseSchedules
-                }
-            }
-
-            return GlucoseScheduleList()
-        }
-        set {
-            let encoder = JSONEncoder()
-            if let val = newValue, let encoded = try? encoder.encode(val) {
-                set(encoded, forKey: Key.glucoseSchedules.rawValue)
-            }
-        }
-    }
 }

+ 0 - 380
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/AlarmSettings/AlarmSettingsView.swift

@@ -1,380 +0,0 @@
-//
-//  AlarmSettingsView.swift
-//  LibreTransmitterUI
-//
-//  Created by Bjørn Inge Berg on 11/05/2021.
-//  Copyright © 2021 Mark Wilson. All rights reserved.
-//
-
-import SwiftUI
-import HealthKit
-
-private class AlarmSettingsIcons {
-
-    public static var Shared = AlarmSettingsIcons()
-
-    public func getImage(resourceName: String) -> some View{
-        let bundle = Bundle(for: type(of: self))
-        if let uiimage = UIImage(named: resourceName, in: bundle, compatibleWith: nil) {
-            return AnyView(Image(uiImage: uiimage))
-        }
-        return AnyView(EmptyView())
-    }
-    lazy var icons8_schedule_50 : some View = getImage(resourceName: "icons8-schedule-50")
-    lazy var icons8_drop_down_arrow_50 : some View = getImage(resourceName: "icons8-drop-down-arrow-50")
-    lazy var icons8_slide_up_50: some View = getImage(resourceName: "icons8-slide-up-50")
-}
-
-
-class AlarmScheduleState : ObservableObject, Identifiable, Hashable{
-
-    var id = UUID()
-
-    private let glucoseRate = 18.018
-
-    @Published var lowmgdl : Double = 72
-    @Published var highmgdl : Double = 180  
-    @Published var enabled : Bool? = false
-
-
-    @Published var alarmDateComponents  : AlarmTimeCellExternalState = AlarmTimeCellExternalState()
-
-    public func setLowAlarm(forUnit unit: HKUnit, lowAlarm: Double) {
-
-        if unit == HKUnit.millimolesPerLiter {
-            self.lowmgdl = lowAlarm * glucoseRate
-            return
-        }
-
-        self.lowmgdl = lowAlarm
-    }
-
-    public func getLowAlarm(forUnit unit: HKUnit) -> Double {
-
-        if unit == HKUnit.millimolesPerLiter {
-            return (lowmgdl / glucoseRate).roundTo(places: 1)
-        }
-
-        return lowmgdl
-
-
-
-
-    }
-
-    public func setHighAlarm(forUnit unit: HKUnit, highAlarm: Double) {
-
-        if unit == HKUnit.millimolesPerLiter {
-            self.highmgdl = highAlarm * glucoseRate
-            return
-        }
-
-        self.highmgdl = highAlarm
-    }
-
-    public func getHighAlarm(forUnit unit: HKUnit) -> Double {
-
-        if unit == HKUnit.millimolesPerLiter {
-            return (highmgdl / glucoseRate).roundTo(places: 1)
-        }
-        return highmgdl
-
-    }
-
-
-}
-
-class AlarmSettingsState : ObservableObject     {
-    @Published var schedules : [AlarmScheduleState] = []
-
-
-    static private func setDateComponentState(_ state: AlarmScheduleState) {
-        if state.alarmDateComponents.startComponents == nil {
-            state.alarmDateComponents.startComponents = DateComponents(hour:0, minute: 0)
-        }
-
-        if state.alarmDateComponents.endComponents == nil {
-            state.alarmDateComponents.endComponents = DateComponents(hour:0, minute: 0)
-        }
-
-        let from = state.alarmDateComponents.startComponents!.ToTimeString()
-        let to = state.alarmDateComponents.endComponents!.ToTimeString()
-
-        state.alarmDateComponents.componentsAsText = "\(from) - \(to)"
-
-
-    }
-
-    //this is just to be able to use old serialized schedules from uikit version
-    //i.e. we want to be drop in compatible
-    static func loadState()-> AlarmSettingsState {
-
-        guard let storedState = UserDefaults.standard.glucoseSchedules, storedState.schedules.count > 0 else {
-            print("stored state for alarms was empty")
-            let newState = AlarmSettingsState()
-            for _ in (0..<GlucoseScheduleList.minimumSchedulesCount) {
-                let schedule = AlarmScheduleState()
-                setDateComponentState(schedule)
-                schedule.enabled = false
-                newState.schedules.append(schedule)
-            }
-
-            return newState
-        }
-
-        let alarmState = AlarmSettingsState()
-
-        print("stored state for alarms contains \(storedState.schedules.count) elements")
-
-        for i in (0..<storedState.schedules.count) {
-            let schedule = AlarmScheduleState()
-
-            schedule.enabled = storedState.schedules[i].enabled
-            schedule.lowmgdl = storedState.schedules[i].lowAlarm ?? -1
-            schedule.highmgdl = storedState.schedules[i].highAlarm ?? -1
-
-
-            schedule.alarmDateComponents.startComponents = storedState.schedules[i].from
-            schedule.alarmDateComponents.endComponents  = storedState.schedules[i].to
-
-            setDateComponentState(schedule)
-
-            alarmState.schedules.append(schedule)
-
-
-        }
-
-        return alarmState
-
-
-    }
-
-
-    func trySaveState() -> StatusMessage?{
-        let legacyState = GlucoseScheduleList()
-        for newStateSchedule in self.schedules {
-            let glucoseSchedule = GlucoseSchedule()
-            glucoseSchedule.enabled = newStateSchedule.enabled
-            //view is using wrapper binding to store these values so should be safe
-            glucoseSchedule.lowAlarm = newStateSchedule.lowmgdl
-            glucoseSchedule.highAlarm = newStateSchedule.highmgdl
-            glucoseSchedule.from = newStateSchedule.alarmDateComponents.startComponents
-            glucoseSchedule.to = newStateSchedule.alarmDateComponents.endComponents
-
-            legacyState.schedules.append(glucoseSchedule)
-
-        }
-        let result = legacyState.validateGlucoseSchedules()
-        switch result {
-        case .success:
-
-            print("glucose schedule valid: \(String(describing: legacyState))")
-            UserDefaults.standard.glucoseSchedules = legacyState
-        case .error(let description):
-            print("Could not save glucose schedules, validation failed: \(description)")
-            return StatusMessage(title: "Error", message: description)
-        }
-
-        return nil
-
-
-    }
-}
-
-
-
-struct AlarmDateRow: View {
-    var schedule: AlarmScheduleState
-    @State var tag: Int
-    @Binding var subviewSelection: Int?
-
-    var body: some View {
-
-        HStack(alignment: .center){
-            NavigationLink(destination: CustomDataPickerView().environmentObject(schedule.alarmDateComponents),
-                           tag: tag,
-                           selection: $subviewSelection) {
-                Group {
-                    AlarmSettingsIcons.Shared.icons8_schedule_50
-                        .frame(maxWidth: 50, alignment: .leading)
-                    TextField("Active from - to ", text:  Binding<String>(get: { "\(schedule.alarmDateComponents.componentsAsText)" },
-                                                      set: { schedule.alarmDateComponents.componentsAsText = $0 }))
-                        .textFieldStyle(RoundedBorderTextFieldStyle())
-                        .disableAutocorrection(true)
-                        .keyboardType(.decimalPad)
-                        .border(Color(UIColor.separator))
-                        .disabled(true)
-                        .frame(minWidth: 130, idealWidth: 130, maxWidth: .infinity, alignment: .center)
-                }.onTapGesture {
-                    print("cheduleActivationRow tapped")
-                    subviewSelection = tag
-                    self.hideKeyboard()
-
-                }
-
-            }
-
-
-            Toggle("", isOn: Binding<Bool>(get: { (schedule.enabled) ?? false },
-                                             set: { schedule.enabled = $0 }))
-                .frame(maxWidth: 50, alignment: .trailing)
-
-        }
-    }
-}
-
-struct AlarmLowRow: View {
-    var schedule: AlarmScheduleState
-    var glucoseUnit: HKUnit
-    var glucoseUnitDesc: String
-
-    var errorReporter: FormErrorState
-
-    var body: some View {
-        HStack(alignment: .center){
-
-            AlarmSettingsIcons.Shared.icons8_drop_down_arrow_50
-                .frame(maxWidth: 50, alignment: .leading)
-            Text("Low")
-                .frame(maxWidth: 100, alignment: .leading)
-            Spacer()
-
-            /*TextField("glucose", text:  Binding<String>(get: { schedule.getLowAlarm(forUnit: glucoseUnit) },
-                                                        set: { schedule.setLowAlarm(forUnit: glucoseUnit, lowAlarm: $0) }))
-                .textFieldStyle(RoundedBorderTextFieldStyle())
-                .disableAutocorrection(true)
-                .keyboardType(.decimalPad)
-                .border(Color(UIColor.separator))
-                .frame(maxWidth: 100, alignment: .trailing)*/
-
-            NumericTextField(description: "glucose", showDescription: false, numericValue: Binding<Double>(get: { schedule.getLowAlarm(forUnit: glucoseUnit) },
-                                                                                                           set: { schedule.setLowAlarm(forUnit: glucoseUnit, lowAlarm: $0) }), formErrorState: errorReporter)
-
-            Text("\(glucoseUnitDesc)")
-                .font(.footnote)
-                .frame(maxWidth: 100, alignment: .trailing)
-
-        }
-        .onTapGesture {
-            self.hideKeyboard()
-        }
-    }
-}
-
-struct AlarmHighRow: View {
-    var schedule: AlarmScheduleState
-    var glucoseUnit: HKUnit
-    var glucoseUnitDesc: String
-
-    var errorReporter: FormErrorState
-
-    var body: some View {
-        HStack(alignment: .center){
-
-            AlarmSettingsIcons.Shared.icons8_slide_up_50
-                .frame(maxWidth: 50, alignment: .leading)
-            Text("High")
-                .frame(maxWidth: 100, alignment: .leading)
-            Spacer()
-
-            NumericTextField(description: "glucose", showDescription: false, numericValue: Binding<Double>(get: { schedule.getHighAlarm(forUnit: glucoseUnit) },
-                                                                                                           set: { schedule.setHighAlarm(forUnit: glucoseUnit, highAlarm: $0) }), formErrorState: errorReporter)
-
-
-            Text("\(glucoseUnitDesc)")
-                .font(.footnote)
-                .frame(maxWidth: 100, alignment: .trailing)
-
-
-        }
-        .onTapGesture {
-            self.hideKeyboard()
-        }
-    }
-}
-
-struct AlarmSettingsView: View {
-
-
-
-    private(set) var glucoseUnit: HKUnit
-
-    var glucoseUnitDesc : String {
-        //"mmol/L"
-        glucoseUnit.localizedShortUnitString
-    }
-
-
-    @State private var presentableStatus: StatusMessage?
-
-    @StateObject var alarmState = AlarmSettingsState.loadState()
-
-
-    @State private var subviewSelection: Int? = nil
-
-    
-
-
-    var body: some View {
-
-        list
-        .ignoresSafeArea()
-        .alert(item: $presentableStatus) { status in
-            Alert(title: Text(status.title), message: Text(status.message) , dismissButton: .default(Text("Got it!")))
-        }
-        
-
-    }
-
-    @StateObject var errorReporter = FormErrorState()
-    
-
-    var list: some View {
-
-        List {
-            ForEach(Array(alarmState.schedules.enumerated()), id: \.1) { i, schedule in
-                Section(header: Text(NSLocalizedString("Schedule ", comment: "") + "\(i+1)")){
-                    AlarmDateRow(schedule: schedule, tag: i, subviewSelection: $subviewSelection)
-                    AlarmLowRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter)
-                    AlarmHighRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter)
-
-                }.onTapGesture {
-                    self.hideKeyboard()
-                }
-
-            }
-
-
-            Section {
-                Button("Save") {
-                    saveButtonAction()
-                }.buttonStyle(BlueButtonStyle())
-            }
-        }
-        .listStyle(InsetGroupedListStyle())
-
-    }
-
-    func saveButtonAction() {
-        print("tapped save schedules")
-        if errorReporter.hasAnyError {
-            presentableStatus = StatusMessage(title: "Error", message: "Some ui element was incorrectly specified")
-            return
-        }
-        if let error = alarmState.trySaveState() {
-            presentableStatus = error
-        } else {
-            presentableStatus = StatusMessage(title: "Success", message: "Schedules were saved successfully!")
-        }
-
-
-    }
-
-    
-}
-
-struct AlarmSettingsView_Previews: PreviewProvider {
-    static var previews: some View {
-        AlarmSettingsView(glucoseUnit: .millimolesPerLiter)
-    }
-}

+ 0 - 270
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/AlarmSettings/CustomDataPickerView.swift

@@ -1,270 +0,0 @@
-//
-//  CustomDataPickerView.swift
-//  LibreTransmitterUI
-//
-//  Created by Bjørn Inge Berg on 28/04/2021.
-//  Copyright © 2021 Mark Wilson. All rights reserved.
-//
-
-import SwiftUI
-
-protocol CustomDataPickerDelegate: AnyObject {
-    func pickerDidPickValidRange()
-
-}
-
-class AlarmTimeCellExternalState :ObservableObject, Identifiable, Hashable {
-
-    var id = UUID()
-
-    @Published var start : Int = 0
-    @Published var end : Int = 0
-
-    // These will be auto populøated
-    // when the start and end properties above change
-    @Published var startComponents : DateComponents? = nil
-    @Published var endComponents : DateComponents? = nil
-
-    @Published var componentsAsText : String = ""
-
-}
-
-//handle parts of alarmsettingsview's state (=externalstate)
-struct CustomDataPickerView: View {
-    private var startComponentTimes : [DateComponents]
-    private var endComponentTimes : [DateComponents]
-
-    private var startTimes = [String]()
-    private var endTimes = [String]()
-
-
-
-    @Environment(\.presentationMode) var presentationMode
-    @EnvironmentObject var externalState: AlarmTimeCellExternalState
-
-    @State var externalStateCopy: AlarmTimeCellExternalState = AlarmTimeCellExternalState()
-
-    public weak var delegate: CustomDataPickerDelegate?
-
-    private func popView() {
-        self.presentationMode.wrappedValue.dismiss()
-    }
-
-    static func defaultTimeArray() -> [DateComponents] {
-        var arr  = [DateComponents]()
-
-        for hr in 0...23 {
-            for min in 0 ..< 2 {
-                var components = DateComponents()
-                components.hour = hr
-                components.minute = min == 1 ? 30 : 0
-                arr.append(components)
-            }
-        }
-        var components = DateComponents()
-        components.hour = 0
-        components.minute = 0
-        arr.append(components)
-
-        return arr
-    }
-
-
-
-    private func callDelegate() {
-        delegate?.pickerDidPickValidRange()
-    }
-
-    private func verifyRange(){
-
-        // This can be simplified but decided not to do so
-        // because the intention becomes more clear
-
-        var isok : Bool
-
-        if externalState.start == 0 || externalState.end == 0 {
-            isok = true
-        } else {
-            if externalState.start > externalState.end {
-                isok = false
-            } else if externalState.end < externalState.start {
-                isok = false
-            } else {
-                isok = true
-            }
-        }
-
-        print("is ok? \(isok)")
-        if isok {
-            updateTextualState()
-            callDelegate()
-            popView()
-
-        } else {
-            presentableStatus = .init(title: "Interval error", message: "Selected time interval was incorrectly specified")
-        }
-
-
-    }
-
-    
-    var pickers: some View {
-        HStack {
-            Picker("", selection: $externalState.start.animation(), content: {
-                ForEach(startTimes.indices) { i in
-                    Text("\(startTimes[i])").tag(i)
-               }
-            })
-            //.border(Color.green)
-
-            .zIndex(10)
-            .frame(width: 100)
-            .clipped()
-            .labelsHidden()
-
-            Text("To ")
-
-            Picker("", selection: $externalState.end.animation(), content: {
-                ForEach(endTimes.indices) { i in
-                    Text("\(endTimes[i])").tag(i)
-               }
-            })
-            //.border(Color.red)
-            .zIndex(11)
-            .frame(width: 100)
-            .clipped()
-            .labelsHidden()
-
-        //}
-
-        .navigationBarBackButtonHidden(true)
-        .navigationBarItems(leading:
-            Button("Cancel"){
-                print("cancel button pressed, restoring state...")
-                restoreAlarmExternalState()
-                popView()
-
-            }.accentColor(.red), trailing:
-                Button("Save") {
-                    print("Save button pressed...")
-                    verifyRange()
-                }
-                .accentColor(.red)
-
-        )
-        }
-
-
-    }
-
-
-    @State private var presentableStatus: StatusMessage?
-
-    private func updateTextualState(_ shouldDelete: Bool = false){
-        if shouldDelete {
-            externalState.componentsAsText = ""
-            return
-        }
-        if let p1 = externalState.startComponents?.ToTimeString(), let p2 = externalState.endComponents?.ToTimeString() {
-            externalState.componentsAsText = "\(p1)-\(p2)"
-        }
-    }
-
-
-    var body: some View {
-
-        pickers
-        .pickerStyle(InlinePickerStyle())
-        .onChange(of: externalState.start, perform: { value in
-            print("selectedtart changed to \(value)")
-            externalState.startComponents = startComponentTimes[value]
-
-
-
-        })
-        .onChange(of: externalState.end, perform: { value in
-            print("selectedEnd changed to \(value)")
-            externalState.endComponents = endComponentTimes[value]
-
-
-
-        })
-        .onAppear {
-            //this could potentially fail with out of bounds but we trust our parent view!
-            externalState.startComponents = startComponentTimes[externalState.start]
-            externalState.endComponents = endComponentTimes[externalState.end]
-            updateTextualState()
-
-
-            copyAlarmExternalState()
-
-        }
-        .alert(item: $presentableStatus) { status in
-            Alert(title: Text(status.title), message: Text(status.message) , dismissButton: .default(Text("Got it!")))
-        }
-
-
-    }
-
-    //decided against uding nscoding with copy() here
-    private func copyAlarmExternalState() {
-        externalStateCopy = AlarmTimeCellExternalState()
-        /*
-         var id = UUID()
-
-         @Published var start : Int = 0
-         @Published var end : Int = 0
-
-         // These will be auto populøated
-         // when the start and end properties above change
-         @Published var startComponents : DateComponents? = nil
-         @Published var endComponents : DateComponents? = nil
-
-         @Published var componentsAsText : String = ""**/
-        externalStateCopy.id = externalState.id
-        externalStateCopy.start = externalState.start
-        externalStateCopy.end = externalState.end
-        externalStateCopy.startComponents = externalState.startComponents
-        externalStateCopy.endComponents = externalStateCopy.endComponents
-        externalStateCopy.componentsAsText = externalState.componentsAsText
-
-
-    }
-
-    private func restoreAlarmExternalState(){
-        externalState.id = externalStateCopy.id
-        externalState.start = externalStateCopy.start
-        externalState.end = externalStateCopy.end
-        externalState.startComponents = externalStateCopy.startComponents
-        externalStateCopy.endComponents =  externalStateCopy.endComponents
-        externalState.componentsAsText = externalStateCopy.componentsAsText
-
-
-    }
-
-
-    init() {
-        startComponentTimes = Self.defaultTimeArray()
-        endComponentTimes = Self.defaultTimeArray()
-
-
-        //string representations of the datecomponents arrays   
-
-        for component in startComponentTimes {
-            startTimes.append(component.ToTimeString(wantsAMPM:  Date.LocaleWantsAMPM))
-        }
-
-        for component in endComponentTimes {
-            endTimes.append(component.ToTimeString(wantsAMPM:  Date.LocaleWantsAMPM))
-
-        }
-
-
-    }
-}
-
-struct CustomDataPickerView_Previews: PreviewProvider {
-    static var previews: some View {
-        CustomDataPickerView().environmentObject(AlarmTimeCellExternalState())
-    }
-}

+ 2 - 16
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SettingsView.swift

@@ -151,7 +151,7 @@ struct SettingsView: View {
         overview
             //.navigationViewStyle(StackNavigationViewStyle())
             .navigationBarTitle(Text("Libre Bluetooth"), displayMode: .inline)
-            .navigationBarItems(trailing: dismissButton)
+            .navigationBarItems(leading: dismissButton)
             .onAppear{
                 print("dabear:: settingsview appeared")
                 //While loop does this request on our behalf, freeaps does not
@@ -181,12 +181,6 @@ struct SettingsView: View {
 
     }
 
-    var snoozeSection: some View {
-        Section {
-            Text("Snooze Alerts").frame(alignment: .center)
-        }
-    }
-
     var measurementSection : some View {
         Section(header: Text("Last measurement")) {
             if glucoseUnit == .millimolesPerLiter {
@@ -275,7 +269,7 @@ struct SettingsView: View {
             // to notify the parent to close the cgmviewcontrollers navigation
             notifyComplete.notify()
         }) {
-            Text("Done")
+            Text("Close")
         }
     }
 
@@ -303,13 +297,6 @@ struct SettingsView: View {
     //todo: replace sub with navigationlinks
     var advancedSection: some View {
         Section(header: Text("Advanced")) {
-            //these subviews don't really need to be notified once glucose unit changes
-            // so we just pass glucoseunit directly on init
-            ZStack {
-                NavigationLink(destination: AlarmSettingsView(glucoseUnit: self.glucoseUnit)) {
-                    SettingsItem(title: "Alarms", detail: .constant(""))
-                }
-            }
             ZStack {
                 NavigationLink(destination: GlucoseSettingsView(glucoseUnit: self.glucoseUnit)) {
                     SettingsItem(title: "Glucose Settings", detail: .constant(""))
@@ -339,7 +326,6 @@ struct SettingsView: View {
 
     var overview: some View {
         List {
-            snoozeSection
             measurementSection
 //            if !glucoseMeasurement.predictionDate.isEmpty{
 //                predictionSection

+ 0 - 140
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SnoozeView.swift

@@ -1,140 +0,0 @@
-//
-//  TestView.swift
-//  MiaomiaoClientUI
-//
-//  Created by Bjørn Inge Berg on 15/10/2020.
-//  Copyright © 2020 Bjørn Inge Vikhammermo Berg. All rights reserved.
-//
-
-import SwiftUI
-
-struct SnoozeView: View {
-
-    private var pickerTimes: [TimeInterval] {
-        SnoozeView.pickerTimesArray()
-    }
-
-    private var formatter: DateComponentsFormatter {
-        let formatter = DateComponentsFormatter()
-        formatter.allowsFractionalUnits = false
-        formatter.unitsStyle = .full
-        return formatter
-    }
-
-    private func formatInterval(_ interval: TimeInterval) -> String {
-        formatter.string(from: interval)!
-    }
-
-    private var dateFormatter: DateFormatter {
-        let formatter = DateFormatter()
-        formatter.timeStyle = .short
-        return formatter
-    }
-
-    @Binding var isAlarming : Bool
-    @Binding var activeAlarms: LibreTransmitter.GlucoseScheduleAlarmResult
-
-    static func pickerTimesArray() -> [TimeInterval] {
-        var arr  = [TimeInterval]()
-
-        let mins10 = 0.166_67
-        let mins20 = mins10 * 2
-        let mins30 = mins10 * 3
-        //let mins40 = mins10 * 4
-
-        for hr in 0..<2 {
-            for min in [0.0, mins20, mins20 * 2] {
-                arr.append(TimeInterval(hours: Double(hr) + min))
-            }
-        }
-        for hr in 2..<4 {
-            for min in [0.0, mins30] {
-                arr.append(TimeInterval(hours: Double(hr) + min))
-            }
-        }
-
-        for hr in 4...8 {
-            arr.append(TimeInterval(hours: Double(hr)))
-        }
-
-        return arr
-    }
-
-    func getSnoozeDescription() -> String {
-        var snoozeDescription  = ""
-        var celltext = ""
-
-        switch activeAlarms {
-            case .high:
-                celltext = NSLocalizedString("High Glucose Alarm active", comment: "High Glucose Alarm active")
-            case .low:
-                celltext = NSLocalizedString("Low Glucose Alarm active", comment: "Low Glucose Alarm active")
-            case .none:
-                celltext = NSLocalizedString("No Glucose Alarm active", comment: "No Glucose Alarm active")
-        }
-
-        if let until = GlucoseScheduleList.snoozedUntil, until > Date() {
-            snoozeDescription = String(format: NSLocalizedString("snoozing until %@", comment: "snoozing until %@"), dateFormatter.string(from: until))
-        } else {
-            snoozeDescription = NSLocalizedString("not snoozing", comment: "not snoozing")  
-        }
-
-        return [celltext, snoozeDescription].joined(separator: ", ")
-    }
-
-    @State private var selectedInterval = 0
-    @State private var snoozeDescription = "nothing to see here"
-
-    var snoozeButton: some View {
-        VStack(alignment: .leading) {
-            Button(action: {
-                print("snooze from testview clicked")
-                let interval = pickerTimes[selectedInterval]
-                let snoozeFor = formatter.string(from: interval)!
-                let untilDate = Date() + interval
-                UserDefaults.standard.snoozedUntil = untilDate < Date() ? nil : untilDate
-                print("will snooze for \(snoozeFor) until \(dateFormatter.string(from: untilDate))")
-                snoozeDescription = getSnoozeDescription()
-            }) {
-                Text("Click to Snooze Alerts")
-                    .padding()
-            }
-        }
-
-    }
-
-    var snoozePicker: some View {
-        VStack {
-            Picker(selection: $selectedInterval, label: Text("Strength")) {
-                ForEach(0 ..< pickerTimes.count) {
-                    Text(formatInterval(self.pickerTimes[$0]))
-                }
-            }
-            .pickerStyle(.wheel)
-        }
-
-    }
-
-    var snoozeDesc : some View {
-        VStack(alignment: .leading) {
-            Text(snoozeDescription)
-        }
-    }
-
-    var body: some View {
-        Form {
-            snoozeDesc
-            snoozePicker
-            snoozeButton
-        }
-        .onAppear {
-            snoozeDescription = getSnoozeDescription()
-        }
-    }
-}
-
-struct TestView_Previews: PreviewProvider {
-    static var previews: some View {
-        SnoozeView(isAlarming: .constant(true), activeAlarms: .constant(.none))
-    }
-}

+ 0 - 21
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Observables/AlarmStatus.swift

@@ -1,21 +0,0 @@
-//
-//  AlarmStatus.swift
-//  LibreTransmitter
-//
-//  Created by Bjørn Inge Berg on 09/07/2021.
-//  Copyright © 2021 Mark Wilson. All rights reserved.
-//
-
-import Foundation
-public class AlarmStatus : ObservableObject , Equatable, Hashable{
-    @Published public var isAlarming = false
-    @Published public var glucoseScheduleAlarmResult = GlucoseScheduleAlarmResult.none
-
-    public static func ==(lhs: AlarmStatus, rhs: AlarmStatus) -> Bool {
-         lhs.isAlarming == rhs.isAlarming && lhs.glucoseScheduleAlarmResult == rhs.glucoseScheduleAlarmResult
-    }
-
-    static public func createNew() -> AlarmStatus{
-        AlarmStatus()
-    }
-}

+ 58 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -290,6 +290,9 @@
 		E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0127368630002FF094 /* APSAssembly.swift */; };
 		E00EEC0827368630002FF094 /* NetworkAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E00EEC0227368630002FF094 /* NetworkAssembly.swift */; };
 		E013D872273AC6FE0014109C /* GlucoseSimulatorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */; };
+		E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06B9119275B5EEA003C04B6 /* Array+Extension.swift */; };
+		E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */; };
+		E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */; };
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
 		E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
@@ -299,6 +302,11 @@
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
 		F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
+		F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692A9274B7AAE0037068D /* HealthKitManager.swift */; };
+		F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692CE274B999A0037068D /* HealthKitDataFlow.swift */; };
+		F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D0274B99B60037068D /* HealthKitProvider.swift */; };
+		F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */; };
+		F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D5274B9A450037068D /* HealthKitStateModel.swift */; };
 		FA630397F76B582C8D8681A7 /* BasalProfileEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42369F66CF91F30624C0B3A6 /* BasalProfileEditorProvider.swift */; };
 /* End PBXBuildFile section */
 
@@ -688,11 +696,19 @@
 		E00EEC0127368630002FF094 /* APSAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APSAssembly.swift; sourceTree = "<group>"; };
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
 		E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSimulatorSource.swift; sourceTree = "<group>"; };
+		E06B9119275B5EEA003C04B6 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = "<group>"; };
+		E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = HealthKit.framework; path = System/Library/Frameworks/HealthKit.framework; sourceTree = SDKROOT; };
+		E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitSample.swift; sourceTree = "<group>"; };
 		E26904AACA8D9C15D229D675 /* SnoozeStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SnoozeStateModel.swift; sourceTree = "<group>"; };
 		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
 		E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigProvider.swift; sourceTree = "<group>"; };
 		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
+		F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
+		F90692CE274B999A0037068D /* HealthKitDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitDataFlow.swift; sourceTree = "<group>"; };
+		F90692D0274B99B60037068D /* HealthKitProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitProvider.swift; sourceTree = "<group>"; };
+		F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleHealthKitRootView.swift; sourceTree = "<group>"; };
+		F90692D5274B9A450037068D /* HealthKitStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitStateModel.swift; sourceTree = "<group>"; };
 		FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
@@ -719,6 +735,7 @@
 				38B17B6625DD90E0005CAE3D /* SwiftDate in Frameworks */,
 				3818AA60274C26A300843DB3 /* Crypto.framework in Frameworks */,
 				3833B46D26012030003021B3 /* Algorithms in Frameworks */,
+				E0CC2C5C275B9F0F00A7BC71 /* HealthKit.framework in Frameworks */,
 				3818AA62274C26A400843DB3 /* MinimedKit.framework in Frameworks */,
 				3818AA58274C26A300843DB3 /* LoopKit.framework in Frameworks */,
 			);
@@ -823,6 +840,7 @@
 		3811DE0325C9D31700A708ED /* Modules */ = {
 			isa = PBXGroup;
 			children = (
+				F90692CD274B99850037068D /* HealthKit */,
 				6DC5D590658EF8B8DF94F9F5 /* AddCarbs */,
 				A9A4C88374496B3C89058A89 /* AddTempTarget */,
 				672F63EEAE27400625E14BAD /* AutotuneConfig */,
@@ -951,6 +969,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				F90692A8274B7A980037068D /* HealthKit */,
 				38E8754D275556E100975559 /* WatchManager */,
 				38E87406274F9AA500975559 /* UserNotifiactions */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
@@ -1084,6 +1103,7 @@
 		3818AA48274C267000843DB3 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				E0CC2C5B275B9DAE00A7BC71 /* HealthKit.framework */,
 				38E87402274F78C000975559 /* libswiftCoreNFC.tbd */,
 				38E873FD274F761800975559 /* CoreNFC.framework */,
 				3818AA70274C278200843DB3 /* LoopTestingKit.framework */,
@@ -1238,6 +1258,7 @@
 				38A0364125ED069400FCBB52 /* TempBasal.swift */,
 				3871F39B25ED892B0013ECB5 /* TempTarget.swift */,
 				3811DE8E25C9D80400A708ED /* User.swift */,
+				E0D4F80427513ECF00BDF1FE /* HealthKitSample.swift */,
 			);
 			path = Models;
 			sourceTree = "<group>";
@@ -1264,6 +1285,7 @@
 				3811DE5525C9D4D500A708ED /* Publisher.swift */,
 				38E98A3625F5509500C0CED0 /* String+Extensions.swift */,
 				3811DEE325CA063400A708ED /* PropertyWrappers */,
+				E06B9119275B5EEA003C04B6 /* Array+Extension.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -1770,6 +1792,33 @@
 			path = CGM;
 			sourceTree = "<group>";
 		};
+		F90692A8274B7A980037068D /* HealthKit */ = {
+			isa = PBXGroup;
+			children = (
+				F90692A9274B7AAE0037068D /* HealthKitManager.swift */,
+			);
+			path = HealthKit;
+			sourceTree = "<group>";
+		};
+		F90692CD274B99850037068D /* HealthKit */ = {
+			isa = PBXGroup;
+			children = (
+				F90692CE274B999A0037068D /* HealthKitDataFlow.swift */,
+				F90692D0274B99B60037068D /* HealthKitProvider.swift */,
+				F90692D5274B9A450037068D /* HealthKitStateModel.swift */,
+				F90692D4274B9A160037068D /* View */,
+			);
+			path = HealthKit;
+			sourceTree = "<group>";
+		};
+		F90692D4274B9A160037068D /* View */ = {
+			isa = PBXGroup;
+			children = (
+				F90692D2274B9A130037068D /* AppleHealthKitRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
@@ -2004,6 +2053,7 @@
 				38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */,
 				38D0B3B625EBE24900CB6E88 /* Battery.swift in Sources */,
 				38C4D33725E9A1A300D30B77 /* DispatchQueue+Extensions.swift in Sources */,
+				F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */,
 				3862CC2E2743F9F700BF832C /* CalendarManager.swift in Sources */,
 				38B4F3C325E2A20B00E76A18 /* PumpSetupView.swift in Sources */,
 				38E4453C274E411700EC9A94 /* Disk+Codable.swift in Sources */,
@@ -2128,10 +2178,12 @@
 				2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */,
 				6B9625766B697D1C98E455A2 /* PumpSettingsEditorStateModel.swift in Sources */,
 				A0B8EC8CC5CD1DD237D1BCD2 /* PumpSettingsEditorRootView.swift in Sources */,
+				E06B911A275B5EEA003C04B6 /* Array+Extension.swift in Sources */,
 				38EA0600262091870064E39B /* BolusProgressViewStyle.swift in Sources */,
 				389ECDFE2601061500D86C4F /* View+Snapshot.swift in Sources */,
 				38FEF3FE2738083E00574A46 /* CGMProvider.swift in Sources */,
 				38E98A3725F5509500C0CED0 /* String+Extensions.swift in Sources */,
+				F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */,
 				385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */,
 				8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */,
 				389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */,
@@ -2139,6 +2191,7 @@
 				63E890B4D951EAA91C071D5C /* BasalProfileEditorStateModel.swift in Sources */,
 				38FEF3FA2737E42000574A46 /* BaseStateModel.swift in Sources */,
 				385CEA8225F23DFD002D6D5B /* NightscoutStatus.swift in Sources */,
+				F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */,
 				38887CCE25F5725200944304 /* IOBEntry.swift in Sources */,
 				38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */,
 				CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */,
@@ -2176,6 +2229,7 @@
 				D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */,
 				38E4451E274DB04600EC9A94 /* AppDelegate.swift in Sources */,
 				5BFA1C2208114643B77F8CEB /* AddTempTargetProvider.swift in Sources */,
+				E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */,
 				919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */,
 				8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */,
 				38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */,
@@ -2186,9 +2240,11 @@
 				38569353270B5E350002C50D /* CGMRootView.swift in Sources */,
 				69A31254F2451C20361D172F /* BolusStateModel.swift in Sources */,
 				0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */,
+				F90692D3274B9A130037068D /* AppleHealthKitRootView.swift in Sources */,
 				3862CC1F273FDC9200BF832C /* CalibrationsChart.swift in Sources */,
 				711C0CB42CAABE788916BC9D /* ManualTempBasalDataFlow.swift in Sources */,
 				BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */,
+				F90692D6274B9A450037068D /* HealthKitStateModel.swift in Sources */,
 				C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */,
 				38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */,
 				7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */,
@@ -2607,6 +2663,7 @@
 				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME) WatchKit Extension";
 				INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",
@@ -2642,6 +2699,7 @@
 				INFOPLIST_KEY_CFBundleDisplayName = "$(APP_DISPLAY_NAME) WatchKit Extension";
 				INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_MODULE_NAME).ComplicationController";
 				INFOPLIST_KEY_NSHumanReadableCopyright = "";
+				INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO;
 				LD_RUNPATH_SEARCH_PATHS = (
 					"$(inherited)",
 					"@executable_path/Frameworks",

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

@@ -33,17 +33,17 @@
 		<key>FreeAPSWatch (Complication).xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>144</integer>
+			<integer>18</integer>
 		</dict>
 		<key>FreeAPSWatch (Notification).xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>143</integer>
+			<integer>17</integer>
 		</dict>
 		<key>FreeAPSWatch.xcscheme_^#shared#^_</key>
 		<dict>
 			<key>orderHint</key>
-			<integer>142</integer>
+			<integer>16</integer>
 		</dict>
 		<key>ReactiveSwift (Playground) 1.xcscheme</key>
 		<dict>

+ 6 - 0
FreeAPS/Resources/FreeAPS.entitlements

@@ -2,6 +2,12 @@
 <!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.developer.healthkit</key>
+	<true/>
+	<key>com.apple.developer.healthkit.access</key>
+	<array/>
+	<key>com.apple.developer.healthkit.background-delivery</key>
+	<true/>
 	<key>com.apple.developer.nfc.readersession.formats</key>
 	<array>
 		<string>TAG</string>

+ 4 - 0
FreeAPS/Resources/Info.plist

@@ -60,6 +60,10 @@
 	<string>Calendar is used to create a new glucose events.</string>
 	<key>NSFaceIDUsageDescription</key>
 	<string>For authorized acces to bolus</string>
+	<key>NSHealthShareUsageDescription</key>
+	<string>Health App is used to store blood glucose data</string>
+	<key>NSHealthUpdateUsageDescription</key>
+	<string>Health App is used to store blood glucose data</string>
 	<key>UIApplicationSceneManifest</key>
 	<dict>
 		<key>UIApplicationSupportsMultipleScenes</key>

+ 1 - 1
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -62,7 +62,7 @@ struct AppGroupSource: GlucoseSource {
     }
 
     func sourceInfo() -> [String: Any]? {
-        [GlucoseSourceKey.description.rawValue: "Group ID: \(String(describing: Bundle.main.appGroupSuiteName))"]
+        [GlucoseSourceKey.description.rawValue: "Group ID: \(Bundle.main.appGroupSuiteName ?? "Not set"))"]
     }
 }
 

+ 2 - 0
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -12,6 +12,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
     @Injected() var apsManager: APSManager!
     @Injected() var settingsManager: SettingsManager!
     @Injected() var libreTransmitter: LibreTransmitterSource!
+    @Injected() var healthKitManager: HealthKitManager!
 
     private var lifetime = Lifetime()
     private let timer = DispatchTimer(timeInterval: 1.minutes.timeInterval)
@@ -71,6 +72,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                     self.glucoseStorage.storeGlucose(filtered)
                     self.apsManager.heartbeat(date: date, force: false)
                     self.nightscoutManager.uploadGlucose()
+                    self.healthKitManager.save(bloodGlucoses: filtered, completion: nil)
                 }
             }
             .store(in: &lifetime)

+ 4 - 0
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -86,4 +86,8 @@ extension OpenAPS {
         static let tempTargetsPresets = "freeaps/temptargets_presets.json"
         static let calibrations = "freeaps/calibrations.json"
     }
+
+    enum HealthKit {
+        static let downloadedGlucose = "healthkit/downloaded-glucose.json"
+    }
 }

+ 36 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -4,6 +4,8 @@ import Swinject
 
 protocol GlucoseStorage {
     func storeGlucose(_ glucose: [BloodGlucose])
+    func removeGlucose(byID id: String)
+    func removeGlucose(byIDCollection ids: [String])
     func recent() -> [BloodGlucose]
     func syncDate() -> Date
     func filterTooFrequentGlucose(_ glucose: [BloodGlucose], at: Date) -> [BloodGlucose]
@@ -48,6 +50,40 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
         }
     }
 
+    func removeGlucose(byIDCollection ids: [String]) {
+        processQueue.sync {
+            let file = OpenAPS.Monitor.glucose
+            self.storage.transaction { storage in
+                let BGInStorage = storage.retrieve(file, as: [BloodGlucose].self)
+                let filteredBG = BGInStorage?.filter { !ids.contains($0.id) } ?? []
+                storage.save(filteredBG, as: file)
+
+                DispatchQueue.main.async {
+                    self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                        $0.glucoseDidUpdate(filteredBG.reversed())
+                    }
+                }
+            }
+        }
+    }
+
+    func removeGlucose(byID id: String) {
+        processQueue.sync {
+            let file = OpenAPS.Monitor.glucose
+            self.storage.transaction { storage in
+                let BGInStorage = storage.retrieve(file, as: [BloodGlucose].self)
+                let filteredBG = BGInStorage?.filter { $0.id != id } ?? []
+                storage.save(filteredBG, as: file)
+
+                DispatchQueue.main.async {
+                    self.broadcaster.notify(GlucoseObserver.self, on: .main) {
+                        $0.glucoseDidUpdate(filteredBG.reversed())
+                    }
+                }
+            }
+        }
+    }
+
     func syncDate() -> Date {
         guard let events = storage.retrieve(OpenAPS.Monitor.glucose, as: [BloodGlucose].self),
               let recent = events.first

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

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

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

@@ -1,4 +1,5 @@
 import Foundation
+import HealthKit
 import Swinject
 
 final class ServiceAssembly: Assembly {
@@ -14,6 +15,8 @@ final class ServiceAssembly: Assembly {
             return reporter
         }
         container.register(CalendarManager.self) { r in BaseCalendarManager(resolver: r) }
+        container.register(HKHealthStore.self) { _ in HKHealthStore() }
+        container.register(HealthKitManager.self) { r in BaseHealthKitManager(resolver: r) }
         container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
         container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) }
     }

+ 11 - 0
FreeAPS/Sources/Helpers/Array+Extension.swift

@@ -0,0 +1,11 @@
+extension Array where Element: Hashable {
+    func removeDublicates() -> Self {
+        var result = Self()
+        for item in self {
+            if !result.contains(item) {
+                result.append(item)
+            }
+        }
+        return result
+    }
+}

+ 3 - 1
FreeAPS/Sources/Helpers/PropertyWrappers/PersistedProperty.swift

@@ -43,6 +43,8 @@ import Foundation
         initialValue = wrappedValue
         lock?.lock()
         defer { lock?.unlock() }
-        setValue(storage.getValue(Value.self, forKey: key) ?? wrappedValue)
+        if storage.getValue(Value.self, forKey: key) == nil {
+            setValue(wrappedValue)
+        }
     }
 }

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

@@ -997,3 +997,12 @@ Enact a temp Basal or a temp target */
 
 /* "Noisy CGM Target Multiplier" */
 "Defaults to 1.3. Increase target by this amount when looping off raw/noisy CGM data" = "Defaults to 1.3. Increase target by this amount when looping off raw/noisy CGM data";
+
+/* */
+"Apple Health" = "Apple Health";
+
+/* */
+"Connect to Apple Health" = "Connect to Apple Health";
+
+/* Show when have not permissions for writing to Health */
+"For write data to Apple Health you must give permissions in Settings > Health > Data Access" = "For write data to Apple Health you must give permissions in Settings > Health > Data Access";

+ 1 - 0
FreeAPS/Sources/Localizations/Main/it.lproj/Localizable.strings

@@ -843,6 +843,7 @@ Enact a temp Basal or a temp target */
 "Carbs required: %d g" = "Carbs required: %d g";
 
 /* */
+
 "To prevent LOW required %d g of carbs" = "To prevent LOW required %d g of carbs";
 
 /* */

+ 9 - 0
FreeAPS/Sources/Localizations/Main/ru.lproj/Localizable.strings

@@ -1003,3 +1003,12 @@ Enact a temp Basal or a temp target */
 
 /* "Noisy CGM Target Multiplier" */
 "Defaults to 1.3. Increase target by this amount when looping off raw/noisy CGM data" = "По умолчанию 1,3. Увеличивает цель на эту величину, когда цикл отключается из за шума сенсора";
+
+/* */
+"Apple Health" = "Apple Health";
+
+/* */
+"Connect to Apple Health" = "Подключить к Apple Health";
+
+/* Show when have not permissions for writing to Health */
+"For write data to Apple Health you must give permissions in Settings > Health > Data Access" = "Чтобы записывать данные в Apple Health вам необходимо дать соответствующие разрешения, перейдя к меню Настройки > Здоровье > Доступ к данным";

+ 15 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -14,6 +14,10 @@ struct FreeAPSSettings: JSON, Equatable {
     var cgm: CGMType = .nightscout
     var uploadGlucose: Bool = false
     var useCalendar: Bool = false
+    // Apple Health Integration
+    var useAppleHealth: Bool = false
+    var needShowInformationTextForSetPermissions: Bool = false
+    // ---
     var glucoseBadge: Bool = false
     var glucoseNotificationsAlways: Bool = false
     var useAlarmSound: Bool = false
@@ -81,6 +85,17 @@ extension FreeAPSSettings: Decodable {
             settings.useCalendar = useCalendar
         }
 
+        if let useAppleHealth = try? container.decode(Bool.self, forKey: .useAppleHealth) {
+            settings.useAppleHealth = useAppleHealth
+        }
+
+        if let needShowInformationTextForSetPermissions = try? container.decode(
+            Bool.self,
+            forKey: .needShowInformationTextForSetPermissions
+        ) {
+            settings.needShowInformationTextForSetPermissions = needShowInformationTextForSetPermissions
+        }
+
         if let glucoseBadge = try? container.decode(Bool.self, forKey: .glucoseBadge) {
             settings.glucoseBadge = glucoseBadge
         }

+ 19 - 0
FreeAPS/Sources/Models/HealthKitSample.swift

@@ -0,0 +1,19 @@
+import Foundation
+
+struct HealthKitSample: JSON, Hashable, Equatable {
+    var healthKitId: String
+    var date: Date
+    var glucose: Int
+
+    static func == (lhs: HealthKitSample, rhs: HealthKitSample) -> Bool {
+        lhs.healthKitId == rhs.healthKitId
+    }
+}
+
+extension HealthKitSample {
+    private enum CodingKeys: String, CodingKey {
+        case healthKitId = "healthkit_id"
+        case date
+        case glucose
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/HealthKit/HealthKitDataFlow.swift

@@ -0,0 +1,5 @@
+enum AppleHealthKit {
+    enum Config {}
+}
+
+protocol AppleHealthKitProvider: Provider {}

+ 3 - 0
FreeAPS/Sources/Modules/HealthKit/HealthKitProvider.swift

@@ -0,0 +1,3 @@
+extension AppleHealthKit {
+    final class Provider: BaseProvider, AppleHealthKitProvider {}
+}

+ 44 - 0
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -0,0 +1,44 @@
+import Combine
+import SwiftUI
+
+extension AppleHealthKit {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() var healthKitManager: HealthKitManager!
+
+        @Published var useAppleHealth = false
+        @Published var needShowInformationTextForSetPermissions = false
+
+        override func subscribe() {
+            useAppleHealth = settingsManager.settings.useAppleHealth
+            needShowInformationTextForSetPermissions = settingsManager.settings.needShowInformationTextForSetPermissions
+
+            subscribeSetting(\.needShowInformationTextForSetPermissions, on: $needShowInformationTextForSetPermissions) { _ in }
+
+            $useAppleHealth
+                .removeDuplicates()
+                .sink { [weak self] value in
+                    guard let self = self else { return }
+                    guard value else {
+                        self.settingsManager.settings.useAppleHealth = false
+                        self.needShowInformationTextForSetPermissions = false
+                        return
+                    }
+
+                    self.healthKitManager.requestPermission { status, error in
+                        guard error == nil else {
+                            return
+                        }
+                        self.settingsManager.settings.useAppleHealth = status
+                        self.healthKitManager.enableBackgroundDelivery()
+                        self.healthKitManager.createObserver()
+                        DispatchQueue.main.async {
+                            if !self.healthKitManager.areAllowAllPermissions {
+                                self.needShowInformationTextForSetPermissions = true
+                            }
+                        }
+                    }
+                }
+                .store(in: &lifetime)
+        }
+    }
+}

+ 27 - 0
FreeAPS/Sources/Modules/HealthKit/View/AppleHealthKitRootView.swift

@@ -0,0 +1,27 @@
+import SwiftUI
+import Swinject
+
+extension AppleHealthKit {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Form {
+                Section {
+                    Toggle("Connect to Apple Health", isOn: $state.useAppleHealth)
+                    if state.needShowInformationTextForSetPermissions {
+                        HStack {
+                            Image(systemName: "exclamationmark.circle.fill")
+                            Text("For write data to Apple Health you must give permissions in Settings > Health > Data Access")
+                                .font(.caption)
+                        }
+                    }
+                }
+            }
+            .onAppear(perform: configureView)
+            .navigationTitle("Apple Health")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 2 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -99,6 +99,8 @@ extension Home {
                 ).onTapGesture {
                     isStatusPopupPresented = true
                 }.onLongPressGesture {
+                    let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
+                    impactHeavy.impactOccurred()
                     state.runLoop()
                 }
                 Spacer()

+ 3 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -20,6 +20,7 @@ extension Settings {
                 Section(header: Text("Services")) {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
                     Text("CGM").navigationLink(to: .cgm, from: self)
+                    Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     Text("Notifications").navigationLink(to: .notificationsConfig, from: self)
                 }
 
@@ -82,6 +83,8 @@ extension Settings {
                         }
 
                         Group {
+                            Text("HealthKit")
+                                .navigationLink(to: .configEditor(file: OpenAPS.HealthKit.downloadedGlucose), from: self)
                             Text("Target presets")
                                 .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
                             Text("Calibrations")

+ 1 - 3
FreeAPS/Sources/Modules/Snooze/View/SnoozeRootView.swift

@@ -107,9 +107,7 @@ extension Snooze {
         }
 
         var snoozeDesc: some View {
-            VStack(alignment: .leading) {
-                Text(snoozeDescription)
-            }
+            Text(snoozeDescription)
         }
 
         var body: some View {

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -21,6 +21,7 @@ enum Screen: Identifiable, Hashable {
     case autotuneConfig
     case dataTable
     case cgm
+    case healthkit
     case libreConfig
     case calibrations
     case notificationsConfig
@@ -70,6 +71,8 @@ extension Screen {
             DataTable.RootView(resolver: resolver)
         case .cgm:
             CGM.RootView(resolver: resolver)
+        case .healthkit:
+            AppleHealthKit.RootView(resolver: resolver)
         case .libreConfig:
             LibreConfig.RootView(resolver: resolver)
         case .calibrations:

+ 274 - 0
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -0,0 +1,274 @@
+import Foundation
+import HealthKit
+import Swinject
+
+protocol HealthKitManager {
+    /// Check availability HealthKit on current device and user's permissions
+    var isAvailableOnCurrentDevice: Bool { get }
+    /// Check all needed permissions
+    /// Return false if one or more permissions are deny or not choosen
+    var areAllowAllPermissions: Bool { get }
+    /// Check availability HealthKit on current device and user's permission of object
+    func isAvailableFor(object: HKObjectType) -> Bool
+    /// Requests user to give permissions on using HealthKit
+    func requestPermission(completion: ((Bool, Error?) -> Void)?)
+    /// Save blood glucose data to HealthKit store
+    func save(bloodGlucoses: [BloodGlucose], completion: ((Result<Bool, Error>) -> Void)?)
+    /// Create observer for data passing beetwen Health Store and FreeAPS
+    func createObserver()
+    /// Enable background delivering objects from Apple Health to FreeAPS
+    func enableBackgroundDelivery()
+}
+
+final class BaseHealthKitManager: HealthKitManager, Injectable {
+    @Injected() private var fileStorage: FileStorage!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+    @Injected() private var healthKitStore: HKHealthStore!
+
+    private enum Config {
+        // unwraped HKObjects
+        static var permissions: Set<HKSampleType> {
+            var result: Set<HKSampleType> = []
+            for permission in optionalPermissions {
+                result.insert(permission!)
+            }
+            return result
+        }
+
+        static let optionalPermissions = Set([Config.healthBGObject])
+        // link to object in HealthKit
+        static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
+
+        static let frequencyBackgroundDeliveryBloodGlucoseFromHealth = HKUpdateFrequency(rawValue: 10)!
+    }
+
+    var isAvailableOnCurrentDevice: Bool {
+        HKHealthStore.isHealthDataAvailable()
+    }
+
+    var areAllowAllPermissions: Bool {
+        var result = true
+        Config.permissions.forEach { permission in
+            if [HKAuthorizationStatus.sharingDenied, HKAuthorizationStatus.notDetermined]
+                .contains(healthKitStore.authorizationStatus(for: permission))
+            {
+                result = false
+            }
+        }
+        return result
+    }
+
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        guard isAvailableOnCurrentDevice, let bjObject = Config.healthBGObject else {
+            return
+        }
+        if isAvailableFor(object: bjObject) {
+            debug(.service, "Create HealthKit Observer for Blood Glucose")
+            createObserver()
+        }
+        enableBackgroundDelivery()
+    }
+
+    func isAvailableFor(object: HKObjectType) -> Bool {
+        let status = healthKitStore.authorizationStatus(for: object)
+        switch status {
+        case .sharingAuthorized:
+            return true
+        default:
+            return false
+        }
+    }
+
+    func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
+        guard isAvailableOnCurrentDevice else {
+            completion?(false, HKError.notAvailableOnCurrentDevice)
+            return
+        }
+        for permission in Config.optionalPermissions {
+            guard permission != nil else {
+                completion?(false, HKError.dataNotAvailable)
+                return
+            }
+        }
+
+        healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
+            completion?(status, error)
+        }
+    }
+
+    func save(bloodGlucoses: [BloodGlucose], completion: ((Result<Bool, Error>) -> Void)? = nil) {
+        for bgItem in bloodGlucoses {
+            let bgQuantity = HKQuantity(
+                unit: .milligramsPerDeciliter,
+                doubleValue: Double(bgItem.glucose!)
+            )
+
+            let bgObjectSample = HKQuantitySample(
+                type: Config.healthBGObject!,
+                quantity: bgQuantity,
+                start: bgItem.dateString,
+                end: bgItem.dateString,
+                metadata: [
+                    "HKMetadataKeyExternalUUID": bgItem.id,
+                    "HKMetadataKeySyncIdentifier": bgItem.id,
+                    "HKMetadataKeySyncVersion": 1,
+                    "fromFreeAPSX": true
+                ]
+            )
+
+            healthKitStore.save(bgObjectSample) { status, error in
+                guard error == nil else {
+                    completion?(Result.failure(error!))
+                    return
+                }
+                completion?(Result.success(status))
+            }
+        }
+    }
+
+    func createObserver() {
+        guard let bgType = Config.healthBGObject else {
+            warning(
+                .service,
+                "Can not create HealthKit Observer, because unable to get the Blood Glucose type",
+                description: nil,
+                error: nil
+            )
+            return
+        }
+
+        let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [unowned self] _, _, observerError in
+
+            if let _ = observerError {
+                return
+            }
+
+            // loading only daily bg
+            let predicate = HKQuery.predicateForSamples(
+                withStart: Date().addingTimeInterval(-1.days.timeInterval),
+                end: nil,
+                options: .strictStartDate
+            )
+
+            healthKitStore.execute(getQueryForDeletedBloodGlucose(sampleType: bgType, predicate: predicate))
+            healthKitStore.execute(getQueryForAddedBloodGlucose(sampleType: bgType, predicate: predicate))
+        }
+        healthKitStore.execute(query)
+    }
+
+    func enableBackgroundDelivery() {
+        guard let bgType = Config.healthBGObject else {
+            warning(
+                .service,
+                "Can not create HealthKit Background Delivery, because unable to get the Blood Glucose type",
+                description: nil,
+                error: nil
+            )
+            return
+        }
+
+        healthKitStore.enableBackgroundDelivery(
+            for: bgType,
+            frequency: Config.frequencyBackgroundDeliveryBloodGlucoseFromHealth
+        ) { status, e in
+            guard e == nil else {
+                warning(.service, "Can not enable background delivery for Apple Health", description: nil, error: e)
+                return
+            }
+            debug(.service, "HealthKit background delivery status is \(status)")
+        }
+    }
+
+    private func getQueryForDeletedBloodGlucose(sampleType: HKQuantityType, predicate: NSPredicate) -> HKQuery {
+        let query = HKAnchoredObjectQuery(
+            type: sampleType,
+            predicate: predicate,
+            anchor: nil,
+            limit: 1000
+        ) { [unowned self] _, _, deletedObjects, _, _ in
+            guard let samples = deletedObjects else {
+                return
+            }
+
+            DispatchQueue.global(qos: .utility).async {
+                var removingBGID = [String]()
+                samples.forEach {
+                    if let idString = $0.metadata?["HKMetadataKeySyncIdentifier"] as? String {
+                        removingBGID.append(idString)
+                    } else {
+                        removingBGID.append($0.uuid.uuidString)
+                    }
+                }
+                glucoseStorage.removeGlucose(byIDCollection: removingBGID)
+            }
+        }
+        return query
+    }
+
+    private func getQueryForAddedBloodGlucose(sampleType: HKQuantityType, predicate: NSPredicate) -> HKQuery {
+        let query = HKSampleQuery(
+            sampleType: sampleType,
+            predicate: predicate,
+            limit: Int(HKObjectQueryNoLimit),
+            sortDescriptors: nil
+        ) { [unowned self] _, results, _ in
+
+            guard let samples = results as? [HKQuantitySample] else {
+                return
+            }
+
+            let oldSamples: [HealthKitSample] = fileStorage
+                .retrieve(OpenAPS.HealthKit.downloadedGlucose, as: [HealthKitSample].self) ?? []
+
+            var newSamples = [HealthKitSample]()
+            for sample in samples {
+                if sample.wasUserEntered {
+                    newSamples.append(HealthKitSample(
+                        healthKitId: sample.uuid.uuidString,
+                        date: sample.startDate,
+                        glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
+                    ))
+                }
+            }
+
+            newSamples = newSamples
+                .filter { !oldSamples.contains($0) }
+
+            newSamples.forEach({ sample in
+                let glucose = BloodGlucose(
+                    _id: sample.healthKitId,
+                    sgv: sample.glucose,
+                    direction: nil,
+                    date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
+                    dateString: sample.date,
+                    unfiltered: nil,
+                    filtered: nil,
+                    noise: nil,
+                    glucose: sample.glucose,
+                    type: "sgv"
+                )
+                glucoseStorage.storeGlucose([glucose])
+            })
+
+            let savingSamples = (newSamples + oldSamples)
+                .removeDublicates()
+                .filter { $0.date >= Date().addingTimeInterval(-1.days.timeInterval) }
+
+            self.fileStorage.save(savingSamples, as: OpenAPS.HealthKit.downloadedGlucose)
+        }
+        return query
+    }
+}
+
+enum HealthKitPermissionRequestStatus {
+    case needRequest
+    case didRequest
+}
+
+enum HKError: Error {
+    // HealthKit work only iPhone (not on iPad)
+    case notAvailableOnCurrentDevice
+    // Some data can be not available on current iOS-device
+    case dataNotAvailable
+}

+ 2 - 2
FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

@@ -83,7 +83,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
                 titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
             } else {
                 content.sound = .default
-                self.playSound()
+                self.playSoundIfNeeded()
             }
 
             titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
@@ -313,7 +313,7 @@ final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, In
     static let soundID: UInt32 = 1336
     private static var stopPlaying = false
 
-    private func playSound(times: Int = 3) {
+    private func playSound(times: Int = 1) {
         guard times > 0, !Self.stopPlaying else {
             return
         }

+ 8 - 3
FreeAPSWatch WatchKit Extension/Views/BolusConfirmationView.swift

@@ -16,12 +16,17 @@ struct BolusConfirmationView: View {
     var body: some View {
         VStack {
             GeometryReader { geo in
-                ZStack {
+                ZStack(alignment: .top) {
                     RoundedRectangle(cornerRadius: elementSize / 2, style: .circular)
                         .fill(.secondary)
                         .frame(width: elementSize, height: geo.size.height)
                         .opacity(0.2)
 
+                    RoundedRectangle(cornerRadius: elementSize / 2, style: .circular)
+                        .fill(Color.insulin)
+                        .frame(width: elementSize, height: elementSize + (geo.size.height - elementSize) * progress / 100)
+                        .opacity(0.2)
+
                     Image(systemName: "arrow.right")
                         .resizable()
                         .frame(width: elementSize / 2, height: elementSize / 2)
@@ -66,7 +71,7 @@ struct BolusConfirmationView: View {
             $crownProgress,
             from: 0.0,
             through: 100.0,
-            by: 1,
+            by: 0.5,
             sensitivity: .high,
             isContinuous: false,
             isHapticFeedbackEnabled: true
@@ -104,6 +109,6 @@ struct BolusConfirmationView: View {
 
 struct BolusConfirmationView_Previews: PreviewProvider {
     static var previews: some View {
-        BolusConfirmationView().environmentObject(WatchStateModel())
+        BolusConfirmationView(progress: 50, done: false).environmentObject(WatchStateModel())
     }
 }

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

@@ -51,6 +51,7 @@ struct BolusView: View {
 
                 HStack {
                     Button {
+                        WKInterfaceDevice.current().play(.click)
                         state.isBolusViewActive = false
                     }
                     label: {

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

@@ -19,15 +19,6 @@ struct CarbsView: View {
         GeometryReader { geo in
             VStack(spacing: 16) {
                 HStack {
-<<<<<<< HEAD
-                    Image("carbs", bundle: nil)
-                        .renderingMode(.template)
-                        .resizable()
-                        .frame(width: 24, height: 24)
-                        .foregroundColor(.loopGreen)
-                    Text("Add Carbs ")
-
-=======
                     Button {
                         WKInterfaceDevice.current().play(.click)
                         let newValue = amount - 5
@@ -70,7 +61,6 @@ struct CarbsView: View {
                             .foregroundColor(.loopGreen)
                         Text("Add Carbs ")
                     }
->>>>>>> 1e5c754... 38mm watch support and fixed bolus rounding
                 }
                 .disabled(amount <= 0)
             }.frame(maxHeight: .infinity)