From 60e11e4330d5439b1ba9fc1b90ee8990e78cebdb Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 8 May 2024 08:14:33 +0200 Subject: [PATCH 01/14] contact trick wip --- FreeAPS.xcodeproj/project.pbxproj | 60 +++ FreeAPS/Resources/Info.plist | 6 + FreeAPS/Sources/APS/OpenAPS/Constants.swift | 1 + .../Sources/Assemblies/ServiceAssembly.swift | 1 + .../Sources/Models/ContactTrickEntry.swift | 71 +++ FreeAPS/Sources/Models/FontWeight.swift | 29 ++ .../Sources/Modules/Base/BaseProvider.swift | 1 + .../ContactTrick/ContactTrickDataFlow.swift | 30 ++ .../ContactTrick/ContactTrickProvider.swift | 28 ++ .../ContactTrick/ContactTrickStateModel.swift | 106 +++++ .../View/ContactTrickRootView.swift | 272 +++++++++++ .../Settings/View/SettingsRootView.swift | 3 +- FreeAPS/Sources/Router/Screen.swift | 3 + .../ContactTrick/ContactPicture.swift | 223 +++++++++ .../ContactTrick/ContactTrickManager.swift | 423 ++++++++++++++++++ .../ContactTrick/ContactTrickState.swift | 41 ++ 16 files changed, 1297 insertions(+), 1 deletion(-) create mode 100644 FreeAPS/Sources/Models/ContactTrickEntry.swift create mode 100644 FreeAPS/Sources/Models/FontWeight.swift create mode 100644 FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift create mode 100644 FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift create mode 100644 FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift create mode 100644 FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift create mode 100644 FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift create mode 100644 FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift create mode 100644 FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 1d7e1a9227..35be544362 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -445,6 +445,15 @@ E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */; }; E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */; }; E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; }; + F2159A4A2BA60A6000A0B716 /* ContactTrickDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A492BA60A6000A0B716 /* ContactTrickDataFlow.swift */; }; + F2159A4C2BA60A8E00A0B716 /* ContactTrickRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A4B2BA60A8E00A0B716 /* ContactTrickRootView.swift */; }; + F2159A4E2BA60AC000A0B716 /* ContactTrickProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A4D2BA60AC000A0B716 /* ContactTrickProvider.swift */; }; + F2159A502BA60AE400A0B716 /* ContactTrickStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A4F2BA60AE400A0B716 /* ContactTrickStateModel.swift */; }; + F2159A522BA60F7A00A0B716 /* FontWeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A512BA60F7A00A0B716 /* FontWeight.swift */; }; + F2159A542BA6207F00A0B716 /* ContactTrickEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */; }; + F2159A572BA6239F00A0B716 /* ContactTrickManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A562BA6239F00A0B716 /* ContactTrickManager.swift */; }; + F2159A592BA78B7400A0B716 /* ContactTrickState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A582BA78B7400A0B716 /* ContactTrickState.swift */; }; + F2159A5B2BA7939C00A0B716 /* ContactPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A5A2BA7939C00A0B716 /* ContactPicture.swift */; }; F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; }; F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; }; F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; }; @@ -1031,6 +1040,15 @@ E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigProvider.swift; sourceTree = ""; }; E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = ""; }; E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = ""; }; + F2159A492BA60A6000A0B716 /* ContactTrickDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickDataFlow.swift; sourceTree = ""; }; + F2159A4B2BA60A8E00A0B716 /* ContactTrickRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickRootView.swift; sourceTree = ""; }; + F2159A4D2BA60AC000A0B716 /* ContactTrickProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickProvider.swift; sourceTree = ""; }; + F2159A4F2BA60AE400A0B716 /* ContactTrickStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickStateModel.swift; sourceTree = ""; }; + F2159A512BA60F7A00A0B716 /* FontWeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontWeight.swift; sourceTree = ""; }; + F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickEntry.swift; sourceTree = ""; }; + F2159A562BA6239F00A0B716 /* ContactTrickManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickManager.swift; sourceTree = ""; }; + F2159A582BA78B7400A0B716 /* ContactTrickState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickState.swift; sourceTree = ""; }; + F2159A5A2BA7939C00A0B716 /* ContactPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPicture.swift; sourceTree = ""; }; F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = ""; }; F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = ""; }; F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = ""; }; @@ -1341,6 +1359,7 @@ 49CA5A152BDA3815001F0D3A /* KetoProtect */, 49CA5A012BD8E459001F0D3A /* B30 */, CE1F2B982B011C58002EDCA0 /* AutoISF */, + F2159A472BA60A0300A0B716 /* ContactTrick */, 195D80B22AF696EE00D25097 /* Dynamic */, BD7DA9A32AE06DBA00601B20 /* BolusCalculatorConfig */, 190EBCC229FF134900BA767D /* StatConfig */, @@ -1480,6 +1499,7 @@ 3811DE9125C9D88200A708ED /* Services */ = { isa = PBXGroup; children = ( + F2159A552BA6238D00A0B716 /* ContactTrick */, 6B1A8D2C2B156EC100E76752 /* LiveActivity */, CEB434E128B8F9BC00B70274 /* Bluetooth */, F90692A8274B7A980037068D /* HealthKit */, @@ -1815,6 +1835,8 @@ CC41E2992B1E1F460070974F /* HistoryLayout.swift */, 19B60B772B5E7E97002F4F74 /* Threshold.swift */, 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */, + F2159A512BA60F7A00A0B716 /* FontWeight.swift */, + F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */, ); path = Models; sourceTree = ""; @@ -2563,6 +2585,35 @@ path = View; sourceTree = ""; }; + F2159A472BA60A0300A0B716 /* ContactTrick */ = { + isa = PBXGroup; + children = ( + F2159A482BA60A1600A0B716 /* View */, + F2159A492BA60A6000A0B716 /* ContactTrickDataFlow.swift */, + F2159A4D2BA60AC000A0B716 /* ContactTrickProvider.swift */, + F2159A4F2BA60AE400A0B716 /* ContactTrickStateModel.swift */, + ); + path = ContactTrick; + sourceTree = ""; + }; + F2159A482BA60A1600A0B716 /* View */ = { + isa = PBXGroup; + children = ( + F2159A4B2BA60A8E00A0B716 /* ContactTrickRootView.swift */, + ); + path = View; + sourceTree = ""; + }; + F2159A552BA6238D00A0B716 /* ContactTrick */ = { + isa = PBXGroup; + children = ( + F2159A562BA6239F00A0B716 /* ContactTrickManager.swift */, + F2159A582BA78B7400A0B716 /* ContactTrickState.swift */, + F2159A5A2BA7939C00A0B716 /* ContactPicture.swift */, + ); + path = ContactTrick; + sourceTree = ""; + }; F5DE2E6D7B2133BBD3353DC7 /* View */ = { isa = PBXGroup; children = ( @@ -2965,6 +3016,7 @@ 388E595C25AD948C0019842D /* FreeAPSApp.swift in Sources */, 38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */, CECCB4262BDBDCF7006E41C4 /* carbPresetResult.swift in Sources */, + F2159A542BA6207F00A0B716 /* ContactTrickEntry.swift in Sources */, 38569348270B5DFB0002C50D /* GlucoseSource.swift in Sources */, CEB434E328B8F9DB00B70274 /* BluetoothStateManager.swift in Sources */, 3811DE4225C9D4A100A708ED /* SettingsDataFlow.swift in Sources */, @@ -3072,14 +3124,17 @@ 495068BD2BDFF1B20048FF3B /* BaseIntentsRequest.swift in Sources */, 495068BC2BDFF1B20048FF3B /* AppShortcuts.swift in Sources */, 3811DEB025C9D88300A708ED /* BaseKeychain.swift in Sources */, + F2159A5B2BA7939C00A0B716 /* ContactPicture.swift in Sources */, 3811DE4325C9D4A100A708ED /* SettingsProvider.swift in Sources */, 45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */, 3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */, 38C4D33A25E9A1ED00D30B77 /* NSObject+AssociatedValues.swift in Sources */, + F2159A572BA6239F00A0B716 /* ContactTrickManager.swift in Sources */, 38DF179027733EAD00B3528F /* SnowScene.swift in Sources */, 38AAF8712600C1B0004AF583 /* MainChartView.swift in Sources */, 195F00482B5C267D00DAC71A /* DescriptionView.swift in Sources */, 19DC677F29CA675700FD9EC4 /* OverrideProfilesDataFlow.swift in Sources */, + F2159A522BA60F7A00A0B716 /* FontWeight.swift in Sources */, 1935364028496F7D001E0B16 /* Oref2_variables.swift in Sources */, CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */, 38E4453A274E411700EC9A94 /* Disk+[UIImage].swift in Sources */, @@ -3173,6 +3228,7 @@ CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */, E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */, DD399FB31EACB9343C944C4C /* PreferencesEditorStateModel.swift in Sources */, + F2159A4E2BA60AC000A0B716 /* ContactTrickProvider.swift in Sources */, 19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */, 44190F0BBA464D74B857D1FB /* PreferencesEditorRootView.swift in Sources */, E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */, @@ -3192,9 +3248,11 @@ E0D4F80527513ECF00BDF1FE /* HealthKitSample.swift in Sources */, 919DBD08F13BAFB180DF6F47 /* AddTempTargetStateModel.swift in Sources */, 49CA5A1A2BDA3873001F0D3A /* KetoProtectDataFlow.swift in Sources */, + F2159A4A2BA60A6000A0B716 /* ContactTrickDataFlow.swift in Sources */, 8BC2F5A29AD1ED08AC0EE013 /* AddTempTargetRootView.swift in Sources */, 38A00B1F25FC00F7006BC0B0 /* Autotune.swift in Sources */, 38AAF85525FFF846004AF583 /* CurrentGlucoseView.swift in Sources */, + F2159A4C2BA60A8E00A0B716 /* ContactTrickRootView.swift in Sources */, 041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */, 23888883D4EA091C88480FF2 /* BolusProvider.swift in Sources */, 38E98A2D25F52DC400C0CED0 /* NSLocking+Extensions.swift in Sources */, @@ -3220,6 +3278,7 @@ 38E4453B274E411700EC9A94 /* Disk+VolumeInformation.swift in Sources */, 7BCFACB97C821041BA43A114 /* ManualTempBasalRootView.swift in Sources */, 38E44534274E411700EC9A94 /* Disk+InternalHelpers.swift in Sources */, + F2159A592BA78B7400A0B716 /* ContactTrickState.swift in Sources */, 38A00B2325FC2B55006BC0B0 /* LRUCache.swift in Sources */, 3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */, 891DECF7BC20968D7F566161 /* AutotuneConfigProvider.swift in Sources */, @@ -3235,6 +3294,7 @@ 38569349270B5DFB0002C50D /* AppGroupSource.swift in Sources */, F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */, BA00D96F7B2FF169A06FB530 /* CGMStateModel.swift in Sources */, + F2159A502BA60AE400A0B716 /* ContactTrickStateModel.swift in Sources */, 61962FCAF8A2D222553AC5A3 /* LibreConfigDataFlow.swift in Sources */, BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */, 6EADD581738D64431902AC0A /* LibreConfigProvider.swift in Sources */, diff --git a/FreeAPS/Resources/Info.plist b/FreeAPS/Resources/Info.plist index 3bb17a29f7..9709c39a2e 100644 --- a/FreeAPS/Resources/Info.plist +++ b/FreeAPS/Resources/Info.plist @@ -115,6 +115,12 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown + NSCalendarsFullAccessUsageDescription + To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay + NSContactsUsageDescription + To update contacts with BG reading values (contact trick) + LSApplicationCategoryType + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/FreeAPS/Sources/APS/OpenAPS/Constants.swift b/FreeAPS/Sources/APS/OpenAPS/Constants.swift index b4df9ab0fe..137c1cac30 100644 --- a/FreeAPS/Sources/APS/OpenAPS/Constants.swift +++ b/FreeAPS/Sources/APS/OpenAPS/Constants.swift @@ -39,6 +39,7 @@ extension OpenAPS { static let carbRatios = "settings/carb_ratios.json" static let tempTargets = "settings/temptargets.json" static let model = "settings/model.json" + static let contactTrick = "settings/contact_trick.json" } enum Monitor { diff --git a/FreeAPS/Sources/Assemblies/ServiceAssembly.swift b/FreeAPS/Sources/Assemblies/ServiceAssembly.swift index f5a49697b2..bef9d8d665 100644 --- a/FreeAPS/Sources/Assemblies/ServiceAssembly.swift +++ b/FreeAPS/Sources/Assemblies/ServiceAssembly.swift @@ -20,6 +20,7 @@ final class ServiceAssembly: Assembly { container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) } container.register(WatchManager.self) { r in BaseWatchManager(resolver: r) } container.register(GarminManager.self) { r in BaseGarminManager(resolver: r) } + container.register(ContactTrickManager.self) { r in BaseContactTrickManager(resolver: r) } if #available(iOS 16.2, *) { container.register(LiveActivityBridge.self) { r in diff --git a/FreeAPS/Sources/Models/ContactTrickEntry.swift b/FreeAPS/Sources/Models/ContactTrickEntry.swift new file mode 100644 index 0000000000..f67fb4e257 --- /dev/null +++ b/FreeAPS/Sources/Models/ContactTrickEntry.swift @@ -0,0 +1,71 @@ + +struct ContactTrickEntry: JSON, Equatable { + var enabled: Bool = false + var layout: ContactTrickLayout + var primary: ContactTrickValue = .bg + var secondary: ContactTrickValue? = .trend + var contactId: String? = nil + var displayName: String? = nil + var trend: Bool = false + var ring: Bool = false + var darkMode: Bool = true + var fontSize: Int = 100 + var fontName: String = "Default Font" + var fontWeight: FontWeight = .medium + + func isDefaultFont() -> Bool { + fontName == "Default Font" + } +} + +protocol ContactTrickObserver { + func basalProfileDidChange(_ entry: [ContactTrickEntry]) +} + +extension ContactTrickEntry { + private enum CodingKeys: String, CodingKey { + case enabled + case layout + case primary + case secondary + case contactId + case displayName + case trend + case ring + case darkMode + case fontSize + case fontName + case fontWeight + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let enabled = try container.decode(Bool.self, forKey: .enabled) + let layout = try container.decode(ContactTrickLayout.self, forKey: .layout) + let primary = try container.decode(ContactTrickValue.self, forKey: .primary) + let secondary = try container.decodeIfPresent(ContactTrickValue.self, forKey: .secondary) + let contactId = try container.decodeIfPresent(String.self, forKey: .contactId) + let displayName = try container.decodeIfPresent(String.self, forKey: .displayName) + let trend = try container.decode(Bool.self, forKey: .trend) + let ring = try container.decode(Bool.self, forKey: .ring) + let darkMode = try container.decode(Bool.self, forKey: .darkMode) + let fontSize = try container.decode(Int.self, forKey: .fontSize) + let fontName = try container.decodeIfPresent(String.self, forKey: .fontName) ?? "Default Font" + let fontWeight = try container.decode(FontWeight.self, forKey: .fontWeight) + + self = ContactTrickEntry( + enabled: enabled, + layout: layout, + primary: primary, + secondary: secondary, + contactId: contactId, + displayName: displayName, + trend: trend, + ring: ring, + darkMode: darkMode, + fontSize: fontSize, + fontName: fontName, + fontWeight: fontWeight + ) + } +} diff --git a/FreeAPS/Sources/Models/FontWeight.swift b/FreeAPS/Sources/Models/FontWeight.swift new file mode 100644 index 0000000000..671d7d92f3 --- /dev/null +++ b/FreeAPS/Sources/Models/FontWeight.swift @@ -0,0 +1,29 @@ +import Foundation + +enum FontWeight: String, JSON, Identifiable, CaseIterable, Codable { + var id: String { rawValue } + + case light + case regular + case medium + case semibold + case bold + case black + + var displayName: String { + switch self { + case .light: + return NSLocalizedString("Light", comment: "") + case .regular: + return NSLocalizedString("Regular", comment: "") + case .medium: + return NSLocalizedString("Medium", comment: "") + case .semibold: + return NSLocalizedString("Semibold", comment: "") + case .bold: + return NSLocalizedString("Bold", comment: "") + case .black: + return NSLocalizedString("Black", comment: "") + } + } +} diff --git a/FreeAPS/Sources/Modules/Base/BaseProvider.swift b/FreeAPS/Sources/Modules/Base/BaseProvider.swift index deb4bc5424..9f85cbb33c 100644 --- a/FreeAPS/Sources/Modules/Base/BaseProvider.swift +++ b/FreeAPS/Sources/Modules/Base/BaseProvider.swift @@ -11,6 +11,7 @@ class BaseProvider: Provider, Injectable { @Injected() var deviceManager: DeviceDataManager! @Injected() var storage: FileStorage! @Injected() var bluetoothProvider: BluetoothStateManager! + @Injected() var contactTrickManager: ContactTrickManager! required init(resolver: Resolver) { injectServices(resolver) diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift new file mode 100644 index 0000000000..4c3cf67fb3 --- /dev/null +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickDataFlow.swift @@ -0,0 +1,30 @@ +import Combine +import Foundation + +enum ContactTrick { + enum Config {} + + class Item: Identifiable, Hashable, Equatable { + let id = UUID() + var index: Int = 0 + var entry: ContactTrickEntry + + init(index: Int, entry: ContactTrickEntry) { + self.index = index + self.entry = entry + } + + static func == (lhs: Item, rhs: Item) -> Bool { + lhs.index == rhs.index + } + + func hash(into hasher: inout Hasher) { + hasher.combine(index) + } + } +} + +protocol ContactTrickProvider: Provider { + var contacts: [ContactTrickEntry] { get } + func saveContacts(_ contacts: [ContactTrickEntry]) -> AnyPublisher +} diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift new file mode 100644 index 0000000000..a9f08f6dde --- /dev/null +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickProvider.swift @@ -0,0 +1,28 @@ +import Combine +import Foundation + +extension ContactTrick { + final class Provider: BaseProvider, ContactTrickProvider { + private let processQueue = DispatchQueue(label: "ContactTrickProvider.processQueue") + + var contacts: [ContactTrickEntry] { + storage.retrieve(OpenAPS.Settings.contactTrick, as: [ContactTrickEntry].self) + ?? [ContactTrickEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.contactTrick)) + ?? [] + } + + func saveContacts(_ contacts: [ContactTrickEntry]) -> AnyPublisher { + Future { promise in + self.storage.save(contacts, as: OpenAPS.Settings.contactTrick) + self.contactTrickManager.updateContacts(contacts: contacts) { result in + switch result { + case .success: + promise(.success(())) + case let .failure(error): + promise(.failure(error)) + } + } + }.eraseToAnyPublisher() + } + } +} diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift new file mode 100644 index 0000000000..cbb2e76970 --- /dev/null +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift @@ -0,0 +1,106 @@ +import ConnectIQ +import SwiftUI + +enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { + var id: String { rawValue } + case bg + case delta + case trend + case time + case cob + case iob + case isf + case override + case ring + + var displayName: String { + switch self { + case .bg: + return NSLocalizedString("BG", comment: "") + case .delta: + return NSLocalizedString("Delta", comment: "") + case .trend: + return NSLocalizedString("Trend", comment: "") + case .time: + return NSLocalizedString("Time", comment: "") + case .cob: + return NSLocalizedString("COB", comment: "") + case .iob: + return NSLocalizedString("IOB", comment: "") + case .isf: + return NSLocalizedString("ISF", comment: "") + case .override: + return NSLocalizedString("Override %", comment: "") + case .ring: + return NSLocalizedString("Ring", comment: "") + } + } +} + +enum ContactTrickLayout: String, JSON, CaseIterable, Identifiable, Codable { + var id: String { rawValue } + case single + case split + case ring + + var displayName: String { + switch self { + case .single: + return NSLocalizedString("Single", comment: "") + case .split: + return NSLocalizedString("Split", comment: "") + case .ring: + return NSLocalizedString("Ring", comment: "") + } + } +} + +extension ContactTrick { + final class StateModel: BaseStateModel { + @Published var syncInProgress = false + @Published var items: [Item] = [] + + override func subscribe() { + items = provider.contacts.enumerated().map { index, contact in + Item( + index: index, + entry: contact + ) + } + } + + func add() { + let newItem = Item( + index: items.count, + entry: ContactTrickEntry( + enabled: false, + value: .bg, + contactId: nil, + displayName: nil, + trend: false, + ring: false, + darkMode: true, + fontSize: 100, + fontName: "Default Font", + fontWeight: .medium + ) + ) + + items.append(newItem) + } + + func save() { + syncInProgress = true + let contacts = items.map { item -> ContactTrickEntry in + item.entry + } + provider.saveContacts(contacts) + .receive(on: DispatchQueue.main) + .sink { _ in + print("saved!") + self.syncInProgress = false + } receiveValue: {} + .store(in: &lifetime) + } + } +} diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift new file mode 100644 index 0000000000..1facd6f710 --- /dev/null +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -0,0 +1,272 @@ +import Contacts +import ContactsUI +import SwiftUI +import Swinject + +extension ContactTrick { + struct RootView: BaseView { + let resolver: Resolver + @StateObject var state = StateModel() + + @State private var contactStore = CNContactStore() + @State private var authorization = CNContactStore.authorizationStatus(for: .contacts) + + var body: some View { + Form { + switch authorization { + case .authorized: + Section(header: Text("Contacts")) { + list + addButton + } + Section { + HStack { + if state.syncInProgress { + ProgressView().padding(.trailing, 10) + } + Button { state.save() } + label: { + Text(state.syncInProgress ? "Saving..." : "Save") + } + .disabled(state.syncInProgress || state.items.isEmpty) + } + } + + case .notDetermined: + Section { + Text( + "Need to ask for contacts access" + ) + } + Section { + Button(action: onRequestContactsAccess) { + Text("Grant access to contacts") + } + } + + case .denied: + Section { + Text( + "Contacts access denied" + ) + } + + case .restricted: + Section { + Text( + "Contacts access - restricted (parental control?)" + ) + } + + @unknown default: + Section { + Text( + "Contacts access - unknown" + ) + } + } + } + .dynamicTypeSize(...DynamicTypeSize.xxLarge) + .onAppear(perform: configureView) + .navigationTitle("Contact Trick") + .navigationBarTitleDisplayMode(.automatic) + .navigationBarItems( + trailing: EditButton() + ) + } + + private func contactSettings(for index: Int) -> some View { + EntryView(entry: $state.items[index].entry) + } + + private var list: some View { + List { + ForEach(state.items.indexed(), id: \.1.id) { index, _ in + NavigationLink(destination: contactSettings(for: index)) { + HStack { + Text( + state.items[index].entry.displayName ?? "Contact not selected" + ) + .font(.body) + .minimumScaleFactor(0.5) + .lineLimit(1) + + Spacer() + + Text( + state.items[index].entry.value.displayName + ) + .foregroundColor(.accentColor) + } + } + .moveDisabled(true) + } + .onDelete(perform: onDelete) + } + } + + private var addButton: some View { + AnyView(Button(action: onAdd) { Text("Add") }) + } + + func onAdd() { + state.add() + } + + func onRequestContactsAccess() { + contactStore.requestAccess(for: .contacts) { _, _ in + DispatchQueue.main.async { + authorization = CNContactStore.authorizationStatus(for: .contacts) + } + } + } + + private func onDelete(offsets: IndexSet) { + state.items.remove(atOffsets: offsets) + } + } + + struct EntryView: View { + @Binding var entry: ContactTrickEntry + @State private var showContactPicker = false + @State private var availableFonts: [String]? = nil + + private let fontSizes: [Int] = [70, 80, 90, 100, 110, 120, 130, 140, 150] + + var body: some View { + Form { + Section { + if let displayName = entry.displayName { + Text(displayName) + } + Button(entry.contactId == nil ? "Select contact" : "Change contact") { + showContactPicker = true + } + } + Section { + Toggle("Enabled", isOn: $entry.enabled) + Picker( + selection: $entry.value, + label: Text("Display") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } + } + } + if entry.value == .bg { + Section { + VStack { + Toggle("Trend", isOn: $entry.trend) + Toggle("Ring", isOn: $entry.ring) + } + } + } + if entry.value != .ring { + Section(header: Text("Font")) { + if entry.isDefaultFont() && availableFonts == nil { + HStack(spacing: 0) { + Button { + loadFonts() + } label: { + Text(entry.fontName) + } + } + } else { + Picker( + selection: $entry.fontName, + label: EmptyView() + ) { + ForEach(availableFonts!, id: \.self) { f in + Text(f).tag(f) + } + } + .pickerStyle(.navigationLink) + .labelsHidden() + } + HStack(spacing: 0) { + Picker( + selection: $entry.fontSize, + label: Text("Size") + ) { + ForEach(fontSizes, id: \.self) { s in + Text("\(s)").tag(s) + } + } + } + if entry.isDefaultFont() { + Picker( + selection: $entry.fontWeight, + label: Text("Weight") + ) { + ForEach(FontWeight.allCases) { w in + Text(w.displayName).tag(w) + } + } + } + } + } + Section { + Toggle("Dark mode", isOn: $entry.darkMode) + } + } +// .navigationTitle(entry.displayName ?? "Contact not selected") + .fullScreenCover(isPresented: $showContactPicker) { + ContactPicker(entry: $entry) + } + } + + private func loadFonts() { + if availableFonts != nil { + return + } + var data = [String]() + + data.append("Default Font") + UIFont.familyNames.forEach { family in + UIFont.fontNames(forFamilyName: family).forEach { font in + data.append(font) + } + } + availableFonts = data + } + } + + struct ContactPicker: UIViewControllerRepresentable { + @Binding var entry: ContactTrickEntry + + func makeUIViewController(context: Context) -> CNContactPickerViewController { + let picker = CNContactPickerViewController() + picker.delegate = context.coordinator + return picker + } + + func updateUIViewController(_: CNContactPickerViewController, context _: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, CNContactPickerDelegate { + var parent: ContactPicker + + init(_ parent: ContactPicker) { + self.parent = parent + } + + func contactPicker(_: CNContactPickerViewController, didSelect contact: CNContact) { + parent.entry.contactId = contact.identifier + let display = if let emailAddress = contact.emailAddresses.first { + "\(emailAddress.value)" + } else { + "\(contact.familyName) \(contact.givenName))" + } + if display.isEmpty { + parent.entry.displayName = "Unnamed contact" + } else { + parent.entry.displayName = display + } + } + } + } +} diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index 2037a5b33e..51082be845 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -71,7 +71,8 @@ extension Settings { Text("Middleware") .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self) Text("Notifications").navigationLink(to: .notificationsConfig, from: self) - Text("App Icons").navigationLink(to: .iconConfig, from: self) + Text("Contact trick").navigationLink(to: .contactTrick, from: self) + Text("App Icons").navigationLink(to: .iconConfig, from: self) } header: { Text("Features") } Section { diff --git a/FreeAPS/Sources/Router/Screen.swift b/FreeAPS/Sources/Router/Screen.swift index bd6f3b46b3..3c8fcba824 100644 --- a/FreeAPS/Sources/Router/Screen.swift +++ b/FreeAPS/Sources/Router/Screen.swift @@ -39,6 +39,7 @@ enum Screen: Identifiable, Hashable { case B30Conf case KetoConfig + case contactTrick var id: Int { String(reflecting: self).hashValue } } @@ -117,6 +118,8 @@ extension Screen { AIMIB30Conf.RootView(resolver: resolver) case .KetoConfig: KetoConf.RootView(resolver: resolver) + case .contactTrick: + ContactTrick.RootView(resolver: resolver) } } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift new file mode 100644 index 0000000000..2fc260d5b5 --- /dev/null +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -0,0 +1,223 @@ +import Foundation +import SwiftUI + +struct ContactPicture: View { + @Binding var contact: ContactTrickEntry + @Binding var state: ContactTrickState + + static let normalColorDark = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) + static let normalColorLight = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) + + static let notUrgentColorDark = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) + static let notUrgentColorLight = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) + + static let urgentColorDark = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) + static let urgentColorLight = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) + + static let unknownColorDark = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) + static let unknownColorLight = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) + +// static func getColor(value: String, range: BgRangeDescription, valueIsUpToDate: Bool?, darkMode: Bool) -> Color { +// if let valueIsUpToDate, valueIsUpToDate { +// return switch range { +// case .inRange: +// darkMode ? Self.normalColorDark : Self.normalColorLight +// case .notUrgent: +// darkMode ? Self.notUrgentColorDark : Self.notUrgentColorLight +// case .urgent: +// darkMode ? Self.urgentColorDark : Self.urgentColorLight +// } +// } else { +// return darkMode ? Self.unknownColorDark : Self.unknownColorLight +// } +// +// } + + static func getImage( + contact _: ContactTrickEntry, + state _: ContactTrickState + ) -> UIImage { + let width = 256.0 + let height = 256.0 + let rect = CGRect(x: 0, y: 0, width: width, height: height) + let color: Color + let string: String? + +// if value != nil && range != nil { +// color = getColor(value: value!, range: range!, valueIsUpToDate: valueIsUpToDate, darkMode: darkMode) +// string = value +// } else { +// color = darkMode ? Self.unknownColorDark : Self.unknownColorLight +// string = "—" +// } +// let textColor: Color = darkMode ? +// Color(red: 250 / 256, green: 2500 / 256, blue: 250 / 256) : +// Color(red: 20 / 256, green: 20 / 256, blue: 20 / 256) +// +// UIGraphicsBeginImageContext(rect.size) +// let context = UIGraphicsGetCurrentContext() +// +// let indicator = CGRect(x: (width - width*0.35)/2, y: height*0.15, width: width*0.35, height: height*0.10) +// +// if rangeIndicator { +// context?.setFillColor(color.cgColor!) +// let cornerRadius: CGFloat = 10.0 +// +// if let context = UIGraphicsGetCurrentContext() { +// context.beginPath() +// context.move(to: CGPoint(x: indicator.minX + cornerRadius, y: indicator.minY)) +// context.addArc(tangent1End: CGPoint(x: indicator.maxX, y: indicator.minY), tangent2End: CGPoint(x: indicator.maxX, y: indicator.maxY), radius: cornerRadius) +// context.addArc(tangent1End: CGPoint(x: indicator.maxX, y: indicator.maxY), tangent2End: CGPoint(x: indicator.minX, y: indicator.maxY), radius: cornerRadius) +// context.addArc(tangent1End: CGPoint(x: indicator.minX, y: indicator.maxY), tangent2End: CGPoint(x: indicator.minX, y: indicator.minY), radius: cornerRadius) +// context.addArc(tangent1End: CGPoint(x: indicator.minX, y: indicator.minY), tangent2End: CGPoint(x: indicator.maxX, y: indicator.minY), radius: cornerRadius) +// context.closePath() +// +// context.fillPath() +// } +// } +// +// var theFontSize = fontSize +// var font: UIFont +// +// if fontName != nil { +// font = UIFont(name: fontName!, size: CGFloat(fontSize)) ?? UIFont.systemFont(ofSize: CGFloat(fontSize), weight: fontWeight) +// } else { +// font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: fontWeight) +// } +// +// +// +// var attributes: [NSAttributedString.Key : Any] = [ +// .font : font, +// .foregroundColor : UIColor(textColor), +// .tracking : -fontSize / 17, +// ] +// let slopeAttributes: [NSAttributedString.Key : Any] = [ +// NSAttributedString.Key.font : UIFont.systemFont(ofSize: 80, weight: .regular), +// NSAttributedString.Key.foregroundColor : UIColor(textColor) +// ] +// +// +// if let string { +// var stringSize = string.size(withAttributes: attributes) +// while stringSize.width > width*0.9 { +// theFontSize = theFontSize - 10 +// attributes = [ +// .font : font, +// .foregroundColor : UIColor(textColor), +// .tracking : -fontSize / 17, +// ] +// stringSize = string.size(withAttributes: attributes) +// } +// +// string.draw( +// in: CGRectMake( +// (width - stringSize.width) / 2, +// (height - stringSize.height) / 2, +// stringSize.width, +// stringSize.height +// ), +// withAttributes: attributes +// ) +// if let slopeArrow { +// let slopeArrowSize = slopeArrow.size(withAttributes: slopeAttributes) +// slopeArrow.draw( +// in: CGRectMake( +// (width - slopeArrowSize.width) / 2, +// height - slopeArrowSize.height * 1.05, +// slopeArrowSize.width, +// slopeArrowSize.height +// ), +// withAttributes: slopeAttributes +// ) +// } +// } + let image = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return image ?? UIImage() + } + + var uiImage: UIImage { + ContactPicture.getImage(contact: contact, state: state) + } + + var body: some View { + Image(uiImage: uiImage) + .frame(width: 256, height: 256) + } +} + +struct ContactPicturePreview: View { + @Binding var contact: ContactTrickEntry + @Binding var state: ContactTrickState + + var body: some View { + ZStack { + ContactPicture(contact: $contact, state: $state) + Circle() + .stroke(lineWidth: 20) + .foregroundColor(.white) + } + .frame(width: 256, height: 256) + .clipShape(Circle()) + .preferredColorScheme($contact.wrappedValue.darkMode ? .dark : .light) + } +} + +struct ContactPicture_Previews: PreviewProvider { + struct Preview: View { + @State var rangeIndicator: Bool = true + @State var darkMode: Bool = true + @State var fontSize: Int = 130 + @State var fontWeight: UIFont.Weight = .bold + @State var fontName: String? = "AmericanTypewriter" + + var body: some View { + ContactPicturePreview( + contact: .constant(ContactTrickEntry( + value: .bg, + fontSize: 100, + fontWeight: .medium + )), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "up", + delta: "+0.2" +// glucoseDate: Date? +// glucoseDateInterval: UInt64? +// lastLoopDate: Date? +// lastLoopDateInterval: UInt64? +// bolusIncrement: Decimal? +// maxCOB: Decimal? +// maxBolus: Decimal? +// carbsRequired: Decimal? +// bolusRecommended: Decimal? +// iob: Decimal? +// cob: Decimal? +// tempTargets: [TempTargetContactPreset] = [] +// overrides: [OverrideContactPresets_] = [] +// bolusAfterCarbs: Bool? +// eventualBG: String? +// eventualBGRaw: String? +// profilesOrTempTargets: Bool? +// useNewCalc: Bool? +// isf: Decimal? +// override: String? + )) + + ).previewDisplayName("40") +// ContactPicturePreview(value: .constant("63"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("63") +// ContactPicturePreview(value: .constant("69"), slopeArrow: .constant("\u{2192}" /* → */), range: .constant(BgRangeDescription.inRange), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("69 →") +// ContactPicturePreview(value: .constant("79"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.inRange), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("79") +// ContactPicturePreview(value: .constant("11.3"), slopeArrow: .constant("\u{2198}" /* ↘ */), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("11.3 ↘") +// ContactPicturePreview(value: .constant("166"), slopeArrow: .constant("\u{2191}" /* ↑ */), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("166 ↑") +// ContactPicturePreview(value: .constant("260"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.urgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("260") +// ContactPicturePreview(value: .constant(nil), slopeArrow: .constant(nil), range: .constant(nil), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("Unknown") +// ContactPicturePreview(value: .constant("120"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(false), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("120,no real-time") + } + } + + static var previews: some View { + Preview() + } +} diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift new file mode 100644 index 0000000000..1587e540c3 --- /dev/null +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -0,0 +1,423 @@ +import Algorithms +import Combine +import Foundation +import LoopKit +import LoopKitUI +import MinimedKit +import MockKit +import OmniBLE +import OmniKit +import ShareClient +import SwiftDate +import Swinject +import UserNotifications + +protocol ContactTrickManager { + func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) +} + +private let accessLock = NSRecursiveLock(label: "BaseContactTrickManager.accessLock") + +final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { + private let processQueue = DispatchQueue(label: "BaseContactTrickManager.processQueue") + private var state = ContactTrickState() + + @Injected() private var broadcaster: Broadcaster! + @Injected() private var settingsManager: SettingsManager! + @Injected() private var apsManager: APSManager! + @Injected() private var storage: FileStorage! + @Injected() private var tempTargetsStorage: TempTargetsStorage! + + private var contacts: [ContactTrickEntry] = [] + + let coreDataStorage = CoreDataStorage() + + init(resolver: Resolver) { + super.init() + injectServices(resolver) + + contacts = storage.retrieve(OpenAPS.Settings.contactTrick, as: [ContactTrickEntry].self) + ?? [ContactTrickEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.contactTrick)) + ?? [] + + broadcaster.register(GlucoseObserver.self, observer: self) + broadcaster.register(SuggestionObserver.self, observer: self) + broadcaster.register(SettingsObserver.self, observer: self) + broadcaster.register(PumpHistoryObserver.self, observer: self) + broadcaster.register(PumpSettingsObserver.self, observer: self) + broadcaster.register(BasalProfileObserver.self, observer: self) + broadcaster.register(TempTargetsObserver.self, observer: self) + broadcaster.register(CarbsObserver.self, observer: self) + broadcaster.register(EnactedSuggestionObserver.self, observer: self) + broadcaster.register(PumpBatteryObserver.self, observer: self) + broadcaster.register(PumpReservoirObserver.self, observer: self) + + configureState() + } + + func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) { + processQueue.async { + self.contacts = contacts + completion(.success(())) + } + } + + private func configureState() { + processQueue.async { + let overrideStorage = OverrideStorage() + let readings = self.coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) + let glucoseValues = self.glucoseText(readings) + self.state.glucose = glucoseValues.glucose + self.state.trend = glucoseValues.trend + self.state.delta = glucoseValues.delta + self.state.glucoseDate = readings.first?.date ?? .distantPast + self.state.glucoseDateInterval = self.state.glucoseDate.map { + guard $0.timeIntervalSince1970 > 0 else { return 0 } + return UInt64($0.timeIntervalSince1970) + } + self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self + .apsManager.lastLoopDate + self.state.lastLoopDateInterval = self.state.lastLoopDate.map { + guard $0.timeIntervalSince1970 > 0 else { return 0 } + return UInt64($0.timeIntervalSince1970) + } + self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement + self.state.maxCOB = self.settingsManager.preferences.maxCOB + self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus + self.state.carbsRequired = self.suggestion?.carbsReq + + var insulinRequired = self.suggestion?.insulinReq ?? 0 + + var double: Decimal = 2 + if self.suggestion?.manualBolusErrorString == 0 { + insulinRequired = self.suggestion?.insulinForManualBolus ?? 0 + double = 1 + } + + self.state.useNewCalc = self.settingsManager.settings.useCalc + + if !(self.state.useNewCalc ?? false) { + self.state.bolusRecommended = self.apsManager + .roundBolus(amount: max( + insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double, + 0 + )) + } else { + let recommended = self.newBolusCalc(delta: readings, suggestion: self.suggestion) + self.state.bolusRecommended = self.apsManager + .roundBolus(amount: max(recommended, 0)) + } + + self.state.iob = self.suggestion?.iob + self.state.cob = self.suggestion?.cob + self.state.tempTargets = self.tempTargetsStorage.presets() + .map { target -> TempTargetContactPreset in + let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in + guard currentTarget.id == target.id else { return nil } + let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60)) + return date > Date() ? date : nil + } + return TempTargetContactPreset( + name: target.displayName, + id: target.id, + description: self.descriptionForTarget(target), + until: untilDate + ) + } + + self.state.overrides = overrideStorage.fetchProfiles() + .map { preset -> OverrideContactPresets_ in + let untilDate = overrideStorage.fetchLatestOverride().first.flatMap { currentOverride -> Date? in + guard currentOverride.id == preset.id, currentOverride.enabled else { return nil } + + let duration = Double(currentOverride.duration ?? 0) + let overrideDate: Date = currentOverride.date ?? Date.now + + let date = duration == 0 ? Date.distantFuture : overrideDate.addingTimeInterval(duration * 60) + return date > Date.now ? date : nil + } + + return OverrideContactPresets_( + name: preset.name ?? "", + id: preset.id ?? "", + until: untilDate, + description: self.description(preset) + ) + } + // Is there an active override but no preset? + let currentButNoOverrideNotPreset = self.state.overrides.filter({ $0.until != nil }).first + if let last = overrideStorage.fetchLatestOverride().first, last.enabled, currentButNoOverrideNotPreset == nil { + let duration = Double(last.duration ?? 0) + let overrideDate: Date = last.date ?? Date.now + let date_ = duration == 0 ? Date.distantFuture : overrideDate.addingTimeInterval(duration * 60) + let date = date_ > Date.now ? date_ : nil + + self.state.overrides + .append(OverrideContactPresets_( + name: "custom", + id: last.id ?? "", + until: date, + description: self.description(last) + )) + } + + self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs + self.state.profilesOrTempTargets = self.settingsManager.settings.profilesOrTempTargets + + let eBG = self.eventualBGString() + self.state.eventualBG = eBG.map { "⇢ " + $0 } + self.state.eventualBGRaw = eBG + + self.state.isf = self.suggestion?.isf + + let overrideArray = overrideStorage.fetchLatestOverride() + + if overrideArray.first?.enabled ?? false { + let percentString = "\((overrideArray.first?.percentage ?? 100).formatted(.number)) %" + self.state.override = percentString + } else { + self.state.override = "100 %" + } + + self.updateContacts() + } + } + + private func updateContacts() { + print("state: \(state)") + print("contacts: \(contacts)") + } + + // copy-pastes from the BaseWatchManager + private func newBolusCalc(delta: [Readings], suggestion _: Suggestion?) -> Decimal { + var conversion: Decimal = 1 + // Settings + if settingsManager.settings.units == .mmolL { + conversion = 0.0555 + } + let isf = state.isf ?? 0 + let target = suggestion?.current_target ?? 0 + let carbratio = suggestion?.carbRatio ?? 0 + let bg = delta.first?.glucose ?? 0 + let cob = state.cob ?? 0 + let iob = state.iob ?? 0 + let useFattyMealCorrectionFactor = settingsManager.settings.fattyMeals + let fattyMealFactor = settingsManager.settings.fattyMealFactor + let maxBolus = settingsManager.pumpSettings.maxBolus + var insulinCalculated: Decimal = 0 + // insulin needed for the current blood glucose + let targetDifference = (Decimal(bg) - target) * conversion + let targetDifferenceInsulin = targetDifference / isf + // more or less insulin because of bg trend in the last 15 minutes + var bgDelta: Int = 0 + if delta.count >= 3 { + bgDelta = Int((delta.first?.glucose ?? 0) - delta[2].glucose) + } + let fifteenMinInsulin = (Decimal(bgDelta) * conversion) / isf + // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB + let wholeCobInsulin = cob / carbratio + // determine how much the calculator reduces/ increases the bolus because of IOB + let iobInsulinReduction = (-1) * iob + // adding everything together + // add a calc for the case that no fifteenMinInsulin is available + var wholeCalc: Decimal = 0 + if bgDelta != 0 { + wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin) + } else { + // add (rare) case that no glucose value is available -> maybe display warning? + // if no bg is available, ?? sets its value to 0 + if bg == 0 { + wholeCalc = (iobInsulinReduction + wholeCobInsulin) + } else { + wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin) + } + } + // apply custom factor at the end of the calculations + let result = wholeCalc * settingsManager.settings.overrideFactor + // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView) + if useFattyMealCorrectionFactor { + insulinCalculated = result * fattyMealFactor + } else { + insulinCalculated = result + } + // Not 0 or over maxBolus + insulinCalculated = max(min(insulinCalculated, maxBolus), 0) + return insulinCalculated + } + + private func glucoseText(_ glucose: [Readings]) -> (glucose: String, trend: String, delta: String) { + let glucoseValue = glucose.first?.glucose ?? 0 + + guard !glucose.isEmpty else { return ("--", "--", "--") } + + let delta = glucose.count >= 2 ? glucoseValue - glucose[1].glucose : nil + + let units = settingsManager.settings.units + let glucoseText = glucoseFormatter + .string(from: Double( + units == .mmolL ? Decimal(glucoseValue).asMmolL : Decimal(glucoseValue) + ) as NSNumber)! + + let directionText = glucose.first?.direction ?? "↔︎" + let deltaText = delta + .map { + self.deltaFormatter + .string(from: Double( + units == .mmolL ? Decimal($0).asMmolL : Decimal($0) + ) as NSNumber)! + } ?? "--" + + return (glucoseText, directionText, deltaText) + } + + private func descriptionForTarget(_ target: TempTarget) -> String { + let units = settingsManager.settings.units + + var low = target.targetBottom + var high = target.targetTop + if units == .mmolL { + low = low?.asMmolL + high = high?.asMmolL + } + + let description = + "\(targetFormatter.string(from: (low ?? 0) as NSNumber)!) - \(targetFormatter.string(from: (high ?? 0) as NSNumber)!)" + + " for \(targetFormatter.string(from: target.duration as NSNumber)!) min" + + return description + } + + private func eventualBGString() -> String? { + guard let eventualBG = suggestion?.eventualBG else { + return nil + } + let units = settingsManager.settings.units + return eventualFormatter.string( + from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber + )! + } + + private func description(_ preset: OverridePresets) -> String { + let rawtarget = (preset.target ?? 0) as Decimal + + let targetValue = settingsManager.settings.units == .mmolL ? rawtarget.asMmolL : rawtarget + let target: String = rawtarget > 6 ? glucoseFormatter.string(from: targetValue as NSNumber) ?? "" : "" + + let percentage = preset.percentage != 100 ? preset.percentage.formatted() + "%" : "" + let string = (preset.target ?? 0) as Decimal > 6 && !percentage.isEmpty ? target + " " + settingsManager.settings.units + .rawValue + ", " + percentage : target + percentage + return string + } + + private func description(_ override: Override) -> String { + let rawtarget = (override.target ?? 0) as Decimal + + let targetValue = settingsManager.settings.units == .mmolL ? rawtarget.asMmolL : rawtarget + let target: String = rawtarget > 6 ? glucoseFormatter.string(from: targetValue as NSNumber) ?? "" : "" + + let percentage = override.percentage != 100 ? override.percentage.formatted() + "%" : "" + let string = (override.target ?? 0) as Decimal > 6 && !percentage.isEmpty ? target + " " + settingsManager.settings.units + .rawValue + ", " + percentage : target + percentage + return string + } + + private var glucoseFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 0 + if settingsManager.settings.units == .mmolL { + formatter.minimumFractionDigits = 1 + formatter.maximumFractionDigits = 1 + } + formatter.roundingMode = .halfUp + return formatter + } + + private var eventualFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 2 + return formatter + } + + private var deltaFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + formatter.positivePrefix = "+" + return formatter + } + + private var targetFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.maximumFractionDigits = 1 + return formatter + } + + private var suggestion: Suggestion? { + storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) + } + + private var enactedSuggestion: Suggestion? { + storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self) + } +} + +extension BaseContactTrickManager: + GlucoseObserver, + SuggestionObserver, + SettingsObserver, + PumpHistoryObserver, + PumpSettingsObserver, + BasalProfileObserver, + TempTargetsObserver, + CarbsObserver, + EnactedSuggestionObserver, + PumpBatteryObserver, + PumpReservoirObserver +{ + func glucoseDidUpdate(_: [BloodGlucose]) { + configureState() + } + + func suggestionDidUpdate(_: Suggestion) { + configureState() + } + + func settingsDidChange(_: FreeAPSSettings) { + configureState() + } + + func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) { + // TODO: + } + + func pumpSettingsDidChange(_: PumpSettings) { + configureState() + } + + func basalProfileDidChange(_: [BasalProfileEntry]) { + // TODO: + } + + func tempTargetsDidUpdate(_: [TempTarget]) { + configureState() + } + + func carbsDidUpdate(_: [CarbsEntry]) { + // TODO: + } + + func enactedSuggestionDidUpdate(_: Suggestion) { + configureState() + } + + func pumpBatteryDidChange(_: Battery) { + // TODO: + } + + func pumpReservoirDidChange(_: Decimal) { + // TODO: + } +} diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift new file mode 100644 index 0000000000..b5bdb9be8a --- /dev/null +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift @@ -0,0 +1,41 @@ +import Foundation + +struct ContactTrickState: Codable { + var glucose: String? + var trend: String? + var delta: String? + var glucoseDate: Date? + var glucoseDateInterval: UInt64? + var lastLoopDate: Date? + var lastLoopDateInterval: UInt64? + var bolusIncrement: Decimal? + var maxCOB: Decimal? + var maxBolus: Decimal? + var carbsRequired: Decimal? + var bolusRecommended: Decimal? + var iob: Decimal? + var cob: Decimal? + var tempTargets: [TempTargetContactPreset] = [] + var overrides: [OverrideContactPresets_] = [] + var bolusAfterCarbs: Bool? + var eventualBG: String? + var eventualBGRaw: String? + var profilesOrTempTargets: Bool? + var useNewCalc: Bool? + var isf: Decimal? + var override: String? +} + +struct TempTargetContactPreset: Codable, Identifiable { + let name: String + let id: String + let description: String + let until: Date? +} + +struct OverrideContactPresets_: Codable, Identifiable { + let name: String + let id: String + let until: Date? + let description: String +} From 2142436202f06f2c2754b627692f5a7bca1305b1 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Wed, 20 Mar 2024 05:02:32 +0100 Subject: [PATCH 02/14] contact trick continued --- .../Sources/Models/ContactTrickEntry.swift | 32 +- .../ContactTrick/ContactTrickStateModel.swift | 52 +- .../View/ContactTrickRootView.swift | 144 +++- .../ContactTrick/ContactPicture.swift | 777 ++++++++++++++---- .../ContactTrick/ContactTrickManager.swift | 101 ++- .../ContactTrick/ContactTrickState.swift | 15 +- 6 files changed, 827 insertions(+), 294 deletions(-) diff --git a/FreeAPS/Sources/Models/ContactTrickEntry.swift b/FreeAPS/Sources/Models/ContactTrickEntry.swift index f67fb4e257..dbeae70048 100644 --- a/FreeAPS/Sources/Models/ContactTrickEntry.swift +++ b/FreeAPS/Sources/Models/ContactTrickEntry.swift @@ -1,13 +1,14 @@ struct ContactTrickEntry: JSON, Equatable { var enabled: Bool = false - var layout: ContactTrickLayout - var primary: ContactTrickValue = .bg - var secondary: ContactTrickValue? = .trend + var layout: ContactTrickLayout = .single + var ring1: ContactTrickLargeRing = .none + var ring2: ContactTrickLargeRing = .none + var primary: ContactTrickValue = .glucose + var top: ContactTrickValue = .none + var bottom: ContactTrickValue = .none var contactId: String? = nil var displayName: String? = nil - var trend: Bool = false - var ring: Bool = false var darkMode: Bool = true var fontSize: Int = 100 var fontName: String = "Default Font" @@ -26,12 +27,13 @@ extension ContactTrickEntry { private enum CodingKeys: String, CodingKey { case enabled case layout + case ring1 + case ring2 case primary - case secondary + case top + case bottom case contactId case displayName - case trend - case ring case darkMode case fontSize case fontName @@ -42,12 +44,13 @@ extension ContactTrickEntry { let container = try decoder.container(keyedBy: CodingKeys.self) let enabled = try container.decode(Bool.self, forKey: .enabled) let layout = try container.decode(ContactTrickLayout.self, forKey: .layout) + let ring1 = try container.decode(ContactTrickLargeRing.self, forKey: .ring1) + let ring2 = try container.decode(ContactTrickLargeRing.self, forKey: .ring2) let primary = try container.decode(ContactTrickValue.self, forKey: .primary) - let secondary = try container.decodeIfPresent(ContactTrickValue.self, forKey: .secondary) + let top = try container.decode(ContactTrickValue.self, forKey: .top) + let bottom = try container.decode(ContactTrickValue.self, forKey: .bottom) let contactId = try container.decodeIfPresent(String.self, forKey: .contactId) let displayName = try container.decodeIfPresent(String.self, forKey: .displayName) - let trend = try container.decode(Bool.self, forKey: .trend) - let ring = try container.decode(Bool.self, forKey: .ring) let darkMode = try container.decode(Bool.self, forKey: .darkMode) let fontSize = try container.decode(Int.self, forKey: .fontSize) let fontName = try container.decodeIfPresent(String.self, forKey: .fontName) ?? "Default Font" @@ -56,12 +59,13 @@ extension ContactTrickEntry { self = ContactTrickEntry( enabled: enabled, layout: layout, + ring1: ring1, + ring2: ring2, primary: primary, - secondary: secondary, + top: top, + bottom: bottom, contactId: contactId, displayName: displayName, - trend: trend, - ring: ring, darkMode: darkMode, fontSize: fontSize, fontName: fontName, diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift index cbb2e76970..3c220e1b24 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift @@ -3,30 +3,45 @@ import SwiftUI enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { var id: String { rawValue } - case bg + case none + case glucose + case eventualBG case delta case trend - case time + case glucoseDate + case lastLoopDate case cob case iob + case bolusRecommended + case carbsRequired case isf case override case ring var displayName: String { switch self { - case .bg: - return NSLocalizedString("BG", comment: "") + case .none: + return NSLocalizedString("None", comment: "") + case .glucose: + return NSLocalizedString("Glucose", comment: "") + case .eventualBG: + return NSLocalizedString("Eventual BG", comment: "") case .delta: return NSLocalizedString("Delta", comment: "") case .trend: return NSLocalizedString("Trend", comment: "") - case .time: - return NSLocalizedString("Time", comment: "") + case .glucoseDate: + return NSLocalizedString("Glucose date", comment: "") + case .lastLoopDate: + return NSLocalizedString("Last loop date", comment: "") case .cob: return NSLocalizedString("COB", comment: "") case .iob: return NSLocalizedString("IOB", comment: "") + case .bolusRecommended: + return NSLocalizedString("Bolus recommended", comment: "") + case .carbsRequired: + return NSLocalizedString("Carbs required", comment: "") case .isf: return NSLocalizedString("ISF", comment: "") case .override: @@ -41,7 +56,6 @@ enum ContactTrickLayout: String, JSON, CaseIterable, Identifiable, Codable { var id: String { rawValue } case single case split - case ring var displayName: String { switch self { @@ -49,8 +63,24 @@ enum ContactTrickLayout: String, JSON, CaseIterable, Identifiable, Codable { return NSLocalizedString("Single", comment: "") case .split: return NSLocalizedString("Split", comment: "") - case .ring: - return NSLocalizedString("Ring", comment: "") + } + } +} + +enum ContactTrickLargeRing: String, JSON, CaseIterable, Identifiable, Codable { + var id: String { rawValue } + case none + case loop + case iob + + var displayName: String { + switch self { + case .none: + return NSLocalizedString("Don't show", comment: "") + case .loop: + return NSLocalizedString("Loop status", comment: "") + case .iob: + return NSLocalizedString("IOB", comment: "") } } } @@ -74,11 +104,9 @@ extension ContactTrick { index: items.count, entry: ContactTrickEntry( enabled: false, - value: .bg, + layout: .single, contactId: nil, displayName: nil, - trend: false, - ring: false, darkMode: true, fontSize: 100, fontName: "Default Font", diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift index 1facd6f710..81d7fd930a 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -94,7 +94,7 @@ extension ContactTrick { Spacer() Text( - state.items[index].entry.value.displayName + state.items[index].entry.primary.displayName ) .foregroundColor(.accentColor) } @@ -146,62 +146,118 @@ extension ContactTrick { Section { Toggle("Enabled", isOn: $entry.enabled) Picker( - selection: $entry.value, - label: Text("Display") + selection: $entry.layout, + label: Text("Layout") ) { - ForEach(ContactTrickValue.allCases) { v in + ForEach(ContactTrickLayout.allCases) { v in Text(v.displayName).tag(v) } } } - if entry.value == .bg { - Section { - VStack { - Toggle("Trend", isOn: $entry.trend) - Toggle("Ring", isOn: $entry.ring) + Section { + switch entry.layout { + case .single: + Picker( + selection: $entry.primary, + label: Text("Primary") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } + } + Picker( + selection: $entry.top, + label: Text("Top") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } + } + Picker( + selection: $entry.bottom, + label: Text("Bottom") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } + } + case .split: + Picker( + selection: $entry.top, + label: Text("Top") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } + } + Picker( + selection: $entry.bottom, + label: Text("Bottom") + ) { + ForEach(ContactTrickValue.allCases) { v in + Text(v.displayName).tag(v) + } } } } - if entry.value != .ring { - Section(header: Text("Font")) { - if entry.isDefaultFont() && availableFonts == nil { - HStack(spacing: 0) { - Button { - loadFonts() - } label: { - Text(entry.fontName) - } + + Section(header: Text("Rings")) { + Picker( + selection: $entry.ring1, + label: Text("Outer") + ) { + ForEach(ContactTrickLargeRing.allCases) { v in + Text(v.displayName).tag(v) + } + } + Picker( + selection: $entry.ring2, + label: Text("Inner") + ) { + ForEach(ContactTrickLargeRing.allCases) { v in + Text(v.displayName).tag(v) + } + } + } + + Section(header: Text("Font")) { + if entry.isDefaultFont() && availableFonts == nil { + HStack(spacing: 0) { + Button { + loadFonts() + } label: { + Text(entry.fontName) } - } else { - Picker( - selection: $entry.fontName, - label: EmptyView() - ) { - ForEach(availableFonts!, id: \.self) { f in - Text(f).tag(f) - } + } + } else { + Picker( + selection: $entry.fontName, + label: EmptyView() + ) { + ForEach(availableFonts!, id: \.self) { f in + Text(f).tag(f) } - .pickerStyle(.navigationLink) - .labelsHidden() } - HStack(spacing: 0) { - Picker( - selection: $entry.fontSize, - label: Text("Size") - ) { - ForEach(fontSizes, id: \.self) { s in - Text("\(s)").tag(s) - } + .pickerStyle(.navigationLink) + .labelsHidden() + } + HStack(spacing: 0) { + Picker( + selection: $entry.fontSize, + label: Text("Size") + ) { + ForEach(fontSizes, id: \.self) { s in + Text("\(s)").tag(s) } } - if entry.isDefaultFont() { - Picker( - selection: $entry.fontWeight, - label: Text("Weight") - ) { - ForEach(FontWeight.allCases) { w in - Text(w.displayName).tag(w) - } + } + if entry.isDefaultFont() { + Picker( + selection: $entry.fontWeight, + label: Text("Weight") + ) { + ForEach(FontWeight.allCases) { w in + Text(w.displayName).tag(w) } } } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index 2fc260d5b5..3750aaffaa 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -2,141 +2,456 @@ import Foundation import SwiftUI struct ContactPicture: View { + // copy paste from watch app MainView.swift + private enum Config { + static let lag: TimeInterval = 30 + } + @Binding var contact: ContactTrickEntry @Binding var state: ContactTrickState - static let normalColorDark = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) - static let normalColorLight = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) - - static let notUrgentColorDark = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) - static let notUrgentColorLight = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) - - static let urgentColorDark = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) - static let urgentColorLight = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) - - static let unknownColorDark = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) - static let unknownColorLight = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) - -// static func getColor(value: String, range: BgRangeDescription, valueIsUpToDate: Bool?, darkMode: Bool) -> Color { -// if let valueIsUpToDate, valueIsUpToDate { -// return switch range { -// case .inRange: -// darkMode ? Self.normalColorDark : Self.normalColorLight -// case .notUrgent: -// darkMode ? Self.notUrgentColorDark : Self.notUrgentColorLight -// case .urgent: -// darkMode ? Self.urgentColorDark : Self.urgentColorLight -// } -// } else { -// return darkMode ? Self.unknownColorDark : Self.unknownColorLight -// } -// -// } + private static let normalColorDark = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) + private static let normalColorLight = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) + + private static let notUrgentColorDark = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) + private static let notUrgentColorLight = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) + + private static let urgentColorDark = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) + private static let urgentColorLight = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) + + private static let unknownColorDark = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) + private static let unknownColorLight = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) + + private static let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + return formatter + }() + + private static let ringWidth = 0.05 // percent + private static let ringGap = 0.02 // percent static func getImage( - contact _: ContactTrickEntry, - state _: ContactTrickState + contact: ContactTrickEntry, + state: ContactTrickState ) -> UIImage { let width = 256.0 let height = 256.0 - let rect = CGRect(x: 0, y: 0, width: width, height: height) - let color: Color - let string: String? - -// if value != nil && range != nil { -// color = getColor(value: value!, range: range!, valueIsUpToDate: valueIsUpToDate, darkMode: darkMode) -// string = value -// } else { -// color = darkMode ? Self.unknownColorDark : Self.unknownColorLight -// string = "—" -// } -// let textColor: Color = darkMode ? -// Color(red: 250 / 256, green: 2500 / 256, blue: 250 / 256) : -// Color(red: 20 / 256, green: 20 / 256, blue: 20 / 256) -// -// UIGraphicsBeginImageContext(rect.size) -// let context = UIGraphicsGetCurrentContext() -// -// let indicator = CGRect(x: (width - width*0.35)/2, y: height*0.15, width: width*0.35, height: height*0.10) -// -// if rangeIndicator { -// context?.setFillColor(color.cgColor!) -// let cornerRadius: CGFloat = 10.0 -// -// if let context = UIGraphicsGetCurrentContext() { -// context.beginPath() -// context.move(to: CGPoint(x: indicator.minX + cornerRadius, y: indicator.minY)) -// context.addArc(tangent1End: CGPoint(x: indicator.maxX, y: indicator.minY), tangent2End: CGPoint(x: indicator.maxX, y: indicator.maxY), radius: cornerRadius) -// context.addArc(tangent1End: CGPoint(x: indicator.maxX, y: indicator.maxY), tangent2End: CGPoint(x: indicator.minX, y: indicator.maxY), radius: cornerRadius) -// context.addArc(tangent1End: CGPoint(x: indicator.minX, y: indicator.maxY), tangent2End: CGPoint(x: indicator.minX, y: indicator.minY), radius: cornerRadius) -// context.addArc(tangent1End: CGPoint(x: indicator.minX, y: indicator.minY), tangent2End: CGPoint(x: indicator.maxX, y: indicator.minY), radius: cornerRadius) -// context.closePath() -// -// context.fillPath() -// } -// } -// -// var theFontSize = fontSize -// var font: UIFont -// -// if fontName != nil { -// font = UIFont(name: fontName!, size: CGFloat(fontSize)) ?? UIFont.systemFont(ofSize: CGFloat(fontSize), weight: fontWeight) -// } else { -// font = UIFont.systemFont(ofSize: CGFloat(fontSize), weight: fontWeight) -// } -// -// -// -// var attributes: [NSAttributedString.Key : Any] = [ -// .font : font, -// .foregroundColor : UIColor(textColor), -// .tracking : -fontSize / 17, -// ] -// let slopeAttributes: [NSAttributedString.Key : Any] = [ -// NSAttributedString.Key.font : UIFont.systemFont(ofSize: 80, weight: .regular), -// NSAttributedString.Key.foregroundColor : UIColor(textColor) -// ] -// -// -// if let string { -// var stringSize = string.size(withAttributes: attributes) -// while stringSize.width > width*0.9 { -// theFontSize = theFontSize - 10 -// attributes = [ -// .font : font, -// .foregroundColor : UIColor(textColor), -// .tracking : -fontSize / 17, -// ] -// stringSize = string.size(withAttributes: attributes) -// } -// -// string.draw( -// in: CGRectMake( -// (width - stringSize.width) / 2, -// (height - stringSize.height) / 2, -// stringSize.width, -// stringSize.height -// ), -// withAttributes: attributes -// ) -// if let slopeArrow { -// let slopeArrowSize = slopeArrow.size(withAttributes: slopeAttributes) -// slopeArrow.draw( -// in: CGRectMake( -// (width - slopeArrowSize.width) / 2, -// height - slopeArrowSize.height * 1.05, -// slopeArrowSize.width, -// slopeArrowSize.height -// ), -// withAttributes: slopeAttributes -// ) -// } -// } + var rect = CGRect(x: 0, y: 0, width: width, height: height) + let textColor: Color = contact.darkMode ? + Color(red: 250 / 256, green: 250 / 256, blue: 250 / 256) : + Color(red: 20 / 256, green: 20 / 256, blue: 20 / 256) + let secondaryTextColor: Color = contact.darkMode ? + Color(red: 200 / 256, green: 200 / 256, blue: 200 / 256) : + Color(red: 60 / 256, green: 60 / 256, blue: 60 / 256) + let fontWeight = contact.fontWeight.toUI() + + UIGraphicsBeginImageContext(rect.size) + + if contact.ring1 != .none { + // offset from the white ring + rect = CGRect( + x: rect.minX + width * ringGap * 2, + y: rect.minY + height * ringGap * 2, + width: rect.width - width * ringGap * 4, + height: rect.height - width * ringGap * 4 + ) + + let ringRect = CGRect( + x: rect.minX + width * ringGap, + y: rect.minY + height * ringGap, + width: rect.width - width * ringGap * 2, + height: rect.height - width * ringGap * 2 + ) + drawRing(ring: contact.ring1, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth) + } + + if contact.ring1 != .none || contact.ring2 != .none { + rect = CGRect( + x: rect.minX + width * (ringWidth + ringGap), + y: rect.minY + height * (ringWidth + ringGap), + width: rect.width - width * (ringWidth + ringGap) * 2, + height: rect.height - height * (ringWidth + ringGap) * 2 + ) + } + + if contact.ring2 != .none { + let ringRect = CGRect( + x: rect.minX + width * ringGap, + y: rect.minY + height * ringGap, + width: rect.width - width * ringGap * 2, + height: rect.height - width * ringGap * 2 + ) + drawRing(ring: contact.ring2, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth) + } + + if contact.ring2 != .none { + rect = CGRect( + x: rect.minX + width * (ringWidth + ringGap), + y: rect.minY + height * (ringWidth + ringGap), + width: rect.width - width * (ringWidth + ringGap) * 2, + height: rect.height - height * (ringWidth + ringGap) * 2 + ) + } + + switch contact.layout { + case .single: + let showTop = contact.top != .none + let showBottom = contact.bottom != .none + let primaryRect = (showTop || showBottom) ? CGRect( + x: rect.minX, + y: rect.minY + rect.height * 0.30, + width: rect.width, + height: rect.height * 0.40 + ) : rect + let topRect = CGRect(x: rect.minX, y: rect.minY + rect.height * 0.10, width: rect.width, height: rect.height * 0.20) + let bottomRect = CGRect( + x: rect.minX, + y: rect.minY + rect.height * 0.70, + width: rect.width, + height: rect.height * 0.20 + ) + let secondaryFontSize = Int(Double(contact.fontSize) * 0.80) + + displayPiece( + value: contact.primary, + contact: contact, + state: state, + rect: primaryRect, + fontName: contact.fontName, + fontSize: contact.fontSize, + fontWeight: fontWeight, + color: textColor + ) + if showTop { + displayPiece( + value: contact.top, + contact: contact, + state: state, + rect: topRect, + fontName: contact.fontName, + fontSize: secondaryFontSize, + fontWeight: fontWeight, + color: secondaryTextColor + ) + } + if showBottom { + displayPiece( + value: contact.bottom, + contact: contact, + state: state, + rect: bottomRect, + fontName: contact.fontName, + fontSize: secondaryFontSize, + fontWeight: fontWeight, + color: secondaryTextColor + ) + } + + case .split: + let topRect = CGRect(x: rect.minX, y: rect.minY + height * 0.20, width: rect.width, height: rect.height * 0.30) + let bottomRect = CGRect(x: rect.minX, y: rect.minY + height * 0.50, width: rect.width, height: rect.height * 0.30) + let splitFontSize = Int(Double(contact.fontSize) * 0.80) + + displayPiece( + value: contact.top, + contact: contact, + state: state, + rect: topRect, + fontName: contact.fontName, + fontSize: splitFontSize, + fontWeight: fontWeight, + color: textColor + ) + displayPiece( + value: contact.bottom, + contact: contact, + state: state, + rect: bottomRect, + fontName: contact.fontName, + fontSize: splitFontSize, + fontWeight: fontWeight, + color: textColor + ) + } let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image ?? UIImage() } + private static func displayPiece( + value: ContactTrickValue, + contact: ContactTrickEntry, + state: ContactTrickState, + rect: CGRect, + fontName: String?, + fontSize: Int, + fontWeight: UIFont.Weight, + color: Color + ) { + switch value { + case .none: + break + case .glucose: + drawText( + text: state.glucose, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .eventualBG: + drawText( + text: state.eventualBG, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .delta: + drawText( + text: state.delta, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .trend: + drawText( + text: state.trend, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .glucoseDate: + drawText( + text: state.glucoseDate.map { formatter.string(from: $0) }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .lastLoopDate: + drawText( + text: state.lastLoopDate.map { formatter.string(from: $0) }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .cob: + drawText( + text: state.cob.map { $0.formatted() }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .iob: + drawText( + text: state.iob.map { $0.formatted() }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .bolusRecommended: + drawText( + text: state.bolusRecommended.map { $0.formatted() }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .carbsRequired: + drawText( + text: state.carbsRequired.map { $0.formatted() }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .isf: + drawText( + text: state.isf.map { $0.formatted() }, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + case .override: + drawText( + text: state.override, + rect: rect, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + color: color + ) + + case .ring: + drawRing(ring: .loop, contact: contact, state: state, rect: rect, strokeWidth: rect.width * ringWidth) + } + } + + private static func drawText( + text: String?, + rect: CGRect, + fontName: String?, + fontSize: Int, + fontWeight: UIFont.Weight, + color: Color + ) { + var theFontSize = fontSize + + func makeAttributes(size: Int) -> [NSAttributedString.Key: Any] { + let font = if let fontName { + UIFont(name: fontName, size: CGFloat(size)) ?? UIFont.systemFont(ofSize: CGFloat(size)) + } else { + UIFont.systemFont(ofSize: CGFloat(size), weight: fontWeight) + } + return [ + .font: font, + .foregroundColor: UIColor(color) + ] + } + + var attributes: [NSAttributedString.Key: Any] = makeAttributes(size: theFontSize) + + if let text { + var stringSize = text.size(withAttributes: attributes) + while stringSize.width > rect.width * 0.85 || stringSize.height > rect.height * 0.95, theFontSize > 30 { + theFontSize = theFontSize - 10 + attributes = makeAttributes(size: theFontSize) + stringSize = text.size(withAttributes: attributes) + } + + text.draw( + in: CGRectMake( + rect.minX + (rect.width - stringSize.width) / 2, + rect.minY + (rect.height - stringSize.height) / 2, + rect.minX + stringSize.width, + rect.minY + stringSize.height + ), + withAttributes: attributes + ) + } + } + + private static func drawRing( + ring: ContactTrickLargeRing, + contact: ContactTrickEntry, + state: ContactTrickState, + rect: CGRect, + strokeWidth: Double + ) { + guard let context = UIGraphicsGetCurrentContext() else { + print("no context") + return + } + switch ring { + case .loop: + let color = ringColor(contact: contact, state: state) + + let strokeWidth = strokeWidth + let center = CGPoint(x: rect.midX, y: rect.midY) + let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2 + + context.setLineWidth(strokeWidth) + context.setStrokeColor(UIColor(color).cgColor) + + context.addArc(center: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: false) + + context.strokePath() + case .iob: + if let iob = state.iob { + drawProgressBar( + rect: rect, + progress: Double(iob) / Double(state.maxIOB), + colors: [.blue, .red], + strokeWidth: strokeWidth + ) + } + default: + break + } + } + + private static func drawProgressBar( + rect: CGRect, + progress: Double, + colors: [Color], + strokeWidth: Double + ) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + let center = CGPoint(x: rect.midX, y: rect.midY) + let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2 + let lineWidth: CGFloat = strokeWidth + let startAngle: CGFloat = -(.pi + .pi / 4.0) + let endAngle: CGFloat = .pi / 4.0 + let progressAngle = startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0) + + let colors = colors.map { c in UIColor(c).cgColor } + let locations: [CGFloat] = [0.0, 1.0] + guard let gradient = CGGradient( + colorsSpace: CGColorSpaceCreateDeviceRGB(), + colors: colors as CFArray, + locations: locations + ) else { + return + } + + context.saveGState() + context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: progressAngle, clockwise: false) + context.setLineWidth(lineWidth) + context.setLineCap(.round) + let segmentPath = context.path! + context.strokePath() + context.saveGState() + context.addPath(segmentPath) + context.replacePathWithStrokedPath() + context.clip() + context.drawLinearGradient( + gradient, + start: CGPoint(x: rect.minX, y: rect.midY), + end: CGPoint(x: rect.maxX, y: rect.midY), + options: [] + ) + context.restoreGState() + } + + private static func ringColor( + contact _: ContactTrickEntry, + state: ContactTrickState + ) -> Color { + guard let lastLoopDate = state.lastLoopDate else { + return .loopGray + } + let delta = Date().timeIntervalSince(lastLoopDate) - Config.lag + + if delta <= 5.minutes.timeInterval { + return .loopGreen + } else if delta <= 10.minutes.timeInterval { + return .loopYellow + } else { + return .loopRed + } + } + var uiImage: UIImage { ContactPicture.getImage(contact: contact, state: state) } @@ -147,6 +462,25 @@ struct ContactPicture: View { } } +extension FontWeight { + func toUI() -> UIFont.Weight { + switch self { + case .light: + UIFont.Weight.light + case .regular: + UIFont.Weight.regular + case .medium: + UIFont.Weight.medium + case .semibold: + UIFont.Weight.semibold + case .bold: + UIFont.Weight.bold + case .black: + UIFont.Weight.black + } + } +} + struct ContactPicturePreview: View { @Binding var contact: ContactTrickEntry @Binding var state: ContactTrickState @@ -174,46 +508,173 @@ struct ContactPicture_Previews: PreviewProvider { var body: some View { ContactPicturePreview( - contact: .constant(ContactTrickEntry( - value: .bg, - fontSize: 100, - fontWeight: .medium - )), + contact: .constant( + ContactTrickEntry( + primary: .glucose, + top: .delta, + bottom: .trend, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + delta: "+0.2", + cob: 25 + )) + + ).previewDisplayName("bg + trend + delta") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + primary: .glucose, + top: .ring, + bottom: .trend, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + lastLoopDate: .now + )) + + ).previewDisplayName("bg + trend + ring") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .loop, + primary: .glucose, + top: .none, + bottom: .trend, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + lastLoopDate: .now + )) + + ).previewDisplayName("bg + trend + ring1") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .loop, + primary: .glucose, + top: .none, + bottom: .eventualBG, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + lastLoopDate: .now - 7.minutes, + eventualBG: "⇢ 6.2" + )) + + ).previewDisplayName("bg + eventual + ring1") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .loop, + primary: .glucoseDate, + top: .none, + bottom: .none, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + glucoseDate: .now - 3.minutes, + lastLoopDate: .now + )) + + ).previewDisplayName("glucoseDate + ring1") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .loop, + primary: .lastLoopDate, + top: .none, + bottom: .none, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + lastLoopDate: .now - 2.minutes + )) + + ).previewDisplayName("lastLoopDate + ring1") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .loop, + ring2: .iob, + primary: .glucose, + top: .none, + bottom: .none, + fontSize: 100, + fontWeight: .medium + ) + ), state: .constant(ContactTrickState( glucose: "6.8", - trend: "up", - delta: "+0.2" -// glucoseDate: Date? -// glucoseDateInterval: UInt64? -// lastLoopDate: Date? -// lastLoopDateInterval: UInt64? -// bolusIncrement: Decimal? -// maxCOB: Decimal? -// maxBolus: Decimal? -// carbsRequired: Decimal? -// bolusRecommended: Decimal? -// iob: Decimal? -// cob: Decimal? -// tempTargets: [TempTargetContactPreset] = [] -// overrides: [OverrideContactPresets_] = [] -// bolusAfterCarbs: Bool? -// eventualBG: String? -// eventualBGRaw: String? -// profilesOrTempTargets: Bool? -// useNewCalc: Bool? -// isf: Decimal? -// override: String? + lastLoopDate: .now, + iob: 6.1, + maxIOB: 8.0 + )) + + ).previewDisplayName("bg + ring1 + ring2") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + layout: .split, + top: .iob, + bottom: .cob, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + iob: 1.5, + cob: 25 + )) + + ).previewDisplayName("iob + cob") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + layout: .split, + top: .override, + bottom: .iob, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + iob: 1.5, + override: "75 %" )) - ).previewDisplayName("40") -// ContactPicturePreview(value: .constant("63"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("63") -// ContactPicturePreview(value: .constant("69"), slopeArrow: .constant("\u{2192}" /* → */), range: .constant(BgRangeDescription.inRange), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("69 →") -// ContactPicturePreview(value: .constant("79"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.inRange), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("79") -// ContactPicturePreview(value: .constant("11.3"), slopeArrow: .constant("\u{2198}" /* ↘ */), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("11.3 ↘") -// ContactPicturePreview(value: .constant("166"), slopeArrow: .constant("\u{2191}" /* ↑ */), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("166 ↑") -// ContactPicturePreview(value: .constant("260"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.urgent), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("260") -// ContactPicturePreview(value: .constant(nil), slopeArrow: .constant(nil), range: .constant(nil), valueIsUpToDate: .constant(true), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("Unknown") -// ContactPicturePreview(value: .constant("120"), slopeArrow: .constant(nil), range: .constant(BgRangeDescription.notUrgent), valueIsUpToDate: .constant(false), rangeIndicator: $rangeIndicator, darkMode: $darkMode, fontSize: $fontSize, fontWeight: $fontWeight, fontName: $fontName).previewDisplayName("120,no real-time") + ).previewDisplayName("overrides + iob") } } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 1587e540c3..4aae17a1cb 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -1,5 +1,6 @@ import Algorithms import Combine +import Contacts import Foundation import LoopKit import LoopKitUI @@ -21,6 +22,8 @@ private let accessLock = NSRecursiveLock(label: "BaseContactTrickManager.accessL final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private let processQueue = DispatchQueue(label: "BaseContactTrickManager.processQueue") private var state = ContactTrickState() + private let contactStore = CNContactStore() + private var workItem: DispatchWorkItem? @Injected() private var broadcaster: Broadcaster! @Injected() private var settingsManager: SettingsManager! @@ -56,8 +59,10 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) { + print("update contacts: \(contacts)") processQueue.async { self.contacts = contacts + self.renderContacts() completion(.success(())) } } @@ -71,19 +76,8 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { self.state.trend = glucoseValues.trend self.state.delta = glucoseValues.delta self.state.glucoseDate = readings.first?.date ?? .distantPast - self.state.glucoseDateInterval = self.state.glucoseDate.map { - guard $0.timeIntervalSince1970 > 0 else { return 0 } - return UInt64($0.timeIntervalSince1970) - } self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self .apsManager.lastLoopDate - self.state.lastLoopDateInterval = self.state.lastLoopDate.map { - guard $0.timeIntervalSince1970 > 0 else { return 0 } - return UInt64($0.timeIntervalSince1970) - } - self.state.bolusIncrement = self.settingsManager.preferences.bolusIncrement - self.state.maxCOB = self.settingsManager.preferences.maxCOB - self.state.maxBolus = self.settingsManager.pumpSettings.maxBolus self.state.carbsRequired = self.suggestion?.carbsReq var insulinRequired = self.suggestion?.insulinReq ?? 0 @@ -109,6 +103,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } self.state.iob = self.suggestion?.iob + self.state.maxIOB = self.settingsManager.preferences.maxIOB self.state.cob = self.suggestion?.cob self.state.tempTargets = self.tempTargetsStorage.presets() .map { target -> TempTargetContactPreset in @@ -125,43 +120,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { ) } - self.state.overrides = overrideStorage.fetchProfiles() - .map { preset -> OverrideContactPresets_ in - let untilDate = overrideStorage.fetchLatestOverride().first.flatMap { currentOverride -> Date? in - guard currentOverride.id == preset.id, currentOverride.enabled else { return nil } - - let duration = Double(currentOverride.duration ?? 0) - let overrideDate: Date = currentOverride.date ?? Date.now - - let date = duration == 0 ? Date.distantFuture : overrideDate.addingTimeInterval(duration * 60) - return date > Date.now ? date : nil - } - - return OverrideContactPresets_( - name: preset.name ?? "", - id: preset.id ?? "", - until: untilDate, - description: self.description(preset) - ) - } - // Is there an active override but no preset? - let currentButNoOverrideNotPreset = self.state.overrides.filter({ $0.until != nil }).first - if let last = overrideStorage.fetchLatestOverride().first, last.enabled, currentButNoOverrideNotPreset == nil { - let duration = Double(last.duration ?? 0) - let overrideDate: Date = last.date ?? Date.now - let date_ = duration == 0 ? Date.distantFuture : overrideDate.addingTimeInterval(duration * 60) - let date = date_ > Date.now ? date_ : nil - - self.state.overrides - .append(OverrideContactPresets_( - name: "custom", - id: last.id ?? "", - until: date, - description: self.description(last) - )) - } - - self.state.bolusAfterCarbs = !self.settingsManager.settings.skipBolusScreenAfterCarbs self.state.profilesOrTempTargets = self.settingsManager.settings.profilesOrTempTargets let eBG = self.eventualBGString() @@ -179,13 +137,52 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { self.state.override = "100 %" } - self.updateContacts() + self.renderContacts() } } - private func updateContacts() { - print("state: \(state)") - print("contacts: \(contacts)") + private func renderContacts() { + print("render contacts") + contacts.forEach { renderContact($0) } + workItem = DispatchWorkItem(block: { + print("in updateContact, no updates received for more than 5 minutes") + self.renderContacts() + }) + DispatchQueue.main.asyncAfter(deadline: .now() + 5 * 60 + 15, execute: workItem!) + } + + private func renderContact(_ entry: ContactTrickEntry) { + print("render contact: \(entry)") + guard let contactId = entry.contactId else { + return + } + + let keysToFetch = [CNContactImageDataKey] as [CNKeyDescriptor] + + let contact: CNContact + do { + contact = try contactStore.unifiedContact(withIdentifier: contactId, keysToFetch: keysToFetch) + } catch { + print("in updateContact, an error has been thrown while fetching the selected contact") + return + } + + guard let mutableContact = contact.mutableCopy() as? CNMutableContact else { + return + } + + mutableContact.imageData = ContactPicture.getImage( + contact: entry, + state: state + ).pngData() + + let saveRequest = CNSaveRequest() + saveRequest.update(mutableContact) + do { + try contactStore.execute(saveRequest) + } catch { + print("in updateContact, failed to update the contact") + } } // copy-pastes from the BaseWatchManager diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift index b5bdb9be8a..a7ab8d1cab 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift @@ -5,19 +5,13 @@ struct ContactTrickState: Codable { var trend: String? var delta: String? var glucoseDate: Date? - var glucoseDateInterval: UInt64? var lastLoopDate: Date? - var lastLoopDateInterval: UInt64? - var bolusIncrement: Decimal? - var maxCOB: Decimal? - var maxBolus: Decimal? var carbsRequired: Decimal? var bolusRecommended: Decimal? var iob: Decimal? + var maxIOB: Decimal = 0.0 var cob: Decimal? var tempTargets: [TempTargetContactPreset] = [] - var overrides: [OverrideContactPresets_] = [] - var bolusAfterCarbs: Bool? var eventualBG: String? var eventualBGRaw: String? var profilesOrTempTargets: Bool? @@ -32,10 +26,3 @@ struct TempTargetContactPreset: Codable, Identifiable { let description: String let until: Date? } - -struct OverrideContactPresets_: Codable, Identifiable { - let name: String - let id: String - let until: Date? - let description: String -} From 93607387c9ecbceb171cdabdc1a2dec0aaa37b89 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Wed, 20 Mar 2024 05:56:15 +0100 Subject: [PATCH 03/14] contact trick fix crash in settings --- .../View/ContactTrickRootView.swift | 2 +- .../ContactTrick/ContactPicture.swift | 34 ++++++++++++++++--- .../ContactTrick/ContactTrickManager.swift | 4 +-- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift index 81d7fd930a..82c0250906 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -221,7 +221,7 @@ extension ContactTrick { } Section(header: Text("Font")) { - if entry.isDefaultFont() && availableFonts == nil { + if availableFonts == nil { HStack(spacing: 0) { Button { loadFonts() diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index 3750aaffaa..0c96d25f03 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -28,7 +28,7 @@ struct ContactPicture: View { return formatter }() - private static let ringWidth = 0.05 // percent + private static let ringWidth = 0.07 // percent private static let ringGap = 0.02 // percent static func getImage( @@ -104,20 +104,26 @@ struct ContactPicture: View { width: rect.width, height: rect.height * 0.40 ) : rect - let topRect = CGRect(x: rect.minX, y: rect.minY + rect.height * 0.10, width: rect.width, height: rect.height * 0.20) + let topRect = CGRect( + x: rect.minX, + y: rect.minY + rect.height * 0.10, + width: rect.width, + height: rect.height * 0.20 + ) let bottomRect = CGRect( x: rect.minX, y: rect.minY + rect.height * 0.70, width: rect.width, height: rect.height * 0.20 ) - let secondaryFontSize = Int(Double(contact.fontSize) * 0.80) + let secondaryFontSize = Int(Double(contact.fontSize) * 0.70) displayPiece( value: contact.primary, contact: contact, state: state, rect: primaryRect, + fitHeigh: false, fontName: contact.fontName, fontSize: contact.fontSize, fontWeight: fontWeight, @@ -129,6 +135,7 @@ struct ContactPicture: View { contact: contact, state: state, rect: topRect, + fitHeigh: true, fontName: contact.fontName, fontSize: secondaryFontSize, fontWeight: fontWeight, @@ -141,6 +148,7 @@ struct ContactPicture: View { contact: contact, state: state, rect: bottomRect, + fitHeigh: true, fontName: contact.fontName, fontSize: secondaryFontSize, fontWeight: fontWeight, @@ -158,6 +166,7 @@ struct ContactPicture: View { contact: contact, state: state, rect: topRect, + fitHeigh: true, fontName: contact.fontName, fontSize: splitFontSize, fontWeight: fontWeight, @@ -168,6 +177,7 @@ struct ContactPicture: View { contact: contact, state: state, rect: bottomRect, + fitHeigh: true, fontName: contact.fontName, fontSize: splitFontSize, fontWeight: fontWeight, @@ -184,6 +194,7 @@ struct ContactPicture: View { contact: ContactTrickEntry, state: ContactTrickState, rect: CGRect, + fitHeigh: Bool, fontName: String?, fontSize: Int, fontWeight: UIFont.Weight, @@ -196,6 +207,7 @@ struct ContactPicture: View { drawText( text: state.glucose, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -205,6 +217,7 @@ struct ContactPicture: View { drawText( text: state.eventualBG, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -214,6 +227,7 @@ struct ContactPicture: View { drawText( text: state.delta, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -223,6 +237,7 @@ struct ContactPicture: View { drawText( text: state.trend, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -232,6 +247,7 @@ struct ContactPicture: View { drawText( text: state.glucoseDate.map { formatter.string(from: $0) }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -241,6 +257,7 @@ struct ContactPicture: View { drawText( text: state.lastLoopDate.map { formatter.string(from: $0) }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -250,6 +267,7 @@ struct ContactPicture: View { drawText( text: state.cob.map { $0.formatted() }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -259,6 +277,7 @@ struct ContactPicture: View { drawText( text: state.iob.map { $0.formatted() }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -268,6 +287,7 @@ struct ContactPicture: View { drawText( text: state.bolusRecommended.map { $0.formatted() }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -277,6 +297,7 @@ struct ContactPicture: View { drawText( text: state.carbsRequired.map { $0.formatted() }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -286,6 +307,7 @@ struct ContactPicture: View { drawText( text: state.isf.map { $0.formatted() }, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -295,6 +317,7 @@ struct ContactPicture: View { drawText( text: state.override, rect: rect, + fitHeigh: fitHeigh, fontName: fontName, fontSize: fontSize, fontWeight: fontWeight, @@ -309,6 +332,7 @@ struct ContactPicture: View { private static func drawText( text: String?, rect: CGRect, + fitHeigh: Bool, fontName: String?, fontSize: Int, fontWeight: UIFont.Weight, @@ -332,7 +356,7 @@ struct ContactPicture: View { if let text { var stringSize = text.size(withAttributes: attributes) - while stringSize.width > rect.width * 0.85 || stringSize.height > rect.height * 0.95, theFontSize > 30 { + while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 { theFontSize = theFontSize - 10 attributes = makeAttributes(size: theFontSize) stringSize = text.size(withAttributes: attributes) @@ -380,7 +404,7 @@ struct ContactPicture: View { drawProgressBar( rect: rect, progress: Double(iob) / Double(state.maxIOB), - colors: [.blue, .red], + colors: [contact.darkMode ? .blue : .blue, contact.darkMode ? .pink : .red], strokeWidth: strokeWidth ) } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 4aae17a1cb..6e6eebd07b 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -142,7 +142,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } private func renderContacts() { - print("render contacts") contacts.forEach { renderContact($0) } workItem = DispatchWorkItem(block: { print("in updateContact, no updates received for more than 5 minutes") @@ -152,8 +151,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } private func renderContact(_ entry: ContactTrickEntry) { - print("render contact: \(entry)") - guard let contactId = entry.contactId else { + guard let contactId = entry.contactId, entry.enabled else { return } From d81d972cc0e3f3fd1c64b9cf87adfcc5c75c5c46 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Thu, 21 Mar 2024 00:18:16 +0100 Subject: [PATCH 04/14] tweaks --- .../Services/ContactTrick/ContactPicture.swift | 15 +++++++++++---- .../ContactTrick/ContactTrickManager.swift | 1 + 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index 0c96d25f03..f2f3d420c2 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -28,6 +28,13 @@ struct ContactPicture: View { return formatter }() + private static let numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.decimalSeparator = "." + return formatter + }() + private static let ringWidth = 0.07 // percent private static let ringGap = 0.02 // percent @@ -106,13 +113,13 @@ struct ContactPicture: View { ) : rect let topRect = CGRect( x: rect.minX, - y: rect.minY + rect.height * 0.10, + y: rect.minY + rect.height * 0.07, width: rect.width, height: rect.height * 0.20 ) let bottomRect = CGRect( x: rect.minX, - y: rect.minY + rect.height * 0.70, + y: rect.minY + rect.height * 0.73, width: rect.width, height: rect.height * 0.20 ) @@ -580,8 +587,8 @@ struct ContactPicture_Previews: PreviewProvider { ) ), state: .constant(ContactTrickState( - glucose: "6.8", - trend: "↗︎", + glucose: "8.8", + trend: "→", lastLoopDate: .now )) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 6e6eebd07b..851f412eeb 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -319,6 +319,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal + formatter.locale = Locale(identifier: "en_US") formatter.maximumFractionDigits = 0 if settingsManager.settings.units == .mmolL { formatter.minimumFractionDigits = 1 From bccbc127fec16900b6f7d93d7d13ae7210d8e2b8 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Thu, 21 Mar 2024 00:32:28 +0100 Subject: [PATCH 05/14] empty settings/contact_trick.json --- FreeAPS/Resources/json/defaults/settings/contact_trick.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 FreeAPS/Resources/json/defaults/settings/contact_trick.json diff --git a/FreeAPS/Resources/json/defaults/settings/contact_trick.json b/FreeAPS/Resources/json/defaults/settings/contact_trick.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/FreeAPS/Resources/json/defaults/settings/contact_trick.json @@ -0,0 +1 @@ +[] From a799251b5aab6d25a02a9913de31553a449f5f04 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Sat, 23 Mar 2024 01:10:18 +0100 Subject: [PATCH 06/14] some tweaks and cleanups --- .../Sources/Models/ContactTrickEntry.swift | 34 +- FreeAPS/Sources/Models/FontTracking.swift | 32 ++ .../ContactTrick/ContactTrickStateModel.swift | 18 +- .../View/ContactTrickRootView.swift | 42 +- .../ContactTrick/ContactPicture.swift | 510 +++++++++++------- .../ContactTrick/ContactTrickManager.swift | 233 +------- .../ContactTrick/ContactTrickState.swift | 18 +- 7 files changed, 423 insertions(+), 464 deletions(-) create mode 100644 FreeAPS/Sources/Models/FontTracking.swift diff --git a/FreeAPS/Sources/Models/ContactTrickEntry.swift b/FreeAPS/Sources/Models/ContactTrickEntry.swift index dbeae70048..f30ffe3051 100644 --- a/FreeAPS/Sources/Models/ContactTrickEntry.swift +++ b/FreeAPS/Sources/Models/ContactTrickEntry.swift @@ -10,9 +10,12 @@ struct ContactTrickEntry: JSON, Equatable { var contactId: String? = nil var displayName: String? = nil var darkMode: Bool = true + var ringWidth: Int = 7 + var ringGap: Int = 2 var fontSize: Int = 100 var fontName: String = "Default Font" var fontWeight: FontWeight = .medium + var fontTracking: FontTracking = .normal func isDefaultFont() -> Bool { fontName == "Default Font" @@ -35,26 +38,32 @@ extension ContactTrickEntry { case contactId case displayName case darkMode + case ringWidth + case ringGap case fontSize case fontName case fontWeight + case fontTracking } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let enabled = try container.decode(Bool.self, forKey: .enabled) - let layout = try container.decode(ContactTrickLayout.self, forKey: .layout) - let ring1 = try container.decode(ContactTrickLargeRing.self, forKey: .ring1) - let ring2 = try container.decode(ContactTrickLargeRing.self, forKey: .ring2) - let primary = try container.decode(ContactTrickValue.self, forKey: .primary) - let top = try container.decode(ContactTrickValue.self, forKey: .top) - let bottom = try container.decode(ContactTrickValue.self, forKey: .bottom) + let enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? false + let layout = try container.decodeIfPresent(ContactTrickLayout.self, forKey: .layout) ?? .single + let ring1 = try container.decodeIfPresent(ContactTrickLargeRing.self, forKey: .ring1) ?? .none + let ring2 = try container.decodeIfPresent(ContactTrickLargeRing.self, forKey: .ring2) ?? .none + let primary = try container.decodeIfPresent(ContactTrickValue.self, forKey: .primary) ?? .glucose + let top = try container.decodeIfPresent(ContactTrickValue.self, forKey: .top) ?? .none + let bottom = try container.decodeIfPresent(ContactTrickValue.self, forKey: .bottom) ?? .none let contactId = try container.decodeIfPresent(String.self, forKey: .contactId) let displayName = try container.decodeIfPresent(String.self, forKey: .displayName) - let darkMode = try container.decode(Bool.self, forKey: .darkMode) - let fontSize = try container.decode(Int.self, forKey: .fontSize) + let darkMode = try container.decodeIfPresent(Bool.self, forKey: .darkMode) ?? true + let ringWidth = try container.decodeIfPresent(Int.self, forKey: .ringWidth) ?? 7 + let ringGap = try container.decodeIfPresent(Int.self, forKey: .ringGap) ?? 2 + let fontSize = try container.decodeIfPresent(Int.self, forKey: .fontSize) ?? 100 let fontName = try container.decodeIfPresent(String.self, forKey: .fontName) ?? "Default Font" - let fontWeight = try container.decode(FontWeight.self, forKey: .fontWeight) + let fontWeight = try container.decodeIfPresent(FontWeight.self, forKey: .fontWeight) ?? .regular + let fontTracking = try container.decodeIfPresent(FontTracking.self, forKey: .fontTracking) ?? .normal self = ContactTrickEntry( enabled: enabled, @@ -67,9 +76,12 @@ extension ContactTrickEntry { contactId: contactId, displayName: displayName, darkMode: darkMode, + ringWidth: ringWidth, + ringGap: ringGap, fontSize: fontSize, fontName: fontName, - fontWeight: fontWeight + fontWeight: fontWeight, + fontTracking: fontTracking ) } } diff --git a/FreeAPS/Sources/Models/FontTracking.swift b/FreeAPS/Sources/Models/FontTracking.swift new file mode 100644 index 0000000000..99a50546d1 --- /dev/null +++ b/FreeAPS/Sources/Models/FontTracking.swift @@ -0,0 +1,32 @@ +import Foundation + +enum FontTracking: String, JSON, Identifiable, CaseIterable, Codable { + var id: String { rawValue } + + case tighter + case tight + case normal + case wide + + var displayName: String { + switch self { + case .tighter: + NSLocalizedString("Tighter", comment: "") + case .tight: + NSLocalizedString("Tight", comment: "") + case .normal: + NSLocalizedString("Normal", comment: "") + case .wide: + NSLocalizedString("Wide", comment: "") + } + } + + var value: Double { + switch self { + case .tighter: -0.05 + case .tight: -0.025 + case .normal: 0 + case .wide: 0.05 + } + } +} diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift index 3c220e1b24..d120e56872 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift @@ -12,10 +12,6 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { case lastLoopDate case cob case iob - case bolusRecommended - case carbsRequired - case isf - case override case ring var displayName: String { @@ -38,14 +34,6 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { return NSLocalizedString("COB", comment: "") case .iob: return NSLocalizedString("IOB", comment: "") - case .bolusRecommended: - return NSLocalizedString("Bolus recommended", comment: "") - case .carbsRequired: - return NSLocalizedString("Carbs required", comment: "") - case .isf: - return NSLocalizedString("ISF", comment: "") - case .override: - return NSLocalizedString("Override %", comment: "") case .ring: return NSLocalizedString("Ring", comment: "") } @@ -72,6 +60,8 @@ enum ContactTrickLargeRing: String, JSON, CaseIterable, Identifiable, Codable { case none case loop case iob + case cob + case iobcob var displayName: String { switch self { @@ -81,6 +71,10 @@ enum ContactTrickLargeRing: String, JSON, CaseIterable, Identifiable, Codable { return NSLocalizedString("Loop status", comment: "") case .iob: return NSLocalizedString("IOB", comment: "") + case .cob: + return NSLocalizedString("COB", comment: "") + case .iobcob: + return NSLocalizedString("IOB+COB", comment: "") } } } diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift index 82c0250906..41907a9d90 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -132,6 +132,8 @@ extension ContactTrick { @State private var availableFonts: [String]? = nil private let fontSizes: [Int] = [70, 80, 90, 100, 110, 120, 130, 140, 150] + private let ringWidths: [Int] = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + private let ringGaps: [Int] = [0, 1, 2, 3, 4, 5] var body: some View { Form { @@ -218,6 +220,22 @@ extension ContactTrick { Text(v.displayName).tag(v) } } + Picker( + selection: $entry.ringWidth, + label: Text("Width") + ) { + ForEach(ringWidths, id: \.self) { s in + Text("\(s)").tag(s) + } + } + Picker( + selection: $entry.ringGap, + label: Text("Gap") + ) { + ForEach(ringGaps, id: \.self) { s in + Text("\(s)").tag(s) + } + } } Section(header: Text("Font")) { @@ -241,14 +259,12 @@ extension ContactTrick { .pickerStyle(.navigationLink) .labelsHidden() } - HStack(spacing: 0) { - Picker( - selection: $entry.fontSize, - label: Text("Size") - ) { - ForEach(fontSizes, id: \.self) { s in - Text("\(s)").tag(s) - } + Picker( + selection: $entry.fontSize, + label: Text("Size") + ) { + ForEach(fontSizes, id: \.self) { s in + Text("\(s)").tag(s) } } if entry.isDefaultFont() { @@ -261,6 +277,16 @@ extension ContactTrick { } } } + if entry.isDefaultFont() { + Picker( + selection: $entry.fontTracking, + label: Text("Tracking") + ) { + ForEach(FontTracking.allCases) { w in + Text(w.displayName).tag(w) + } + } + } } Section { Toggle("Dark mode", isOn: $entry.darkMode) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index f2f3d420c2..4fa08e7ae8 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -2,7 +2,6 @@ import Foundation import SwiftUI struct ContactPicture: View { - // copy paste from watch app MainView.swift private enum Config { static let lag: TimeInterval = 30 } @@ -10,18 +9,6 @@ struct ContactPicture: View { @Binding var contact: ContactTrickEntry @Binding var state: ContactTrickState - private static let normalColorDark = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) - private static let normalColorLight = Color(red: 17 / 256, green: 156 / 256, blue: 12 / 256) - - private static let notUrgentColorDark = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) - private static let notUrgentColorLight = Color(red: 254 / 256, green: 149 / 256, blue: 4 / 256) - - private static let urgentColorDark = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) - private static let urgentColorLight = Color(red: 255 / 256, green: 52 / 256, blue: 0 / 256) - - private static let unknownColorDark = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) - private static let unknownColorLight = Color(red: 0x88 / 256, green: 0x88 / 256, blue: 0x88 / 256) - private static let formatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "HH:mm" @@ -35,9 +22,6 @@ struct ContactPicture: View { return formatter }() - private static let ringWidth = 0.07 // percent - private static let ringGap = 0.02 // percent - static func getImage( contact: ContactTrickEntry, state: ContactTrickState @@ -49,19 +33,28 @@ struct ContactPicture: View { Color(red: 250 / 256, green: 250 / 256, blue: 250 / 256) : Color(red: 20 / 256, green: 20 / 256, blue: 20 / 256) let secondaryTextColor: Color = contact.darkMode ? - Color(red: 200 / 256, green: 200 / 256, blue: 200 / 256) : - Color(red: 60 / 256, green: 60 / 256, blue: 60 / 256) + Color(red: 220 / 256, green: 220 / 256, blue: 220 / 256) : + Color(red: 40 / 256, green: 40 / 256, blue: 40 / 256) let fontWeight = contact.fontWeight.toUI() UIGraphicsBeginImageContext(rect.size) + let ringWidth = Double(contact.ringWidth) / 100.0 + let ringGap = Double(contact.ringGap) / 100.0 + + rect = CGRect( + x: rect.minX + width * ringGap, + y: rect.minY + height * ringGap, + width: rect.width - width * ringGap * 2, + height: rect.height - height * ringGap * 2 + ) + if contact.ring1 != .none { - // offset from the white ring rect = CGRect( - x: rect.minX + width * ringGap * 2, - y: rect.minY + height * ringGap * 2, - width: rect.width - width * ringGap * 4, - height: rect.height - width * ringGap * 4 + x: rect.minX + width * ringGap, + y: rect.minY + height * ringGap, + width: rect.width - width * ringGap * 2, + height: rect.height - height * ringGap * 2 ) let ringRect = CGRect( @@ -105,25 +98,60 @@ struct ContactPicture: View { case .single: let showTop = contact.top != .none let showBottom = contact.bottom != .none + + let centerX = rect.minX + rect.width / 2 + let centerY = rect.minY + rect.height / 2 + let radius = min(rect.width, rect.height) / 2 + + let primaryHeight = radius * 0.8 + let topHeight = radius * 0.5 + var bottomHeight = radius * 0.5 + + var primaryY = centerY - primaryHeight / 2 + + if contact.bottom == .none, contact.top != .none { + primaryY += radius * 0.2 + } + if contact.bottom != .none, contact.top == .none { + primaryY -= radius * 0.2 + } + + let topY = primaryY - topHeight + let bottomY = primaryY + primaryHeight + + let primaryWidth = 2 * sqrt(radius * radius - (primaryHeight / 2) * (primaryHeight / 2)) + + let topWidth = 2 * + sqrt(radius * radius - (topHeight + primaryHeight / 2) * (topHeight + primaryHeight / 2)) + var bottomWidth = 2 * + sqrt(radius * radius - (bottomHeight + primaryHeight / 2) * (bottomHeight + primaryHeight / 2)) + + if contact.bottom != .none, contact.top == .none, + contact.ring1 == .iob || contact.ring1 == .cob || contact.ring1 == .iobcob, contact.ring2 == .none + { + bottomWidth = bottomWidth + width * ringWidth * 2 + bottomHeight = bottomHeight + height * ringWidth * 2 + } + let primaryRect = (showTop || showBottom) ? CGRect( - x: rect.minX, - y: rect.minY + rect.height * 0.30, - width: rect.width, - height: rect.height * 0.40 + x: centerX - primaryWidth / 2, + y: primaryY, + width: primaryWidth, + height: primaryHeight ) : rect let topRect = CGRect( - x: rect.minX, - y: rect.minY + rect.height * 0.07, - width: rect.width, - height: rect.height * 0.20 + x: centerX - topWidth / 2, + y: topY, + width: topWidth, + height: topHeight ) let bottomRect = CGRect( - x: rect.minX, - y: rect.minY + rect.height * 0.73, - width: rect.width, - height: rect.height * 0.20 + x: centerX - bottomWidth / 2, + y: bottomY, + width: bottomWidth, + height: bottomHeight ) - let secondaryFontSize = Int(Double(contact.fontSize) * 0.70) + let secondaryFontSize = Int(Double(contact.fontSize) * 0.90) displayPiece( value: contact.primary, @@ -134,6 +162,7 @@ struct ContactPicture: View { fontName: contact.fontName, fontSize: contact.fontSize, fontWeight: fontWeight, + fontTracking: contact.fontTracking, color: textColor ) if showTop { @@ -146,6 +175,7 @@ struct ContactPicture: View { fontName: contact.fontName, fontSize: secondaryFontSize, fontWeight: fontWeight, + fontTracking: contact.fontTracking, color: secondaryTextColor ) } @@ -159,13 +189,34 @@ struct ContactPicture: View { fontName: contact.fontName, fontSize: secondaryFontSize, fontWeight: fontWeight, + fontTracking: contact.fontTracking, color: secondaryTextColor ) } case .split: - let topRect = CGRect(x: rect.minX, y: rect.minY + height * 0.20, width: rect.width, height: rect.height * 0.30) - let bottomRect = CGRect(x: rect.minX, y: rect.minY + height * 0.50, width: rect.width, height: rect.height * 0.30) + let centerX = rect.origin.x + rect.size.width / 2 + let centerY = rect.origin.y + rect.size.height / 2 + let radius = min(rect.size.width, rect.size.height) / 2 + + let rectangleHeight = radius * sqrt(2) / 2 + let rectangleWidth = sqrt(2) * radius + + let topY = centerY - rectangleHeight + let bottomY = centerY + + let topRect = CGRect( + x: centerX - rectangleWidth / 2, + y: topY, + width: rectangleWidth, + height: rectangleHeight + ) + let bottomRect = CGRect( + x: centerX - rectangleWidth / 2, + y: bottomY, + width: rectangleWidth, + height: rectangleHeight + ) let splitFontSize = Int(Double(contact.fontSize) * 0.80) displayPiece( @@ -177,6 +228,7 @@ struct ContactPicture: View { fontName: contact.fontName, fontSize: splitFontSize, fontWeight: fontWeight, + fontTracking: contact.fontTracking, color: textColor ) displayPiece( @@ -188,6 +240,7 @@ struct ContactPicture: View { fontName: contact.fontName, fontSize: splitFontSize, fontWeight: fontWeight, + fontTracking: contact.fontTracking, color: textColor ) } @@ -205,135 +258,57 @@ struct ContactPicture: View { fontName: String?, fontSize: Int, fontWeight: UIFont.Weight, + fontTracking: FontTracking, color: Color ) { - switch value { - case .none: - break - case .glucose: - drawText( - text: state.glucose, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .eventualBG: - drawText( - text: state.eventualBG, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .delta: - drawText( - text: state.delta, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .trend: - drawText( - text: state.trend, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .glucoseDate: - drawText( - text: state.glucoseDate.map { formatter.string(from: $0) }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .lastLoopDate: - drawText( - text: state.lastLoopDate.map { formatter.string(from: $0) }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .cob: - drawText( - text: state.cob.map { $0.formatted() }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .iob: - drawText( - text: state.iob.map { $0.formatted() }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .bolusRecommended: - drawText( - text: state.bolusRecommended.map { $0.formatted() }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .carbsRequired: - drawText( - text: state.carbsRequired.map { $0.formatted() }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .isf: - drawText( - text: state.isf.map { $0.formatted() }, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - case .override: - drawText( - text: state.override, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - color: color - ) - - case .ring: - drawRing(ring: .loop, contact: contact, state: state, rect: rect, strokeWidth: rect.width * ringWidth) +// guard let context = UIGraphicsGetCurrentContext() else { +// return +// } +// +// // Set the fill color (optional) +// context.setFillColor(UIColor.red.cgColor) +// +// // Set the stroke color (optional) +// context.setStrokeColor(UIColor.black.cgColor) +// +// // Set the line width (optional) +// context.setLineWidth(2.0) +// +// // Create a rectangle +// let rectangle = CGRect(x: 50, y: 50, width: 100, height: 100) +// +// // Fill the rectangle (if fill color is set) +// context.fill(rect) +// +// // Stroke the rectangle (if stroke color is set) +// context.stroke(rect) + + guard value != .none else { return } + if value == .ring { + drawRing(ring: .loop, contact: contact, state: state, rect: rect, strokeWidth: 10.0) + return + } + let text: String? = switch value { + case .glucose: state.glucose + case .eventualBG: state.eventualBG + case .delta: state.delta + case .trend: state.trend + case .glucoseDate: state.glucoseDate.map { formatter.string(from: $0) } + case .lastLoopDate: state.lastLoopDate.map { formatter.string(from: $0) } + case .cob: state.cob.map { $0.formatted() } + case .iob: state.iob.map { $0.formatted() } + default: nil } + drawText( + text: text, + rect: rect, + fitHeigh: fitHeigh, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + fontTracking: fontTracking, + color: color + ) } private static func drawText( @@ -343,11 +318,12 @@ struct ContactPicture: View { fontName: String?, fontSize: Int, fontWeight: UIFont.Weight, + fontTracking: FontTracking, color: Color ) { var theFontSize = fontSize - func makeAttributes(size: Int) -> [NSAttributedString.Key: Any] { + func makeAttributes(_ size: Int) -> [NSAttributedString.Key: Any] { let font = if let fontName { UIFont(name: fontName, size: CGFloat(size)) ?? UIFont.systemFont(ofSize: CGFloat(size)) } else { @@ -355,17 +331,18 @@ struct ContactPicture: View { } return [ .font: font, - .foregroundColor: UIColor(color) + .foregroundColor: UIColor(color), + .tracking: fontTracking.value ] } - var attributes: [NSAttributedString.Key: Any] = makeAttributes(size: theFontSize) + var attributes: [NSAttributedString.Key: Any] = makeAttributes(theFontSize) if let text { var stringSize = text.size(withAttributes: attributes) while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 { theFontSize = theFontSize - 10 - attributes = makeAttributes(size: theFontSize) + attributes = makeAttributes(theFontSize) stringSize = text.size(withAttributes: attributes) } @@ -389,7 +366,6 @@ struct ContactPicture: View { strokeWidth: Double ) { guard let context = UIGraphicsGetCurrentContext() else { - print("no context") return } switch ring { @@ -415,6 +391,24 @@ struct ContactPicture: View { strokeWidth: strokeWidth ) } + case .cob: + if let cob = state.cob { + drawProgressBar( + rect: rect, + progress: Double(cob) / Double(state.maxCOB), + colors: [contact.darkMode ? .green : .green, contact.darkMode ? .pink : .red], + strokeWidth: strokeWidth + ) + } + case .iobcob: + drawDoubleProgressBar( + rect: rect, + progress1: state.iob.map { Double($0) / Double(state.maxIOB) }, + progress2: state.cob.map { Double($0) / Double(state.maxCOB) }, + colors1: [contact.darkMode ? .blue : .blue, contact.darkMode ? .pink : .red], + colors2: [contact.darkMode ? .green : .green, contact.darkMode ? .pink : .red], + strokeWidth: strokeWidth + ) default: break } @@ -425,16 +419,69 @@ struct ContactPicture: View { progress: Double, colors: [Color], strokeWidth: Double + ) { + let startAngle: CGFloat = -(.pi + .pi / 4.0) + let endAngle: CGFloat = .pi / 4.0 + + drawGradientArc( + rect: rect, + progress: progress, + colors: colors, + strokeWidth: strokeWidth, + startAngle: startAngle, + endAngle: endAngle, + gradientDirection: .leftToRight + ) + } + + private static func drawDoubleProgressBar( + rect: CGRect, + progress1: Double?, + progress2: Double?, + colors1: [Color], + colors2: [Color], + strokeWidth: Double + ) { + if let progress1 = progress1 { + let startAngle1: CGFloat = .pi / 2 + .pi / 6 + let endAngle1: CGFloat = 3 * .pi / 2 - .pi / 8 + drawGradientArc( + rect: rect, + progress: progress1, + colors: colors1, + strokeWidth: strokeWidth, + startAngle: startAngle1, + endAngle: endAngle1, + gradientDirection: .bottomToTop + ) + } + if let progress2 = progress2 { + let startAngle2: CGFloat = .pi / 2 - .pi / 6 + let endAngle2: CGFloat = -.pi / 2 + .pi / 8 + drawGradientArc( + rect: rect, + progress: progress2, + colors: colors2, + strokeWidth: strokeWidth, + startAngle: startAngle2, + endAngle: endAngle2, + gradientDirection: .bottomToTop + ) + } + } + + private static func drawGradientArc( + rect: CGRect, + progress: Double, + colors: [Color], + strokeWidth: Double, + startAngle: Double, + endAngle: Double, + gradientDirection: GradientDirection ) { guard let context = UIGraphicsGetCurrentContext() else { return } - let center = CGPoint(x: rect.midX, y: rect.midY) - let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2 - let lineWidth: CGFloat = strokeWidth - let startAngle: CGFloat = -(.pi + .pi / 4.0) - let endAngle: CGFloat = .pi / 4.0 - let progressAngle = startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0) let colors = colors.map { c in UIColor(c).cgColor } let locations: [CGFloat] = [0.0, 1.0] @@ -447,8 +494,33 @@ struct ContactPicture: View { } context.saveGState() - context.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: progressAngle, clockwise: false) - context.setLineWidth(lineWidth) + + let center = CGPoint(x: rect.midX, y: rect.midY) + let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2 + + // startAngle - The angle to the starting point of the arc, measured in radians from the positive x-axis. + // endAngle - The angle to the end point of the arc, measured in radians from the positive x-axis. + if startAngle > endAngle { + let progressAngle = startAngle - (startAngle - endAngle) * max(min(progress, 1.0), 0.0) + context.addArc( + center: center, + radius: radius, + startAngle: progressAngle, + endAngle: startAngle, + clockwise: false + ) + } else { + let progressAngle = startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0) + context.addArc( + center: center, + radius: radius, + startAngle: startAngle, + endAngle: progressAngle, + clockwise: false + ) + } + + context.setLineWidth(strokeWidth) context.setLineCap(.round) let segmentPath = context.path! context.strokePath() @@ -456,12 +528,23 @@ struct ContactPicture: View { context.addPath(segmentPath) context.replacePathWithStrokedPath() context.clip() - context.drawLinearGradient( - gradient, - start: CGPoint(x: rect.minX, y: rect.midY), - end: CGPoint(x: rect.maxX, y: rect.midY), - options: [] - ) + switch gradientDirection { + case .bottomToTop: + context.drawLinearGradient( + gradient, + start: CGPoint(x: rect.midX, y: rect.maxY), + end: CGPoint(x: rect.midX, y: rect.minY), + options: [] + ) + + case .leftToRight: + context.drawLinearGradient( + gradient, + start: CGPoint(x: rect.minX, y: rect.midY), + end: CGPoint(x: rect.maxX, y: rect.midY), + options: [] + ) + } context.restoreGState() } @@ -512,6 +595,11 @@ extension FontWeight { } } +enum GradientDirection: Int { + case leftToRight + case bottomToTop +} + struct ContactPicturePreview: View { @Binding var contact: ContactTrickEntry @Binding var state: ContactTrickState @@ -557,6 +645,24 @@ struct ContactPicture_Previews: PreviewProvider { ).previewDisplayName("bg + trend + delta") + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + ring1: .iob, + primary: .glucose, + bottom: .trend, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + iob: 6.1, + maxIOB: 8.0 + )) + ).previewDisplayName("bg + trend + iob ring") + ContactPicturePreview( contact: .constant( ContactTrickEntry( @@ -608,7 +714,7 @@ struct ContactPicture_Previews: PreviewProvider { state: .constant(ContactTrickState( glucose: "6.8", lastLoopDate: .now - 7.minutes, - eventualBG: "⇢ 6.2" + eventualBG: "6.2" )) ).previewDisplayName("bg + eventual + ring1") @@ -693,19 +799,43 @@ struct ContactPicture_Previews: PreviewProvider { ContactPicturePreview( contact: .constant( ContactTrickEntry( - layout: .split, - top: .override, - bottom: .iob, + layout: .single, + ring1: .iobcob, + primary: .none, fontSize: 100, fontWeight: .medium ) ), state: .constant(ContactTrickState( - iob: 1.5, - override: "75 %" + iob: 5.5, + cob: 25, + maxIOB: 10, + maxCOB: 120 + )) + + ).previewDisplayName("iobcob ring") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + layout: .single, + ring1: .iobcob, + primary: .glucose, + bottom: .trend, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + glucose: "6.8", + trend: "↗︎", + iob: 5.5, + cob: 25, + maxIOB: 10, + maxCOB: 120 )) - ).previewDisplayName("overrides + iob") + ).previewDisplayName("bg + trend + iobcob ring") } } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 851f412eeb..69b44e2de3 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -2,23 +2,12 @@ import Algorithms import Combine import Contacts import Foundation -import LoopKit -import LoopKitUI -import MinimedKit -import MockKit -import OmniBLE -import OmniKit -import ShareClient -import SwiftDate import Swinject -import UserNotifications protocol ContactTrickManager { func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) } -private let accessLock = NSRecursiveLock(label: "BaseContactTrickManager.accessLock") - final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private let processQueue = DispatchQueue(label: "BaseContactTrickManager.processQueue") private var state = ContactTrickState() @@ -29,7 +18,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { @Injected() private var settingsManager: SettingsManager! @Injected() private var apsManager: APSManager! @Injected() private var storage: FileStorage! - @Injected() private var tempTargetsStorage: TempTargetsStorage! private var contacts: [ContactTrickEntry] = [] @@ -46,14 +34,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { broadcaster.register(GlucoseObserver.self, observer: self) broadcaster.register(SuggestionObserver.self, observer: self) broadcaster.register(SettingsObserver.self, observer: self) - broadcaster.register(PumpHistoryObserver.self, observer: self) - broadcaster.register(PumpSettingsObserver.self, observer: self) - broadcaster.register(BasalProfileObserver.self, observer: self) - broadcaster.register(TempTargetsObserver.self, observer: self) - broadcaster.register(CarbsObserver.self, observer: self) - broadcaster.register(EnactedSuggestionObserver.self, observer: self) - broadcaster.register(PumpBatteryObserver.self, observer: self) - broadcaster.register(PumpReservoirObserver.self, observer: self) configureState() } @@ -76,66 +56,14 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { self.state.trend = glucoseValues.trend self.state.delta = glucoseValues.delta self.state.glucoseDate = readings.first?.date ?? .distantPast - self.state.lastLoopDate = self.enactedSuggestion?.recieved == true ? self.enactedSuggestion?.deliverAt : self - .apsManager.lastLoopDate - self.state.carbsRequired = self.suggestion?.carbsReq - - var insulinRequired = self.suggestion?.insulinReq ?? 0 - - var double: Decimal = 2 - if self.suggestion?.manualBolusErrorString == 0 { - insulinRequired = self.suggestion?.insulinForManualBolus ?? 0 - double = 1 - } - - self.state.useNewCalc = self.settingsManager.settings.useCalc - - if !(self.state.useNewCalc ?? false) { - self.state.bolusRecommended = self.apsManager - .roundBolus(amount: max( - insulinRequired * (self.settingsManager.settings.insulinReqPercentage / 100) * double, - 0 - )) - } else { - let recommended = self.newBolusCalc(delta: readings, suggestion: self.suggestion) - self.state.bolusRecommended = self.apsManager - .roundBolus(amount: max(recommended, 0)) - } + self.state.lastLoopDate = self.apsManager.lastLoopDate self.state.iob = self.suggestion?.iob - self.state.maxIOB = self.settingsManager.preferences.maxIOB self.state.cob = self.suggestion?.cob - self.state.tempTargets = self.tempTargetsStorage.presets() - .map { target -> TempTargetContactPreset in - let untilDate = self.tempTargetsStorage.current().flatMap { currentTarget -> Date? in - guard currentTarget.id == target.id else { return nil } - let date = currentTarget.createdAt.addingTimeInterval(TimeInterval(currentTarget.duration * 60)) - return date > Date() ? date : nil - } - return TempTargetContactPreset( - name: target.displayName, - id: target.id, - description: self.descriptionForTarget(target), - until: untilDate - ) - } - - self.state.profilesOrTempTargets = self.settingsManager.settings.profilesOrTempTargets - - let eBG = self.eventualBGString() - self.state.eventualBG = eBG.map { "⇢ " + $0 } - self.state.eventualBGRaw = eBG - - self.state.isf = self.suggestion?.isf - - let overrideArray = overrideStorage.fetchLatestOverride() - - if overrideArray.first?.enabled ?? false { - let percentString = "\((overrideArray.first?.percentage ?? 100).formatted(.number)) %" - self.state.override = percentString - } else { - self.state.override = "100 %" - } + self.state.maxIOB = self.settingsManager.preferences.maxIOB + self.state.maxCOB = self.settingsManager.preferences.maxCOB + + self.state.eventualBG = self.eventualBGString() self.renderContacts() } @@ -183,63 +111,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } } - // copy-pastes from the BaseWatchManager - private func newBolusCalc(delta: [Readings], suggestion _: Suggestion?) -> Decimal { - var conversion: Decimal = 1 - // Settings - if settingsManager.settings.units == .mmolL { - conversion = 0.0555 - } - let isf = state.isf ?? 0 - let target = suggestion?.current_target ?? 0 - let carbratio = suggestion?.carbRatio ?? 0 - let bg = delta.first?.glucose ?? 0 - let cob = state.cob ?? 0 - let iob = state.iob ?? 0 - let useFattyMealCorrectionFactor = settingsManager.settings.fattyMeals - let fattyMealFactor = settingsManager.settings.fattyMealFactor - let maxBolus = settingsManager.pumpSettings.maxBolus - var insulinCalculated: Decimal = 0 - // insulin needed for the current blood glucose - let targetDifference = (Decimal(bg) - target) * conversion - let targetDifferenceInsulin = targetDifference / isf - // more or less insulin because of bg trend in the last 15 minutes - var bgDelta: Int = 0 - if delta.count >= 3 { - bgDelta = Int((delta.first?.glucose ?? 0) - delta[2].glucose) - } - let fifteenMinInsulin = (Decimal(bgDelta) * conversion) / isf - // determine whole COB for which we want to dose insulin for and then determine insulin for wholeCOB - let wholeCobInsulin = cob / carbratio - // determine how much the calculator reduces/ increases the bolus because of IOB - let iobInsulinReduction = (-1) * iob - // adding everything together - // add a calc for the case that no fifteenMinInsulin is available - var wholeCalc: Decimal = 0 - if bgDelta != 0 { - wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin + fifteenMinInsulin) - } else { - // add (rare) case that no glucose value is available -> maybe display warning? - // if no bg is available, ?? sets its value to 0 - if bg == 0 { - wholeCalc = (iobInsulinReduction + wholeCobInsulin) - } else { - wholeCalc = (targetDifferenceInsulin + iobInsulinReduction + wholeCobInsulin) - } - } - // apply custom factor at the end of the calculations - let result = wholeCalc * settingsManager.settings.overrideFactor - // apply custom factor if fatty meal toggle in bolus calc config settings is on and the box for fatty meals is checked (in RootView) - if useFattyMealCorrectionFactor { - insulinCalculated = result * fattyMealFactor - } else { - insulinCalculated = result - } - // Not 0 or over maxBolus - insulinCalculated = max(min(insulinCalculated, maxBolus), 0) - return insulinCalculated - } - private func glucoseText(_ glucose: [Readings]) -> (glucose: String, trend: String, delta: String) { let glucoseValue = glucose.first?.glucose ?? 0 @@ -265,23 +136,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return (glucoseText, directionText, deltaText) } - private func descriptionForTarget(_ target: TempTarget) -> String { - let units = settingsManager.settings.units - - var low = target.targetBottom - var high = target.targetTop - if units == .mmolL { - low = low?.asMmolL - high = high?.asMmolL - } - - let description = - "\(targetFormatter.string(from: (low ?? 0) as NSNumber)!) - \(targetFormatter.string(from: (high ?? 0) as NSNumber)!)" + - " for \(targetFormatter.string(from: target.duration as NSNumber)!) min" - - return description - } - private func eventualBGString() -> String? { guard let eventualBG = suggestion?.eventualBG else { return nil @@ -292,30 +146,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { )! } - private func description(_ preset: OverridePresets) -> String { - let rawtarget = (preset.target ?? 0) as Decimal - - let targetValue = settingsManager.settings.units == .mmolL ? rawtarget.asMmolL : rawtarget - let target: String = rawtarget > 6 ? glucoseFormatter.string(from: targetValue as NSNumber) ?? "" : "" - - let percentage = preset.percentage != 100 ? preset.percentage.formatted() + "%" : "" - let string = (preset.target ?? 0) as Decimal > 6 && !percentage.isEmpty ? target + " " + settingsManager.settings.units - .rawValue + ", " + percentage : target + percentage - return string - } - - private func description(_ override: Override) -> String { - let rawtarget = (override.target ?? 0) as Decimal - - let targetValue = settingsManager.settings.units == .mmolL ? rawtarget.asMmolL : rawtarget - let target: String = rawtarget > 6 ? glucoseFormatter.string(from: targetValue as NSNumber) ?? "" : "" - - let percentage = override.percentage != 100 ? override.percentage.formatted() + "%" : "" - let string = (override.target ?? 0) as Decimal > 6 && !percentage.isEmpty ? target + " " + settingsManager.settings.units - .rawValue + ", " + percentage : target + percentage - return string - } - private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -344,34 +174,15 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return formatter } - private var targetFormatter: NumberFormatter { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 1 - return formatter - } - private var suggestion: Suggestion? { storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) } - - private var enactedSuggestion: Suggestion? { - storage.retrieve(OpenAPS.Enact.enacted, as: Suggestion.self) - } } extension BaseContactTrickManager: GlucoseObserver, SuggestionObserver, - SettingsObserver, - PumpHistoryObserver, - PumpSettingsObserver, - BasalProfileObserver, - TempTargetsObserver, - CarbsObserver, - EnactedSuggestionObserver, - PumpBatteryObserver, - PumpReservoirObserver + SettingsObserver { func glucoseDidUpdate(_: [BloodGlucose]) { configureState() @@ -384,36 +195,4 @@ extension BaseContactTrickManager: func settingsDidChange(_: FreeAPSSettings) { configureState() } - - func pumpHistoryDidUpdate(_: [PumpHistoryEvent]) { - // TODO: - } - - func pumpSettingsDidChange(_: PumpSettings) { - configureState() - } - - func basalProfileDidChange(_: [BasalProfileEntry]) { - // TODO: - } - - func tempTargetsDidUpdate(_: [TempTarget]) { - configureState() - } - - func carbsDidUpdate(_: [CarbsEntry]) { - // TODO: - } - - func enactedSuggestionDidUpdate(_: Suggestion) { - configureState() - } - - func pumpBatteryDidChange(_: Battery) { - // TODO: - } - - func pumpReservoirDidChange(_: Decimal) { - // TODO: - } } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift index a7ab8d1cab..c6d987ed34 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift @@ -6,23 +6,9 @@ struct ContactTrickState: Codable { var delta: String? var glucoseDate: Date? var lastLoopDate: Date? - var carbsRequired: Decimal? - var bolusRecommended: Decimal? var iob: Decimal? - var maxIOB: Decimal = 0.0 var cob: Decimal? - var tempTargets: [TempTargetContactPreset] = [] var eventualBG: String? - var eventualBGRaw: String? - var profilesOrTempTargets: Bool? - var useNewCalc: Bool? - var isf: Decimal? - var override: String? -} - -struct TempTargetContactPreset: Codable, Identifiable { - let name: String - let id: String - let description: String - let until: Date? + var maxIOB: Decimal = 10.0 + var maxCOB: Decimal = 120.0 } From 2ccbbdc80c0c3f2e65c1b9c0ace8f49f810e19f6 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Sat, 23 Mar 2024 02:50:16 +0100 Subject: [PATCH 07/14] tweaks --- .../View/ContactTrickRootView.swift | 21 +++++++++---------- .../ContactTrick/ContactTrickManager.swift | 2 -- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift index 41907a9d90..aba248165d 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -240,7 +240,8 @@ extension ContactTrick { Section(header: Text("Font")) { if availableFonts == nil { - HStack(spacing: 0) { + HStack { + Spacer() Button { loadFonts() } label: { @@ -267,6 +268,14 @@ extension ContactTrick { Text("\(s)").tag(s) } } + Picker( + selection: $entry.fontTracking, + label: Text("Tracking") + ) { + ForEach(FontTracking.allCases) { w in + Text(w.displayName).tag(w) + } + } if entry.isDefaultFont() { Picker( selection: $entry.fontWeight, @@ -277,16 +286,6 @@ extension ContactTrick { } } } - if entry.isDefaultFont() { - Picker( - selection: $entry.fontTracking, - label: Text("Tracking") - ) { - ForEach(FontTracking.allCases) { w in - Text(w.displayName).tag(w) - } - } - } } Section { Toggle("Dark mode", isOn: $entry.darkMode) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 69b44e2de3..9c2dc8b0dd 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -39,7 +39,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) { - print("update contacts: \(contacts)") processQueue.async { self.contacts = contacts self.renderContacts() @@ -49,7 +48,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private func configureState() { processQueue.async { - let overrideStorage = OverrideStorage() let readings = self.coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) let glucoseValues = self.glucoseText(readings) self.state.glucose = glucoseValues.glucose From 7b11eb02b5979835ef98dbe6e63ac0c670aab76f Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Sat, 23 Mar 2024 03:06:44 +0100 Subject: [PATCH 08/14] un-mess up project.pbxproj --- FreeAPS.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index 35be544362..388ea37f19 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/FreeAPS.xcodeproj/project.pbxproj @@ -454,6 +454,7 @@ F2159A572BA6239F00A0B716 /* ContactTrickManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A562BA6239F00A0B716 /* ContactTrickManager.swift */; }; F2159A592BA78B7400A0B716 /* ContactTrickState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A582BA78B7400A0B716 /* ContactTrickState.swift */; }; F2159A5B2BA7939C00A0B716 /* ContactPicture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2159A5A2BA7939C00A0B716 /* ContactPicture.swift */; }; + F270F68D2BAE374C00F6D8DD /* FontTracking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F270F68C2BAE374C00F6D8DD /* FontTracking.swift */; }; F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; }; F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; }; F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; }; @@ -1049,6 +1050,7 @@ F2159A562BA6239F00A0B716 /* ContactTrickManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickManager.swift; sourceTree = ""; }; F2159A582BA78B7400A0B716 /* ContactTrickState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactTrickState.swift; sourceTree = ""; }; F2159A5A2BA7939C00A0B716 /* ContactPicture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactPicture.swift; sourceTree = ""; }; + F270F68C2BAE374C00F6D8DD /* FontTracking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontTracking.swift; sourceTree = ""; }; F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = ""; }; F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = ""; }; F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = ""; }; @@ -1837,6 +1839,7 @@ 192424CA2B7A64E70063CBF0 /* NIghtscoutExercise.swift */, F2159A512BA60F7A00A0B716 /* FontWeight.swift */, F2159A532BA6207F00A0B716 /* ContactTrickEntry.swift */, + F270F68C2BAE374C00F6D8DD /* FontTracking.swift */, ); path = Models; sourceTree = ""; @@ -3173,6 +3176,7 @@ F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */, 19F95FF729F10FEE00314DDC /* StatStateModel.swift in Sources */, 385CEAC125F2EA52002D6D5B /* Announcement.swift in Sources */, + F270F68D2BAE374C00F6D8DD /* FontTracking.swift in Sources */, 8B759CFCF47B392BB365C251 /* BasalProfileEditorDataFlow.swift in Sources */, 195D80B42AF6973A00D25097 /* DynamicRootView.swift in Sources */, 389442CB25F65F7100FA1F27 /* NightscoutTreatment.swift in Sources */, From cacab46f9cee527c21caa46ac321b24a6c12bd1f Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Sat, 23 Mar 2024 13:22:14 +0100 Subject: [PATCH 09/14] no APSManager, more detailed error logging when updating contacts --- .../ContactTrick/ContactTrickManager.swift | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 9c2dc8b0dd..a428af2a47 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -16,7 +16,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { @Injected() private var broadcaster: Broadcaster! @Injected() private var settingsManager: SettingsManager! - @Injected() private var apsManager: APSManager! @Injected() private var storage: FileStorage! private var contacts: [ContactTrickEntry] = [] @@ -54,7 +53,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { self.state.trend = glucoseValues.trend self.state.delta = glucoseValues.delta self.state.glucoseDate = readings.first?.date ?? .distantPast - self.state.lastLoopDate = self.apsManager.lastLoopDate + self.state.lastLoopDate = self.suggestion?.timestamp self.state.iob = self.suggestion?.iob self.state.cob = self.suggestion?.cob @@ -104,8 +103,26 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { saveRequest.update(mutableContact) do { try contactStore.execute(saveRequest) + } catch let error as NSError { + var details: String? + if error.domain == CNErrorDomain { + switch error.code { + case CNError.authorizationDenied.rawValue: + details = "Authorization denied" + case CNError.communicationError.rawValue: + details = "Communication error" + case CNError.insertedRecordAlreadyExists.rawValue: + details = "Record already exists" + case CNError.dataAccessError.rawValue: + details = "Data access error" + default: + details = "Code \(error.code)" + } + } + print("in updateContact, failed to update the contact - \(details ?? "no details"): \(error.localizedDescription)") + } catch { - print("in updateContact, failed to update the contact") + print("in updateContact, failed to update the contact: \(error.localizedDescription)") } } From 711214b053083473751d959ee33ece2c2cb75b14 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Wed, 27 Mar 2024 17:56:55 +0100 Subject: [PATCH 10/14] refactoring, add missing scheduled task cancellation --- .../ContactTrick/ContactTrickManager.swift | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index a428af2a47..102ccead35 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -10,7 +10,6 @@ protocol ContactTrickManager { final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private let processQueue = DispatchQueue(label: "BaseContactTrickManager.processQueue") - private var state = ContactTrickState() private let contactStore = CNContactStore() private var workItem: DispatchWorkItem? @@ -26,48 +25,53 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { super.init() injectServices(resolver) - contacts = storage.retrieve(OpenAPS.Settings.contactTrick, as: [ContactTrickEntry].self) - ?? [ContactTrickEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.contactTrick)) - ?? [] - broadcaster.register(GlucoseObserver.self, observer: self) broadcaster.register(SuggestionObserver.self, observer: self) broadcaster.register(SettingsObserver.self, observer: self) - configureState() + contacts = storage.retrieve(OpenAPS.Settings.contactTrick, as: [ContactTrickEntry].self) + ?? [ContactTrickEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.contactTrick)) + ?? [] + + processQueue.async { + self.renderContacts() + } } func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) { + self.contacts = contacts + processQueue.async { - self.contacts = contacts self.renderContacts() completion(.success(())) } } - private func configureState() { - processQueue.async { - let readings = self.coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) - let glucoseValues = self.glucoseText(readings) - self.state.glucose = glucoseValues.glucose - self.state.trend = glucoseValues.trend - self.state.delta = glucoseValues.delta - self.state.glucoseDate = readings.first?.date ?? .distantPast - self.state.lastLoopDate = self.suggestion?.timestamp + private func renderContacts() { + if let workItem = workItem, !workItem.isCancelled { + workItem.cancel() + } - self.state.iob = self.suggestion?.iob - self.state.cob = self.suggestion?.cob - self.state.maxIOB = self.settingsManager.preferences.maxIOB - self.state.maxCOB = self.settingsManager.preferences.maxCOB + let readings = coreDataStorage.fetchGlucose(interval: DateFilter().twoHours) + let glucoseValues = glucoseText(readings) - self.state.eventualBG = self.eventualBGString() + let suggestion: Suggestion? = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) - self.renderContacts() - } - } + let state = ContactTrickState( + glucose: glucoseValues.glucose, + trend: glucoseValues.trend, + delta: glucoseValues.delta, + glucoseDate: readings.first?.date ?? .distantPast, + lastLoopDate: suggestion?.timestamp, + iob: suggestion?.iob, + cob: suggestion?.cob, + eventualBG: eventualBGString(suggestion), + maxIOB: settingsManager.preferences.maxIOB, + maxCOB: settingsManager.preferences.maxCOB + ) + + contacts.forEach { renderContact($0, state) } - private func renderContacts() { - contacts.forEach { renderContact($0) } workItem = DispatchWorkItem(block: { print("in updateContact, no updates received for more than 5 minutes") self.renderContacts() @@ -75,7 +79,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { DispatchQueue.main.asyncAfter(deadline: .now() + 5 * 60 + 15, execute: workItem!) } - private func renderContact(_ entry: ContactTrickEntry) { + private func renderContact(_ entry: ContactTrickEntry, _ state: ContactTrickState) { guard let contactId = entry.contactId, entry.enabled else { return } @@ -99,6 +103,10 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { state: state ).pngData() + saveUpdatedContact(mutableContact) + } + + private func saveUpdatedContact(_ mutableContact: CNMutableContact) { let saveRequest = CNSaveRequest() saveRequest.update(mutableContact) do { @@ -151,7 +159,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return (glucoseText, directionText, deltaText) } - private func eventualBGString() -> String? { + private func eventualBGString(_ suggestion: Suggestion?) -> String? { guard let eventualBG = suggestion?.eventualBG else { return nil } @@ -188,10 +196,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { formatter.positivePrefix = "+" return formatter } - - private var suggestion: Suggestion? { - storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) - } } extension BaseContactTrickManager: @@ -200,14 +204,14 @@ extension BaseContactTrickManager: SettingsObserver { func glucoseDidUpdate(_: [BloodGlucose]) { - configureState() + renderContacts() } func suggestionDidUpdate(_: Suggestion) { - configureState() + renderContacts() } func settingsDidChange(_: FreeAPSSettings) { - configureState() + renderContacts() } } From ab65bb1cfbd363a3f29b8c179af4b7b7a30a667b Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Fri, 12 Apr 2024 17:16:58 +0200 Subject: [PATCH 11/14] refactoring, tweaks to image rendering --- .../ContactTrick/ContactTrickStateModel.swift | 3 - .../ContactTrick/ContactPicture.swift | 245 ++++++++++++------ .../ContactTrick/ContactTrickManager.swift | 48 ++-- .../ContactTrick/ContactTrickState.swift | 3 +- 4 files changed, 197 insertions(+), 102 deletions(-) diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift index d120e56872..61c448ffa0 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift @@ -8,7 +8,6 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { case eventualBG case delta case trend - case glucoseDate case lastLoopDate case cob case iob @@ -26,8 +25,6 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { return NSLocalizedString("Delta", comment: "") case .trend: return NSLocalizedString("Trend", comment: "") - case .glucoseDate: - return NSLocalizedString("Glucose date", comment: "") case .lastLoopDate: return NSLocalizedString("Last loop date", comment: "") case .cob: diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index 4fa08e7ae8..3589963c37 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -285,34 +285,45 @@ struct ContactPicture: View { guard value != .none else { return } if value == .ring { - drawRing(ring: .loop, contact: contact, state: state, rect: rect, strokeWidth: 10.0) + drawRing( + ring: .loop, + contact: contact, + state: state, + rect: CGRect( + x: rect.minX + rect.width * 0.10, + y: rect.minY + rect.height * 0.10, + width: rect.width * 0.80, + height: rect.height * 0.80 + ), + strokeWidth: 10.0 + ) return } - let text: String? = switch value { + if let text = switch value { case .glucose: state.glucose case .eventualBG: state.eventualBG case .delta: state.delta case .trend: state.trend - case .glucoseDate: state.glucoseDate.map { formatter.string(from: $0) } - case .lastLoopDate: state.lastLoopDate.map { formatter.string(from: $0) } - case .cob: state.cob.map { $0.formatted() } - case .iob: state.iob.map { $0.formatted() } + case .lastLoopDate: state.lastLoopDate.map({ formatter.string(from: $0) }) + case .cob: state.cobText + case .iob: state.iobText default: nil + } { + drawText( + text: text, + rect: rect, + fitHeigh: fitHeigh, + fontName: fontName, + fontSize: fontSize, + fontWeight: fontWeight, + fontTracking: fontTracking, + color: color + ) } - drawText( - text: text, - rect: rect, - fitHeigh: fitHeigh, - fontName: fontName, - fontSize: fontSize, - fontWeight: fontWeight, - fontTracking: fontTracking, - color: color - ) } private static func drawText( - text: String?, + text: String, rect: CGRect, fitHeigh: Bool, fontName: String?, @@ -338,24 +349,22 @@ struct ContactPicture: View { var attributes: [NSAttributedString.Key: Any] = makeAttributes(theFontSize) - if let text { - var stringSize = text.size(withAttributes: attributes) - while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 { - theFontSize = theFontSize - 10 - attributes = makeAttributes(theFontSize) - stringSize = text.size(withAttributes: attributes) - } - - text.draw( - in: CGRectMake( - rect.minX + (rect.width - stringSize.width) / 2, - rect.minY + (rect.height - stringSize.height) / 2, - rect.minX + stringSize.width, - rect.minY + stringSize.height - ), - withAttributes: attributes - ) + var stringSize = text.size(withAttributes: attributes) + while stringSize.width > rect.width * 0.90 || fitHeigh && (stringSize.height > rect.height * 0.95), theFontSize > 50 { + theFontSize = theFontSize - 10 + attributes = makeAttributes(theFontSize) + stringSize = text.size(withAttributes: attributes) } + + text.draw( + in: CGRectMake( + rect.minX + (rect.width - stringSize.width) / 2, + rect.minY + (rect.height - stringSize.height) / 2, + rect.minX + stringSize.width, + rect.minY + stringSize.height + ), + withAttributes: attributes + ) } private static func drawRing( @@ -443,8 +452,8 @@ struct ContactPicture: View { strokeWidth: Double ) { if let progress1 = progress1 { - let startAngle1: CGFloat = .pi / 2 + .pi / 6 - let endAngle1: CGFloat = 3 * .pi / 2 - .pi / 8 + let startAngle1: CGFloat = .pi / 2 + .pi / 5 + let endAngle1: CGFloat = 3 * .pi / 2 - .pi / 5 drawGradientArc( rect: rect, progress: progress1, @@ -456,8 +465,8 @@ struct ContactPicture: View { ) } if let progress2 = progress2 { - let startAngle2: CGFloat = .pi / 2 - .pi / 6 - let endAngle2: CGFloat = -.pi / 2 + .pi / 8 + let startAngle2: CGFloat = .pi / 2 - .pi / 5 + let endAngle2: CGFloat = -.pi / 2 + .pi / 5 drawGradientArc( rect: rect, progress: progress2, @@ -498,36 +507,55 @@ struct ContactPicture: View { let center = CGPoint(x: rect.midX, y: rect.midY) let radius = min(rect.width, rect.height) / 2 - strokeWidth / 2 - // startAngle - The angle to the starting point of the arc, measured in radians from the positive x-axis. - // endAngle - The angle to the end point of the arc, measured in radians from the positive x-axis. - if startAngle > endAngle { - let progressAngle = startAngle - (startAngle - endAngle) * max(min(progress, 1.0), 0.0) - context.addArc( - center: center, - radius: radius, - startAngle: progressAngle, - endAngle: startAngle, - clockwise: false + // angle - The angle to the starting point of the arc, measured in radians from the positive x-axis. + + context.setLineWidth(strokeWidth) + context.setLineCap(.round) + + let circumference = 2 * .pi * radius + let offsetAngle = (strokeWidth / circumference * 1.1) * 2 * .pi + + let (start, middle, end) = if startAngle > endAngle { + ( + endAngle, + startAngle - (startAngle - endAngle) * max(min(progress, 1.0), 0.0), + startAngle ) } else { - let progressAngle = startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0) - context.addArc( - center: center, + ( + startAngle, + startAngle + (endAngle - startAngle) * max(min(progress, 1.0), 0.0), + endAngle + ) + } + + if start < middle - offsetAngle { + let arcPath1 = UIBezierPath() + arcPath1.addArc( + withCenter: center, radius: radius, - startAngle: startAngle, - endAngle: progressAngle, - clockwise: false + startAngle: start, + endAngle: middle - offsetAngle, + clockwise: true ) + context.addPath(arcPath1.cgPath) + } + + if middle + offsetAngle < end { + let arcPath2 = UIBezierPath() + arcPath2.addArc( + withCenter: center, + radius: radius, + startAngle: middle + offsetAngle, + endAngle: end, + clockwise: true + ) + context.addPath(arcPath2.cgPath) } - context.setLineWidth(strokeWidth) - context.setLineCap(.round) - let segmentPath = context.path! - context.strokePath() - context.saveGState() - context.addPath(segmentPath) context.replacePathWithStrokedPath() context.clip() + switch gradientDirection { case .bottomToTop: context.drawLinearGradient( @@ -545,6 +573,24 @@ struct ContactPicture: View { options: [] ) } + context.resetClip() + + let circleCenter = CGPoint( + x: center.x + radius * cos(middle), + y: center.y + radius * sin(middle) + ) + + context.setLineWidth(strokeWidth * 0.7) + context.setStrokeColor(UIColor.white.cgColor) + context.addArc( + center: circleCenter, + radius: 0, + startAngle: 0, + endAngle: .pi * 2, + clockwise: true + ) + context.strokePath() + context.restoreGState() } @@ -640,7 +686,8 @@ struct ContactPicture_Previews: PreviewProvider { glucose: "6.8", trend: "↗︎", delta: "+0.2", - cob: 25 + cob: 25, + cobText: "25" )) ).previewDisplayName("bg + trend + delta") @@ -659,6 +706,7 @@ struct ContactPicture_Previews: PreviewProvider { glucose: "6.8", trend: "↗︎", iob: 6.1, + iobText: "6.1", maxIOB: 8.0 )) ).previewDisplayName("bg + trend + iob ring") @@ -719,26 +767,6 @@ struct ContactPicture_Previews: PreviewProvider { ).previewDisplayName("bg + eventual + ring1") - ContactPicturePreview( - contact: .constant( - ContactTrickEntry( - ring1: .loop, - primary: .glucoseDate, - top: .none, - bottom: .none, - fontSize: 100, - fontWeight: .medium - ) - ), - state: .constant(ContactTrickState( - glucose: "6.8", - trend: "↗︎", - glucoseDate: .now - 3.minutes, - lastLoopDate: .now - )) - - ).previewDisplayName("glucoseDate + ring1") - ContactPicturePreview( contact: .constant( ContactTrickEntry( @@ -774,6 +802,7 @@ struct ContactPicture_Previews: PreviewProvider { glucose: "6.8", lastLoopDate: .now, iob: 6.1, + iobText: "6.1", maxIOB: 8.0 )) @@ -791,7 +820,9 @@ struct ContactPicture_Previews: PreviewProvider { ), state: .constant(ContactTrickState( iob: 1.5, - cob: 25 + iobText: "1.5", + cob: 25, + cobText: "25" )) ).previewDisplayName("iob + cob") @@ -802,19 +833,65 @@ struct ContactPicture_Previews: PreviewProvider { layout: .single, ring1: .iobcob, primary: .none, + ringWidth: 8, + ringGap: 3, fontSize: 100, fontWeight: .medium ) ), state: .constant(ContactTrickState( - iob: 5.5, + iob: 1, + iobText: "5.5", cob: 25, + cobText: "25", maxIOB: 10, maxCOB: 120 )) ).previewDisplayName("iobcob ring") + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + layout: .single, + ring1: .iobcob, + primary: .none, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + iob: -0.2, + iobText: "0.0", + cob: 0, + cobText: "0", + maxIOB: 10, + maxCOB: 120 + )) + + ).previewDisplayName("iobcob ring (0/0)") + + ContactPicturePreview( + contact: .constant( + ContactTrickEntry( + layout: .single, + ring1: .iobcob, + primary: .none, + fontSize: 100, + fontWeight: .medium + ) + ), + state: .constant(ContactTrickState( + iob: 10, + iobText: "0.0", + cob: 120, + cobText: "0", + maxIOB: 10, + maxCOB: 120 + )) + + ).previewDisplayName("iobcob ring (max/max)") + ContactPicturePreview( contact: .constant( ContactTrickEntry( @@ -830,7 +907,9 @@ struct ContactPicture_Previews: PreviewProvider { glucose: "6.8", trend: "↗︎", iob: 5.5, + iobText: "5.5", cob: 25, + cobText: "25", maxIOB: 10, maxCOB: 120 )) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 102ccead35..0789ec1ead 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -19,20 +19,19 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private var contacts: [ContactTrickEntry] = [] - let coreDataStorage = CoreDataStorage() + private let coreDataStorage = CoreDataStorage() init(resolver: Resolver) { super.init() injectServices(resolver) - broadcaster.register(GlucoseObserver.self, observer: self) broadcaster.register(SuggestionObserver.self, observer: self) broadcaster.register(SettingsObserver.self, observer: self) contacts = storage.retrieve(OpenAPS.Settings.contactTrick, as: [ContactTrickEntry].self) ?? [ContactTrickEntry](from: OpenAPS.defaults(for: OpenAPS.Settings.contactTrick)) ?? [] - + processQueue.async { self.renderContacts() } @@ -40,7 +39,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { func updateContacts(contacts: [ContactTrickEntry], completion: @escaping (Result) -> Void) { self.contacts = contacts - + processQueue.async { self.renderContacts() completion(.success(())) @@ -58,13 +57,18 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { let suggestion: Suggestion? = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) let state = ContactTrickState( - glucose: glucoseValues.glucose, + glucose: bgString(suggestion), trend: glucoseValues.trend, delta: glucoseValues.delta, - glucoseDate: readings.first?.date ?? .distantPast, lastLoopDate: suggestion?.timestamp, iob: suggestion?.iob, + iobText: suggestion?.iob.map { iob in + iobFormatter.string(from: iob as NSNumber)! + }, cob: suggestion?.cob, + cobText: suggestion?.cob.map { cob in + cobFormatter.string(from: cob as NSNumber)! + }, eventualBG: eventualBGString(suggestion), maxIOB: settingsManager.preferences.maxIOB, maxCOB: settingsManager.preferences.maxCOB @@ -159,12 +163,22 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return (glucoseText, directionText, deltaText) } + private func bgString(_ suggestion: Suggestion?) -> String? { + guard let bg = suggestion?.bg else { + return nil + } + let units = settingsManager.settings.units + return glucoseFormatter.string( + from: (units == .mmolL ? bg.asMmolL : bg) as NSNumber + )! + } + private func eventualBGString(_ suggestion: Suggestion?) -> String? { guard let eventualBG = suggestion?.eventualBG else { return nil } let units = settingsManager.settings.units - return eventualFormatter.string( + return glucoseFormatter.string( from: (units == .mmolL ? eventualBG.asMmolL : Decimal(eventualBG)) as NSNumber )! } @@ -172,7 +186,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { private var glucoseFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal - formatter.locale = Locale(identifier: "en_US") formatter.maximumFractionDigits = 0 if settingsManager.settings.units == .mmolL { formatter.minimumFractionDigits = 1 @@ -182,10 +195,20 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return formatter } - private var eventualFormatter: NumberFormatter { + private var iobFormatter: NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimumFractionDigits = 1 + formatter.maximumFractionDigits = 1 + formatter.roundingMode = .halfUp + return formatter + } + + private var cobFormatter: NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal - formatter.maximumFractionDigits = 2 + formatter.maximumFractionDigits = 0 + formatter.roundingMode = .halfUp return formatter } @@ -199,14 +222,9 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { } extension BaseContactTrickManager: - GlucoseObserver, SuggestionObserver, SettingsObserver { - func glucoseDidUpdate(_: [BloodGlucose]) { - renderContacts() - } - func suggestionDidUpdate(_: Suggestion) { renderContacts() } diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift index c6d987ed34..0b61059865 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickState.swift @@ -4,10 +4,11 @@ struct ContactTrickState: Codable { var glucose: String? var trend: String? var delta: String? - var glucoseDate: Date? var lastLoopDate: Date? var iob: Decimal? + var iobText: String? var cob: Decimal? + var cobText: String? var eventualBG: String? var maxIOB: Decimal = 10.0 var maxCOB: Decimal = 120.0 From 251ac200528743a699e19df6b5635e28bdc093a4 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Fri, 12 Apr 2024 17:47:18 +0200 Subject: [PATCH 12/14] revert to using `glucose` from core data (not suggestion), since we're using core data anyways --- .../Services/ContactTrick/ContactTrickManager.swift | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift index 0789ec1ead..c424bd3a11 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift @@ -57,7 +57,7 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { let suggestion: Suggestion? = storage.retrieve(OpenAPS.Enact.suggested, as: Suggestion.self) let state = ContactTrickState( - glucose: bgString(suggestion), + glucose: glucoseValues.glucose, trend: glucoseValues.trend, delta: glucoseValues.delta, lastLoopDate: suggestion?.timestamp, @@ -163,16 +163,6 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable { return (glucoseText, directionText, deltaText) } - private func bgString(_ suggestion: Suggestion?) -> String? { - guard let bg = suggestion?.bg else { - return nil - } - let units = settingsManager.settings.units - return glucoseFormatter.string( - from: (units == .mmolL ? bg.asMmolL : bg) as NSNumber - )! - } - private func eventualBGString(_ suggestion: Suggestion?) -> String? { guard let eventualBG = suggestion?.eventualBG else { return nil From ddd942333692323331273b89ab3e5f9d8e1c9048 Mon Sep 17 00:00:00 2001 From: Iurii Malchenko Date: Fri, 3 May 2024 23:56:57 +0200 Subject: [PATCH 13/14] remove the inner ring --- .../Sources/Models/ContactTrickEntry.swift | 4 ---- .../ContactTrick/ContactTrickStateModel.swift | 2 +- .../View/ContactTrickRootView.swift | 10 +------- .../ContactTrick/ContactPicture.swift | 24 ++----------------- 4 files changed, 4 insertions(+), 36 deletions(-) diff --git a/FreeAPS/Sources/Models/ContactTrickEntry.swift b/FreeAPS/Sources/Models/ContactTrickEntry.swift index f30ffe3051..c2e16925e2 100644 --- a/FreeAPS/Sources/Models/ContactTrickEntry.swift +++ b/FreeAPS/Sources/Models/ContactTrickEntry.swift @@ -3,7 +3,6 @@ struct ContactTrickEntry: JSON, Equatable { var enabled: Bool = false var layout: ContactTrickLayout = .single var ring1: ContactTrickLargeRing = .none - var ring2: ContactTrickLargeRing = .none var primary: ContactTrickValue = .glucose var top: ContactTrickValue = .none var bottom: ContactTrickValue = .none @@ -31,7 +30,6 @@ extension ContactTrickEntry { case enabled case layout case ring1 - case ring2 case primary case top case bottom @@ -51,7 +49,6 @@ extension ContactTrickEntry { let enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? false let layout = try container.decodeIfPresent(ContactTrickLayout.self, forKey: .layout) ?? .single let ring1 = try container.decodeIfPresent(ContactTrickLargeRing.self, forKey: .ring1) ?? .none - let ring2 = try container.decodeIfPresent(ContactTrickLargeRing.self, forKey: .ring2) ?? .none let primary = try container.decodeIfPresent(ContactTrickValue.self, forKey: .primary) ?? .glucose let top = try container.decodeIfPresent(ContactTrickValue.self, forKey: .top) ?? .none let bottom = try container.decodeIfPresent(ContactTrickValue.self, forKey: .bottom) ?? .none @@ -69,7 +66,6 @@ extension ContactTrickEntry { enabled: enabled, layout: layout, ring1: ring1, - ring2: ring2, primary: primary, top: top, bottom: bottom, diff --git a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift index 61c448ffa0..67d6039935 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift @@ -32,7 +32,7 @@ enum ContactTrickValue: String, JSON, CaseIterable, Identifiable, Codable { case .iob: return NSLocalizedString("IOB", comment: "") case .ring: - return NSLocalizedString("Ring", comment: "") + return NSLocalizedString("Loop status", comment: "") } } } diff --git a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift index aba248165d..d8ce5483d5 100644 --- a/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift +++ b/FreeAPS/Sources/Modules/ContactTrick/View/ContactTrickRootView.swift @@ -203,7 +203,7 @@ extension ContactTrick { } } - Section(header: Text("Rings")) { + Section(header: Text("Ring")) { Picker( selection: $entry.ring1, label: Text("Outer") @@ -212,14 +212,6 @@ extension ContactTrick { Text(v.displayName).tag(v) } } - Picker( - selection: $entry.ring2, - label: Text("Inner") - ) { - ForEach(ContactTrickLargeRing.allCases) { v in - Text(v.displayName).tag(v) - } - } Picker( selection: $entry.ringWidth, label: Text("Width") diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index 3589963c37..baa9a51e18 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -66,26 +66,7 @@ struct ContactPicture: View { drawRing(ring: contact.ring1, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth) } - if contact.ring1 != .none || contact.ring2 != .none { - rect = CGRect( - x: rect.minX + width * (ringWidth + ringGap), - y: rect.minY + height * (ringWidth + ringGap), - width: rect.width - width * (ringWidth + ringGap) * 2, - height: rect.height - height * (ringWidth + ringGap) * 2 - ) - } - - if contact.ring2 != .none { - let ringRect = CGRect( - x: rect.minX + width * ringGap, - y: rect.minY + height * ringGap, - width: rect.width - width * ringGap * 2, - height: rect.height - width * ringGap * 2 - ) - drawRing(ring: contact.ring2, contact: contact, state: state, rect: ringRect, strokeWidth: width * ringWidth) - } - - if contact.ring2 != .none { + if contact.ring1 != .none { rect = CGRect( x: rect.minX + width * (ringWidth + ringGap), y: rect.minY + height * (ringWidth + ringGap), @@ -127,7 +108,7 @@ struct ContactPicture: View { sqrt(radius * radius - (bottomHeight + primaryHeight / 2) * (bottomHeight + primaryHeight / 2)) if contact.bottom != .none, contact.top == .none, - contact.ring1 == .iob || contact.ring1 == .cob || contact.ring1 == .iobcob, contact.ring2 == .none + contact.ring1 == .iob || contact.ring1 == .cob || contact.ring1 == .iobcob { bottomWidth = bottomWidth + width * ringWidth * 2 bottomHeight = bottomHeight + height * ringWidth * 2 @@ -790,7 +771,6 @@ struct ContactPicture_Previews: PreviewProvider { contact: .constant( ContactTrickEntry( ring1: .loop, - ring2: .iob, primary: .glucose, top: .none, bottom: .none, From 67c242a7bb47b7beffc78e4b656111c84970f322 Mon Sep 17 00:00:00 2001 From: yurique Date: Wed, 8 May 2024 01:17:50 +0200 Subject: [PATCH 14/14] fixing a lint (#658) ('switch' may only be used as expression in return, throw, or as the source of an assignment) --- .../Sources/Modules/Settings/View/SettingsRootView.swift | 2 +- FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift index 51082be845..4e5852b835 100644 --- a/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift +++ b/FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift @@ -72,7 +72,7 @@ extension Settings { .navigationLink(to: .configEditor(file: OpenAPS.Middleware.determineBasal), from: self) Text("Notifications").navigationLink(to: .notificationsConfig, from: self) Text("Contact trick").navigationLink(to: .contactTrick, from: self) - Text("App Icons").navigationLink(to: .iconConfig, from: self) + Text("App Icons").navigationLink(to: .iconConfig, from: self) } header: { Text("Features") } Section { diff --git a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift index baa9a51e18..1f988fa5b2 100644 --- a/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift +++ b/FreeAPS/Sources/Services/ContactTrick/ContactPicture.swift @@ -280,7 +280,7 @@ struct ContactPicture: View { ) return } - if let text = switch value { + let text: String? = switch value { case .glucose: state.glucose case .eventualBG: state.eventualBG case .delta: state.delta @@ -289,7 +289,9 @@ struct ContactPicture: View { case .cob: state.cobText case .iob: state.iobText default: nil - } { + } + + if let text = text { drawText( text: text, rect: rect,