ContactImageUpdater.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. // LoopFollow
  2. // ContactImageUpdater.swift
  3. // Created by Jonas Björkert on 2024-12-10.
  4. import Contacts
  5. import Foundation
  6. import UIKit
  7. class ContactImageUpdater {
  8. private let contactStore = CNContactStore()
  9. private let queue = DispatchQueue(label: "ContactImageUpdaterQueue")
  10. private var savedBackgroundUIColor: UIColor {
  11. let rawValue = Storage.shared.contactBackgroundColor.value
  12. return ContactColorOption(rawValue: rawValue)?.uiColor ?? .black
  13. }
  14. private var savedTextUIColor: UIColor {
  15. let rawValue = Storage.shared.contactTextColor.value
  16. return ContactColorOption(rawValue: rawValue)?.uiColor ?? .white
  17. }
  18. func updateContactImage(bgValue: String, trend: String, delta: String, stale: Bool) {
  19. queue.async {
  20. guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else {
  21. LogManager.shared.log(category: .contact, message: "Access to contacts is not authorized.")
  22. return
  23. }
  24. let bundleDisplayName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? "LoopFollow"
  25. for contactType in ContactType.allCases {
  26. if contactType == .Delta, Storage.shared.contactDelta.value != .separate {
  27. continue
  28. }
  29. if contactType == .Trend, Storage.shared.contactTrend.value != .separate {
  30. continue
  31. }
  32. let contactName = "\(bundleDisplayName) - \(contactType.rawValue)"
  33. guard let imageData = self.generateContactImage(bgValue: bgValue, trend: trend, delta: delta, stale: stale, contactType: contactType)?.pngData() else {
  34. LogManager.shared.log(category: .contact, message: "Failed to generate contact image for \(contactName).")
  35. continue
  36. }
  37. let predicate = CNContact.predicateForContacts(matchingName: contactName)
  38. let keysToFetch = [CNContactGivenNameKey, CNContactFamilyNameKey, CNContactImageDataKey] as [CNKeyDescriptor]
  39. do {
  40. let contacts = try self.contactStore.unifiedContacts(matching: predicate, keysToFetch: keysToFetch)
  41. if let contact = contacts.first, let mutableContact = contact.mutableCopy() as? CNMutableContact {
  42. mutableContact.imageData = imageData
  43. let saveRequest = CNSaveRequest()
  44. saveRequest.update(mutableContact)
  45. try self.contactStore.execute(saveRequest)
  46. print("Contact image updated successfully for \(contactName).")
  47. } else {
  48. let newContact = CNMutableContact()
  49. newContact.givenName = contactName
  50. newContact.imageData = imageData
  51. let saveRequest = CNSaveRequest()
  52. saveRequest.add(newContact, toContainerWithIdentifier: nil)
  53. try self.contactStore.execute(saveRequest)
  54. print("New contact created with updated image for \(contactName).")
  55. }
  56. } catch {
  57. LogManager.shared.log(category: .contact, message: "Failed to update or create contact for \(contactName): \(error)")
  58. }
  59. }
  60. }
  61. }
  62. private func generateContactImage(bgValue: String, trend: String, delta: String, stale: Bool, contactType: ContactType) -> UIImage? {
  63. let size = CGSize(width: 300, height: 300)
  64. UIGraphicsBeginImageContextWithOptions(size, false, 0)
  65. guard let context = UIGraphicsGetCurrentContext() else { return nil }
  66. savedBackgroundUIColor.setFill()
  67. context.fill(CGRect(origin: .zero, size: size))
  68. let paragraphStyle = NSMutableParagraphStyle()
  69. paragraphStyle.alignment = .center
  70. // Format extraDelta based on the user's unit preference
  71. let unitPreference = Storage.shared.units.value
  72. let yOffset: CGFloat = 48
  73. if contactType == .Trend, Storage.shared.contactTrend.value == .separate {
  74. let trendRect = CGRect(x: 0, y: 46, width: size.width, height: size.height - 80)
  75. let trendFontSize = max(40, 200 - CGFloat(trend.count * 15))
  76. let trendAttributes: [NSAttributedString.Key: Any] = [
  77. .font: UIFont.boldSystemFont(ofSize: trendFontSize),
  78. .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
  79. .paragraphStyle: paragraphStyle,
  80. ]
  81. trend.draw(in: trendRect, withAttributes: trendAttributes)
  82. } else if contactType == .Delta, Storage.shared.contactDelta.value == .separate {
  83. let deltaRect = CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
  84. let deltaFontSize = max(40, 200 - CGFloat(delta.count * 15))
  85. let deltaAttributes: [NSAttributedString.Key: Any] = [
  86. .font: UIFont.boldSystemFont(ofSize: deltaFontSize),
  87. .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
  88. .paragraphStyle: paragraphStyle,
  89. ]
  90. delta.draw(in: deltaRect, withAttributes: deltaAttributes)
  91. } else if contactType == .BG {
  92. let includesExtra = Storage.shared.contactDelta.value == .include || Storage.shared.contactTrend.value == .include
  93. let maxFontSize: CGFloat = includesExtra ? 160 : 200
  94. let fontSize = maxFontSize - CGFloat(bgValue.count * 15)
  95. var bgAttributes: [NSAttributedString.Key: Any] = [
  96. .font: UIFont.boldSystemFont(ofSize: fontSize),
  97. .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
  98. .paragraphStyle: paragraphStyle,
  99. ]
  100. if stale {
  101. // Force background color back to black if stale
  102. UIColor.black.setFill()
  103. context.fill(CGRect(origin: .zero, size: size))
  104. bgAttributes[.strikethroughStyle] = NSUnderlineStyle.single.rawValue
  105. }
  106. let bgRect: CGRect = includesExtra
  107. ? CGRect(x: 0, y: yOffset - 20, width: size.width, height: size.height / 2)
  108. : CGRect(x: 0, y: yOffset, width: size.width, height: size.height - 80)
  109. bgValue.draw(in: bgRect, withAttributes: bgAttributes)
  110. if includesExtra {
  111. let extraRect = CGRect(x: 0, y: size.height / 2 + 6, width: size.width, height: size.height / 2 - 20)
  112. let extraAttributes: [NSAttributedString.Key: Any] = [
  113. .font: UIFont.systemFont(ofSize: 90),
  114. .foregroundColor: stale ? UIColor.gray : savedTextUIColor,
  115. .paragraphStyle: paragraphStyle,
  116. ]
  117. let extra = Storage.shared.contactDelta.value == .include ? delta : trend
  118. extra.draw(in: extraRect, withAttributes: extraAttributes)
  119. }
  120. }
  121. let image = UIGraphicsGetImageFromCurrentImageContext()
  122. UIGraphicsEndImageContext()
  123. return image
  124. }
  125. }