| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- // LoopFollow
- // ContactImageUpdater.swift
- import Contacts
- import Foundation
- import UIKit
- class ContactImageUpdater {
- private let contactStore = CNContactStore()
- private let queue = DispatchQueue(label: "ContactImageUpdaterQueue")
- private var savedBackgroundUIColor: UIColor {
- let rawValue = Storage.shared.contactBackgroundColor.value
- return ContactColorOption(rawValue: rawValue)?.uiColor ?? .black
- }
- private var savedTextUIColor: UIColor {
- let rawValue = Storage.shared.contactTextColor.value
- return ContactColorOption(rawValue: rawValue)?.uiColor ?? .white
- }
- private func textColor(for contactType: ContactType) -> UIColor {
- guard contactType == .BG else { return savedTextUIColor }
- let colorMode = Storage.shared.contactColorMode.value
- // Use raw BG value in mg/dL (same units as highLine/lowLine)
- let bgNumeric = Double(Observable.shared.bg.value ?? 0)
- return colorMode.textColor(for: bgNumeric, staticColor: savedTextUIColor)
- }
- func updateContactImage(bgValue: String, trend: String, delta: String, iob: String, stale: Bool) {
- queue.async {
- guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
- LogManager.shared.log(category: .contact, message: "Access to contacts is not authorized.")
- return
- }
- let bundleDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "LoopFollow"
- for contactType in ContactType.allCases {
- if contactType == .Delta, Storage.shared.contactDelta.value != .separate {
- continue
- }
- if contactType == .Trend, Storage.shared.contactTrend.value != .separate {
- continue
- }
- if contactType == .IOB, Storage.shared.contactIOB.value != .separate {
- continue
- }
- let contactName = "\(bundleDisplayName) - \(contactType.rawValue)"
- let includedFields = self.getIncludedFields(for: contactType)
- let dynamicTextColor = self.textColor(for: contactType)
- guard let imageData = self.generateContactImage(bgValue: bgValue, trend: trend, delta: delta, iob: iob, stale: stale, contactType: contactType, includedFields: includedFields, textColor: dynamicTextColor)?.pngData() else {
- LogManager.shared.log(category: .contact, message: "Failed to generate contact image for \(contactName).")
- continue
- }
- do {
- let keysToFetch = [
- CNContactIdentifierKey as CNKeyDescriptor,
- CNContactGivenNameKey as CNKeyDescriptor,
- CNContactImageDataKey as CNKeyDescriptor,
- ]
- var allMatchingContacts: [CNContact] = []
- let containers = try self.contactStore.containers(matching: nil)
- // Run fast check first
- let namePredicate = CNContact.predicateForContacts(matchingName: contactName)
- let nameContacts = try self.contactStore.unifiedContacts(matching: namePredicate, keysToFetch: keysToFetch)
- let matchingNameContacts = nameContacts.filter { $0.givenName == contactName }
- allMatchingContacts.append(contentsOf: matchingNameContacts)
- // If it fails, make heavy iteration by containers
- if allMatchingContacts.isEmpty {
- for container in containers {
- let containerPredicate = CNContact.predicateForContactsInContainer(withIdentifier: container.identifier)
- let containerContacts = try self.contactStore.unifiedContacts(matching: containerPredicate, keysToFetch: keysToFetch)
- let matchingContacts = containerContacts.filter { $0.givenName == contactName }
- for contact in matchingContacts {
- if !allMatchingContacts.contains(where: { $0.identifier == contact.identifier }) {
- allMatchingContacts.append(contact)
- }
- }
- }
- }
- let saveRequest = CNSaveRequest()
- if let existingContact = allMatchingContacts.first {
- if let mutableContact = existingContact.mutableCopy() as? CNMutableContact {
- mutableContact.imageData = imageData
- saveRequest.update(mutableContact)
- try self.contactStore.execute(saveRequest)
- LogManager.shared.log(category: .contact, message: "Contact image updated successfully for \(contactName).")
- }
- } else {
- // Use default container
- let defaultContainer = self.contactStore.defaultContainerIdentifier()
- let newContact = CNMutableContact()
- newContact.givenName = contactName
- newContact.imageData = imageData
- saveRequest.add(newContact, toContainerWithIdentifier: defaultContainer)
- try self.contactStore.execute(saveRequest)
- LogManager.shared.log(category: .contact, message: "New contact created with updated image for \(contactName).")
- }
- } catch {
- LogManager.shared.log(category: .contact, message: "Failed to update or create contact for \(contactName): \(error)")
- }
- }
- }
- }
- private func getIncludedFields(for contactType: ContactType) -> [ContactType] {
- var included: [ContactType] = []
- if Storage.shared.contactTrend.value == .include,
- Storage.shared.contactTrendTarget.value == contactType
- {
- included.append(.Trend)
- }
- if Storage.shared.contactDelta.value == .include,
- Storage.shared.contactDeltaTarget.value == contactType
- {
- included.append(.Delta)
- }
- if Storage.shared.contactIOB.value == .include,
- Storage.shared.contactIOBTarget.value == contactType
- {
- included.append(.IOB)
- }
- return included
- }
- private func generateContactImage(bgValue: String, trend: String, delta: String, iob: String, stale: Bool, contactType: ContactType, includedFields: [ContactType], textColor: UIColor) -> 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))
- let paragraphStyle = NSMutableParagraphStyle()
- paragraphStyle.alignment = .center
- let yOffset: CGFloat = 48
- // Get the primary value for this contact type
- let primaryValue: String
- switch contactType {
- case .BG: primaryValue = bgValue
- case .Trend: primaryValue = trend
- case .Delta: primaryValue = delta
- case .IOB: primaryValue = iob
- }
- // Build extra values from included fields
- var extraValues: [String] = []
- for field in includedFields {
- switch field {
- case .Trend: extraValues.append(trend)
- case .Delta: extraValues.append(delta)
- case .IOB: extraValues.append(iob)
- case .BG: break
- }
- }
- let hasExtras = !extraValues.isEmpty
- // Determine font sizes based on number of extras
- let maxFontSize: CGFloat = extraValues.count >= 2 ? 140 : (hasExtras ? 160 : 200)
- let extraFontSize: CGFloat = extraValues.count >= 2 ? 60 : 90
- let fontSize = max(40, maxFontSize - CGFloat(primaryValue.count * 15))
- let isBGStale = stale && contactType == .BG
- var primaryAttributes: [NSAttributedString.Key: Any] = [
- .font: UIFont.boldSystemFont(ofSize: fontSize),
- .foregroundColor: isBGStale ? UIColor.gray : textColor,
- .paragraphStyle: paragraphStyle,
- ]
- if isBGStale {
- UIColor.black.setFill()
- context.fill(CGRect(origin: .zero, size: size))
- primaryAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
- }
- let primaryRect: CGRect = hasExtras
- ? 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)
- primaryValue.draw(in: primaryRect, withAttributes: primaryAttributes)
- if hasExtras {
- let extraString = extraValues.joined(separator: " ")
- 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: extraFontSize),
- .foregroundColor: isBGStale ? UIColor.gray : textColor,
- .paragraphStyle: paragraphStyle,
- ]
- extraString.draw(in: extraRect, withAttributes: extraAttributes)
- }
- let image = UIGraphicsGetImageFromCurrentImageContext()
- UIGraphicsEndImageContext()
- return image
- }
- }
|