Jonas Björkert 1 год назад
Родитель
Сommit
d98af8fcc2

+ 12 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -88,6 +88,9 @@
 		DD9ED0CA2D355257000D2A63 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0C92D355256000D2A63 /* LogView.swift */; };
 		DD9ED0CC2D35526E000D2A63 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */; };
 		DD9ED0CE2D35587A000D2A63 /* LogEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ED0CD2D355879000D2A63 /* LogEntry.swift */; };
+		DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */; };
+		DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */; };
+		DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */; };
 		DDAD162F2D2EF9830084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */; };
 		DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */; };
 		DDB0AF552BB1B24A00AFA48B /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = DDB0AF542BB1B24A00AFA48B /* BuildDetails.plist */; };
@@ -362,6 +365,9 @@
 		DD9ED0C92D355256000D2A63 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
 		DD9ED0CB2D35526E000D2A63 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = "<group>"; };
 		DD9ED0CD2D355879000D2A63 /* LogEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogEntry.swift; sourceTree = "<group>"; };
+		DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactColorOption.swift; sourceTree = "<group>"; };
+		DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactIncludeOption.swift; sourceTree = "<group>"; };
+		DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactType.swift; sourceTree = "<group>"; };
 		DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkHeartbeatBluetoothDevice.swift; sourceTree = "<group>"; };
 		DDB0AF502BB1A84500AFA48B /* capture-build-details.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "capture-build-details.sh"; sourceTree = "<group>"; };
 		DDB0AF512BB1A8BE00AFA48B /* BuildDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuildDetails.swift; sourceTree = "<group>"; };
@@ -706,6 +712,9 @@
 		DD50C74D2D0828250057AE6F /* Contact */ = {
 			isa = PBXGroup;
 			children = (
+				DDA9ACAB2D6B316F00E6F1A9 /* ContactType.swift */,
+				DDA9ACA92D6A6B8200E6F1A9 /* ContactIncludeOption.swift */,
+				DDA9ACA72D6A66DD00E6F1A9 /* ContactColorOption.swift */,
 				DD50C7512D0828B40057AE6F /* Settings */,
 				DD50C7542D0862770057AE6F /* ContactImageUpdater.swift */,
 			);
@@ -1437,6 +1446,7 @@
 				FC16A97B249966A3003D6245 /* AlarmSound.swift in Sources */,
 				DDBE3ABD2CB5A961006B37DC /* OverrideView.swift in Sources */,
 				DDB0AF522BB1A8BE00AFA48B /* BuildDetails.swift in Sources */,
+				DDA9ACAA2D6A6B8300E6F1A9 /* ContactIncludeOption.swift in Sources */,
 				DD0C0C622C4175FD00DBADDF /* NSProfile.swift in Sources */,
 				DD58171E2D299FCA0041FB98 /* BluetoothDeviceDelegate.swift in Sources */,
 				DDE69ED22C7256260013EAEC /* RemoteType.swift in Sources */,
@@ -1456,6 +1466,7 @@
 				FCFEECA02488157B00402A7F /* Chart.swift in Sources */,
 				DDCF979424C0D380002C9752 /* UIViewExtension.swift in Sources */,
 				DD0C0C6D2C48606200DBADDF /* CarbMetric.swift in Sources */,
+				DDA9ACA82D6A66E200E6F1A9 /* ContactColorOption.swift in Sources */,
 				DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */,
 				FCEF87AC24A141A700AE6FA0 /* Localizer.swift in Sources */,
 				FC1BDD3224A2585C001B652C /* DataStructs.swift in Sources */,
@@ -1518,6 +1529,7 @@
 				DD4878032C7B297E0048F05C /* StorageValue.swift in Sources */,
 				DD4878192C7C56D60048F05C /* TrioNightscoutRemoteController.swift in Sources */,
 				FC1BDD2B24A22650001B652C /* Stats.swift in Sources */,
+				DDA9ACAC2D6B317100E6F1A9 /* ContactType.swift in Sources */,
 				DDDF6F432D479A9900884336 /* LoopNightscoutRemoteView.swift in Sources */,
 				DDD10F052C529DA200D76A8E /* ObservableValue.swift in Sources */,
 				FC1BDD2D24A23204001B652C /* StatsView.swift in Sources */,

+ 27 - 0
LoopFollow/Contact/ContactColorOption.swift

@@ -0,0 +1,27 @@
+//
+//  ContactColorOption.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-02-22.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import UIKit
+
+enum ContactColorOption: String, CaseIterable {
+    case red, blue, cyan, green, yellow, orange, purple, white, black
+
+    var uiColor: UIColor {
+        switch self {
+        case .red:    return .red
+        case .blue:   return .blue
+        case .cyan:   return .cyan
+        case .green:  return .green
+        case .yellow: return .yellow
+        case .orange: return .orange
+        case .purple: return .purple
+        case .white:  return .white
+        case .black:  return .black
+        }
+    }
+}

+ 100 - 120
LoopFollow/Contact/ContactImageUpdater.swift

@@ -14,61 +14,47 @@ class ContactImageUpdater {
     private let contactStore = CNContactStore()
     private let queue = DispatchQueue(label: "ContactImageUpdaterQueue")
 
-    //convert the saved strings to UI Color
-        private var savedBackgroundUIColor: UIColor {
-            switch ObservableUserDefaults.shared.contactBackgroundColor.value {
-            case "red": return .red
-            case "blue": return .blue
-            case "cyan": return .cyan
-            case "green": return .green
-            case "yellow": return .yellow
-            case "orange": return .orange
-            case "purple": return .purple
-            case "white": return .white
-            case "black": return .black    
-            default: return .black
-            }
-        }
-    
-        private var savedTextUIColor: UIColor {
-            switch ObservableUserDefaults.shared.contactTextColor.value {
-            case "red": return .red
-            case "blue": return .blue
-            case "cyan": return .cyan
-            case "green": return .green
-            case "yellow": return .yellow
-            case "orange": return .orange
-            case "purple": return .purple
-            case "white": return .white
-            case "black": return .black
-            default: return .white
-            }
-        }
+    private var savedBackgroundUIColor: UIColor {
+        let rawValue = Storage.shared.contactBackgroundColor.value
+        return ContactColorOption(rawValue: rawValue)?.uiColor ?? .black
+    }
 
-        func updateContactImage(bgValue: String, extra: String, extraTrend: String, extraDelta: String, stale: Bool) {
-        let contactSuffixes = ["BG", "Trend", "Delta"]
+    private var savedTextUIColor: UIColor {
+        let rawValue = Storage.shared.contactTextColor.value
+        return ContactColorOption(rawValue: rawValue)?.uiColor ?? .white
+    }
+
+    func updateContactImage(bgValue: String, trend: String, delta: String, stale: Bool) {
         queue.async {
             guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
-                print("Access to contacts is not authorized.")
+                LogManager.shared.log(category: .contact, message: "Access to contacts is not authorized.")
                 return
             }
-    
+
             let bundleDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "LoopFollow"
-    
-            for suffix in contactSuffixes {
-                let contactName = "\(bundleDisplayName) - \(suffix)"
-                let contactType = suffix
-                guard let imageData = self.generateContactImage(bgValue: bgValue, extra: extra, extraTrend: extraTrend, extraDelta: extraDelta, stale: stale, contactType: contactType)?.pngData() else {
-                    print("Failed to generate contact image for \(contactName).")
+
+            for contactType in ContactType.allCases {
+                if contactType == .Delta && Storage.shared.contactDelta.value != .separate {
                     continue
                 }
-    
+
+                if contactType == .Trend && Storage.shared.contactTrend.value != .separate {
+                    continue
+                }
+
+                let contactName = "\(bundleDisplayName) - \(contactType.rawValue)"
+
+                guard let imageData = self.generateContactImage(bgValue: bgValue, trend: trend, delta: delta, stale: stale, contactType: contactType)?.pngData() else {
+                    LogManager.shared.log(category: .contact, message: "Failed to generate contact image for \(contactName).")
+                    continue
+                }
+
                 let predicate = CNContact.predicateForContacts(matchingName: contactName)
                 let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactImageDataKey] as [CNKeyDescriptor]
-    
+
                 do {
                     let contacts = try self.contactStore.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
-    
+
                     if let contact = contacts.first, let mutableContact = contact.mutableCopy() as? CNMutableContact {
                         mutableContact.imageData = imageData
                         let saveRequest = CNSaveRequest()
@@ -85,93 +71,87 @@ class ContactImageUpdater {
                         print("New contact created with updated image for \(contactName).")
                     }
                 } catch {
-                    print("Failed to update or create contact for \(contactName): \(error)")
+                    LogManager.shared.log(category: .contact, message: "Failed to update or create contact for \(contactName): \(error)")
                 }
             }
         }
     }
 
-    private func generateContactImage(bgValue: String, extra: String, extraTrend: String, extraDelta: String, stale: Bool, contactType: String) -> UIImage? {
-    let size = CGSize(width: 300, height: 300)
-    UIGraphicsBeginImageContextWithOptions(size, false, 0)
-    guard let context = UIGraphicsGetCurrentContext() else { return nil }
+    private func generateContactImage(bgValue: String, trend: String, delta: String, stale: Bool, contactType: ContactType) -> UIImage? {
+        let size = CGSize(width: 300, height: 300)
+        UIGraphicsBeginImageContextWithOptions(size, false, 0)
+        guard let context = UIGraphicsGetCurrentContext() else { return nil }
 
-    savedBackgroundUIColor.setFill()
-    context.fill(CGRect(origin: .zero, size: size))
+        savedBackgroundUIColor.setFill()
+        context.fill(CGRect(origin: .zero, size: size))
 
-    let paragraphStyle = NSMutableParagraphStyle()
-    paragraphStyle.alignment = .center
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .center
 
-    let maxFontSize: CGFloat = extra.isEmpty ? 200 : 160
-    let fontSize = maxFontSize - CGFloat(bgValue.count * 15)
-    var bgAttributes: [NSAttributedString.Key: Any] = [
-        .font: UIFont.boldSystemFont(ofSize: fontSize),
-        .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
-        .paragraphStyle: paragraphStyle
-    ]
+        // Format extraDelta based on the user's unit preference
+        let unitPreference = UserDefaultsRepository.units.value
+        let yOffset: CGFloat = 48
+        if contactType == .Trend && Storage.shared.contactTrend.value == .separate {
+            let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80)
+            let trendFontSize = max(40, 200 - CGFloat(trend.count * 15))
 
-    if stale {
-        // Force background color back to black if stale
-        UIColor.black.setFill()
-        context.fill(CGRect(origin: .zero, size: size))
-        bgAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
-    }
+            let trendAttributes: [NSAttributedString.Key: Any] = [
+                .font: UIFont.boldSystemFont(ofSize: trendFontSize),
+                .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
+                .paragraphStyle: paragraphStyle
+            ]
 
-    let extraAttributes: [NSAttributedString.Key: Any] = [
-        .font: UIFont.systemFont(ofSize: 90),
-        .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
-        .paragraphStyle: paragraphStyle
-    ]
-
-    let trendFontSize = max(40, 200 - CGFloat(extraTrend.count * 15))
-    let deltaFontSize = max(40, 200 - CGFloat(extraDelta.count * 15))
-    
-    let trendAttributes: [NSAttributedString.Key: Any] = [
-        .font: UIFont.boldSystemFont(ofSize: trendFontSize),
-        .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
-        .paragraphStyle: paragraphStyle
-    ]
-    
-    let deltaAttributes: [NSAttributedString.Key: Any] = [
-        .font: UIFont.boldSystemFont(ofSize: deltaFontSize),
-        .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
-        .paragraphStyle: paragraphStyle
-    ]
-    
-    // Format extraDelta based on the user's unit preference
-    let unitPreference = UserDefaultsRepository.units.value
-    let formattedExtraDelta: String
-    let yOffset: CGFloat = unitPreference == "mg/dL" ? 46 : 61 // Add 15 if mmol is selected
-    if unitPreference == "mg/dL" {
-        formattedExtraDelta = String(format: "%.0f", (extraDelta as NSString).doubleValue)
-    } else {
-        formattedExtraDelta = String(format: "%.1f", (extraDelta as NSString).doubleValue)
-    }
-    
-    if contactType == "Trend" && ObservableUserDefaults.shared.contactTrend.value == "Separate" {
-        // Customizing image for Trend contact when value is Separate
-        let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80)
-        extraTrend.draw(in: trendRect, withAttributes: trendAttributes)
-    } else if contactType == "Delta" && ObservableUserDefaults.shared.contactDelta.value == "Separate" {
-        // Customizing image for Delta contact when value is Separate
-        let deltaRect = CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
-        formattedExtraDelta.draw(in: deltaRect, withAttributes: deltaAttributes)
-    } else if contactType == "BG" {
-        // Customizing image for BG contact
-        let bgRect = extra.isEmpty
-            ? CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
-            : CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2)
-
-        bgValue.draw(in: bgRect, withAttributes: bgAttributes)
-
-        if !extra.isEmpty {
-            let extraRect = CGRect(x: 0, y: size.height / 2 + 6, width: size.width, height: size.height / 2 - 20)
-            extra.draw(in: extraRect, withAttributes: extraAttributes)
+            trend.draw(in: trendRect, withAttributes: trendAttributes)
+        } else if contactType == .Delta && Storage.shared.contactDelta.value == .separate {
+            let deltaRect = CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
+            let deltaFontSize = max(40, 200 - CGFloat(delta.count * 15))
+
+            let deltaAttributes: [NSAttributedString.Key: Any] = [
+                .font: UIFont.boldSystemFont(ofSize: deltaFontSize),
+                .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
+                .paragraphStyle: paragraphStyle
+            ]
+
+            delta.draw(in: deltaRect, withAttributes: deltaAttributes)
+        } else if contactType == .BG {
+            let includesExtra = Storage.shared.contactDelta.value == .include || Storage.shared.contactTrend.value == .include
+
+            let maxFontSize: CGFloat = includesExtra ? 160 : 200
+            let fontSize = maxFontSize - CGFloat(bgValue.count * 15)
+            var bgAttributes: [NSAttributedString.Key: Any] = [
+                .font: UIFont.boldSystemFont(ofSize: fontSize),
+                .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
+                .paragraphStyle: paragraphStyle
+            ]
+
+            if stale {
+                // Force background color back to black if stale
+                UIColor.black.setFill()
+                context.fill(CGRect(origin: .zero, size: size))
+                bgAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
+            }
+
+            let bgRect: CGRect = includesExtra
+            ? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2)
+            : CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
+
+            bgValue.draw(in: bgRect, withAttributes: bgAttributes)
+
+            if includesExtra {
+                let extraRect = CGRect(x: 0, y: size.height / 2 + 6, width: size.width, height: size.height / 2 - 20)
+                let extraAttributes: [NSAttributedString.Key: Any] = [
+                    .font: UIFont.systemFont(ofSize: 90),
+                    .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
+                    .paragraphStyle: paragraphStyle
+                ]
+
+                let extra = Storage.shared.contactDelta.value == .include ? delta : trend
+                extra.draw(in: extraRect, withAttributes: extraAttributes)
+            }
         }
-    }
 
-    let image = UIGraphicsGetImageFromCurrentImageContext()
-    UIGraphicsEndImageContext()
-    return image
-}
+        let image = UIGraphicsGetImageFromCurrentImageContext()
+        UIGraphicsEndImageContext()
+        return image
+    }
 }

+ 13 - 0
LoopFollow/Contact/ContactIncludeOption.swift

@@ -0,0 +1,13 @@
+//
+//  ContactIncludeOption.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-02-22.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+enum ContactIncludeOption: String, Codable, Equatable, CaseIterable {
+    case off = "Off"
+    case include = "Include"
+    case separate = "Separate"
+}

+ 13 - 0
LoopFollow/Contact/ContactType.swift

@@ -0,0 +1,13 @@
+//
+//  ContactSuffix.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-02-23.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+enum ContactType: String, CaseIterable {
+    case BG = "BG"
+    case Trend = "Trend"
+    case Delta = "Delta"
+}

+ 28 - 53
LoopFollow/Contact/Settings/ContactSettingsView.swift

@@ -17,8 +17,6 @@ struct ContactSettingsView: View {
     @State private var alertTitle: String = ""
     @State private var alertMessage: String = ""
 
-    let options = ["Off", "Include", "Separate"]
-
     var body: some View {
         NavigationView {
             Form {
@@ -34,78 +32,55 @@ struct ContactSettingsView: View {
                             if isEnabled {
                                 requestContactAccess()
                             }
-                        }  
+                        }
                 }
 
-                Section(header: Text("Color Options")) {
-                    Text("Select the colors for your BG values.  Note: not all watch faces allow control over colors. Recommend options like Activity or Modular Duo if you want to customize colors.")
-                        .font(.footnote)
-                        .foregroundColor(.secondary)
-                        .padding(.vertical, 4)
-                    
-                    Picker("Select Background Color", selection: $viewModel.contactBackgroundColor) {
-                            Text("Red").tag("red")
-                            Text("Blue").tag("blue")
-                            Text("Cyan").tag("cyan")
-                            Text("Green").tag("green")
-                            Text("Yellow").tag("yellow")
-                            Text("Orange").tag("orange")
-                            Text("Purple").tag("purple")
-                            Text("White").tag("white")
-                            Text("Black").tag("black")
+                if viewModel.contactEnabled {
+                    Section(header: Text("Color Options")) {
+                        Text("Select the colors for your BG values.  Note: not all watch faces allow control over colors. Recommend options like Activity or Modular Duo if you want to customize colors.")
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .padding(.vertical, 4)
+
+                        Picker("Select Background Color", selection: $viewModel.contactBackgroundColor) {
+                            ForEach(ContactColorOption.allCases, id: \.rawValue) { option in
+                                Text(option.rawValue.capitalized).tag(option.rawValue)
+                            }
+                        }
+
+                        Picker("Select Text Color", selection: $viewModel.contactTextColor) {
+                            ForEach(ContactColorOption.allCases, id: \.rawValue) { option in
+                                Text(option.rawValue.capitalized).tag(option.rawValue)
+                            }
                         }
-                        
-                    Picker("Select Text Color", selection: $viewModel.contactTextColor) {
-                        Text("Red").tag("red")
-                        Text("Blue").tag("blue")
-                        Text("Cyan").tag("cyan")
-                        Text("Green").tag("green")
-                        Text("Yellow").tag("yellow")
-                        Text("Orange").tag("orange")
-                        Text("Purple").tag("purple")
-                        Text("White").tag("white")
-                        Text("Black").tag("black")
                     }
-                }
-                    
-                if viewModel.contactEnabled {
+
                     Section(header: Text("Additional Information")) {
                         Text("To see your trend or delta, include one in the original '\(viewModel.contactName)' contact, or create separate contacts ending in '- Trend' and '- Delta' for up to three contacts on your watch face.")
-                        .font(.footnote)
-                        .foregroundColor(.secondary)
-                        .padding(.vertical, 4)
-                        
+                            .font(.footnote)
+                            .foregroundColor(.secondary)
+                            .padding(.vertical, 4)
+
                         Text("Show Trend")
                             .font(.subheadline)
                         Picker("Show Trend", selection: $viewModel.contactTrend) {
-                            ForEach(options, id: \.self) { option in
-                                Text(option).tag(option)
+                            ForEach(ContactIncludeOption.allCases, id: \.self) { option in
+                                Text(option.rawValue).tag(option)
                             }
                         }
                         .pickerStyle(SegmentedPickerStyle())
-                
+
                         Text("Show Delta")
                             .font(.subheadline)
                         Picker("Show Delta", selection: $viewModel.contactDelta) {
-                            ForEach(options, id: \.self) { option in
-                                Text(option).tag(option)
+                            ForEach(ContactIncludeOption.allCases, id: \.self) { option in
+                                Text(option.rawValue).tag(option)
                             }
                         }
                         .pickerStyle(SegmentedPickerStyle())
                     }
                 }
             }
-            .onChange(of: viewModel.contactTrend) { newValue in
-                if newValue == "Include" && viewModel.contactDelta == "Include" {
-                    viewModel.contactDelta = "Off"
-                }
-            }
-            .onChange(of: viewModel.contactDelta) { newValue in
-                if newValue == "Include" && viewModel.contactTrend == "Include" {
-                    viewModel.contactTrend = "Off"
-                }
-            }
-            
             .navigationBarTitle("Contact Settings", displayMode: .inline)
             .toolbar {
                 ToolbarItem(placement: .navigationBarTrailing) {

+ 21 - 21
LoopFollow/Contact/Settings/ContactSettingsViewModel.swift

@@ -22,41 +22,41 @@ class ContactSettingsViewModel: ObservableObject {
 
     @Published var contactEnabled: Bool {
         didSet {
-            storage.contactEnabled.value = contactEnabled
+            Storage.shared.contactEnabled.value = contactEnabled
             triggerRefresh()
         }
     }
 
-    @Published var contactTrend: String {
+    @Published var contactTrend: ContactIncludeOption {
         didSet {
-            if contactTrend == "Include" && contactDelta == "Include" {
-                contactDelta = "Off"
+            if contactTrend == .include && contactDelta == .include {
+                contactDelta = .off
             }
-            storage.contactTrend.value = contactTrend
+            Storage.shared.contactTrend.value = contactTrend
             triggerRefresh()
         }
     }
 
-    @Published var contactDelta: String {
+    @Published var contactDelta: ContactIncludeOption {
         didSet {
-            if contactDelta == "Include" && contactTrend == "Include" {
-                contactTrend = "Off"
+            if contactDelta == .include && contactTrend == .include {
+                contactTrend = .off
             }
-            storage.contactDelta.value = contactDelta
+            Storage.shared.contactDelta.value = contactDelta
             triggerRefresh()
         }
     }
 
     @Published var contactBackgroundColor: String {
         didSet {
-            storage.contactBackgroundColor.value = contactBackgroundColor
+            Storage.shared.contactBackgroundColor.value = contactBackgroundColor
             triggerRefresh()
         }
     }
     
     @Published var contactTextColor: String {
         didSet {
-            storage.contactTextColor.value = contactTextColor
+            Storage.shared.contactTextColor.value = contactTextColor
             triggerRefresh()
         }
     }
@@ -65,25 +65,25 @@ class ContactSettingsViewModel: ObservableObject {
     private var cancellables = Set<AnyCancellable>()
 
     init() {
-        self.contactEnabled = storage.contactEnabled.value
-        self.contactTrend = storage.contactTrend.value
-        self.contactDelta = storage.contactDelta.value
-        self.contactBackgroundColor = storage.contactBackgroundColor.value
-        self.contactTextColor = storage.contactTextColor.value
+        self.contactEnabled = Storage.shared.contactEnabled.value
+        self.contactTrend = Storage.shared.contactTrend.value
+        self.contactDelta = Storage.shared.contactDelta.value
+        self.contactBackgroundColor = Storage.shared.contactBackgroundColor.value
+        self.contactTextColor = Storage.shared.contactTextColor.value
 
-        storage.contactEnabled.$value
+        Storage.shared.contactEnabled.$value
             .assign(to: &$contactEnabled)
 
-        storage.contactTrend.$value
+        Storage.shared.contactTrend.$value
             .assign(to: &$contactTrend)
 
-        storage.contactDelta.$value
+        Storage.shared.contactDelta.$value
             .assign(to: &$contactDelta)
 
-        storage.contactBackgroundColor.$value
+        Storage.shared.contactBackgroundColor.$value
             .assign(to: &$contactBackgroundColor)
         
-        storage.contactTextColor.$value
+        Storage.shared.contactTextColor.$value
             .assign(to: &$contactTextColor)
     }
 

+ 2 - 19
LoopFollow/Controllers/Nightscout/BGData.swift

@@ -267,25 +267,8 @@ extension MainViewController {
             snoozer.DeltaLabel.text = snoozerDelta
 
             // Update contact
-            if ObservableUserDefaults.shared.contactEnabled.value {
-                var extra: String = ""
-                var extraTrend: String = ""
-                var extraDelta: String = ""
-
-                if ObservableUserDefaults.shared.contactTrend.value == "Include" {
-                    extra = snoozerDirection
-                } else if ObservableUserDefaults.shared.contactDelta.value == "Include" {
-                    extra = snoozerDelta
-                }
-
-                if ObservableUserDefaults.shared.contactTrend.value == "Separate" {
-                    extraTrend = snoozerDirection
-                }
-                if ObservableUserDefaults.shared.contactDelta.value == "Separate" {
-                    extraDelta = snoozerDelta
-                }
-
-                self.contactImageUpdater.updateContactImage(bgValue: bgTextStr, extra: extra, extraTrend: extraTrend, extraDelta: extraDelta, stale: deltaTime >= 12)
+            if Storage.shared.contactEnabled.value {
+                self.contactImageUpdater.updateContactImage(bgValue: bgTextStr, trend: snoozerDirection, delta: snoozerDelta, stale: deltaTime >= 12)
             }
         }
     }

+ 0 - 7
LoopFollow/Storage/ObservableUserDefaults.swift

@@ -17,12 +17,5 @@ class ObservableUserDefaults {
     var nsWriteAuth = ObservableUserDefaultsValue<Bool>(key: "nsWriteAuth", default: false)
     var nsAdminAuth = ObservableUserDefaultsValue<Bool>(key: "nsAdminAuth", default: false)
 
-    var contactEnabled = ObservableUserDefaultsValue<Bool>(key: "contactEnabled", default: false)
-    var contactTrend = ObservableUserDefaultsValue<String>(key: "contactTrend", default: "Off")
-    var contactDelta = ObservableUserDefaultsValue<String>(key: "contactDelta", default: "Off")
-    var contactBackgroundColor = ObservableUserDefaultsValue<String>(key: "contactBackgroundColor", default: "black")
-    var contactTextColor = ObservableUserDefaultsValue<String>(key: "contactTextColor", default: "white")
-    
-
     private init() {}
 }

+ 5 - 1
LoopFollow/Storage/Storage.swift

@@ -37,7 +37,11 @@ class Storage {
 
     var debugLogLevel = StorageValue<Bool>(key: "debugLogLevel", defaultValue: false)
 
-
+    var contactTrend = StorageValue<ContactIncludeOption>(key: "contactTrend", defaultValue: .off)
+    var contactDelta = StorageValue<ContactIncludeOption>(key: "contactDelta", defaultValue: .off)
+    var contactEnabled = StorageValue<Bool>(key: "contactEnabled", defaultValue: false)
+    var contactBackgroundColor = StorageValue<String>(key: "contactBackgroundColor", defaultValue: ContactColorOption.black.rawValue)
+    var contactTextColor = StorageValue<String>(key: "contactTextColor", defaultValue: ContactColorOption.white.rawValue)
     static let shared = Storage()
 
     private init() { }