浏览代码

Prevent duplicate contacts and orphaned entries

- Add contact existence validation against iOS Contacts
- Handle duplicate contacts by reusing existing ones
- Remove orphaned Core Data entries automatically
polscm32 aka Marvout 1 年之前
父节点
当前提交
1b01960461

+ 36 - 6
FreeAPS/Sources/Modules/ContactTrick/ContactTrickStateModel.swift

@@ -34,14 +34,44 @@ extension ContactTrick {
             }
         }
 
-        /// Fetches all ContactTrickEntries from Core Data.
+        /// Fetches all ContactTrickEntries and validates them against iOS Contacts.
         func fetchContactTrickEntriesAndUpdateUI() async {
-            let entries = await contactTrickStorage.fetchContactTrickEntries()
+            // 1. Get all entries from Core Data
+            let cdEntries = await contactTrickStorage.fetchContactTrickEntries()
+
+            // 2. Validate entries against iOS Contacts
+            let validatedEntries = await validateEntries(cdEntries)
+
+            // 3. Update UI with validated entries
             await MainActor.run {
-                self.contactTrickEntries = entries
+                self.contactTrickEntries = validatedEntries
             }
         }
 
+        /// Validates entries against iOS Contacts and removes invalid ones
+        private func validateEntries(_ entries: [ContactTrickEntry]) async -> [ContactTrickEntry] {
+            var validated: [ContactTrickEntry] = []
+
+            for entry in entries {
+                if let contactId = entry.contactId {
+                    // Check if contact still exists in iOS Contacts
+                    let exists = await contactTrickManager.validateContactExists(withIdentifier: contactId)
+
+                    if exists {
+                        validated.append(entry)
+                    } else {
+                        // Contact was deleted in iOS, remove from Core Data
+                        if let objectID = entry.managedObjectID {
+                            await contactTrickStorage.deleteContactTrickEntry(objectID)
+                            debugPrint("Removed orphaned contact entry: \(entry.name)")
+                        }
+                    }
+                }
+            }
+
+            return validated
+        }
+
         /// Creates a new contact in Apple Contacts and saves it to Core Data.
         /// - Parameters:
         ///   - entry: The ContactTrickEntry to be saved.
@@ -121,16 +151,16 @@ extension ContactTrick {
             await updateContactTrick(entry)
 
             // 2. Update the contact in Apple Contacts.
-            
+
             /// Update name
             let contactUpdated = await contactTrickManager
                 .updateContact(withIdentifier: contactId, newName: entry.name) // TODO: - Probably not needed anymore
-            
+
             guard contactUpdated else {
                 debugPrint("\(DebuggingIdentifiers.failed) Failed to update contact.")
                 return
             }
-            
+
             /// Update state and image
             await contactTrickManager.updateContactTrickState()
             await contactTrickManager.setImageForContact(contactId: contactId)

+ 40 - 8
FreeAPS/Sources/Services/ContactTrick/ContactTrickManager.swift

@@ -10,6 +10,7 @@ protocol ContactTrickManager {
     func updateContact(withIdentifier identifier: String, newName: String) async -> Bool
     @MainActor func updateContactTrickState() async
     func setImageForContact(contactId: String) async
+    func validateContactExists(withIdentifier identifier: String) async -> Bool
 }
 
 final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
@@ -283,11 +284,28 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
         }
     }
 
-    /// Creates a new contact in the Apple contact list.
+    /// Creates a new contact in the Apple contact list or updates an existing one with the same name.
     /// - Parameter name: The name of the contact.
-    /// - Returns: The generated `identifier` of the contact, or `nil` if an error occurs.
+    /// - Returns: The `identifier` of the created/updated contact, or `nil` if an error occurs.
     func createContact(name: String) async -> String? {
         do {
+            // First check if a contact with this name already exists
+            let predicate = CNContact.predicateForContacts(matchingName: name)
+            let existingContacts = try contactStore.unifiedContacts(
+                matching: predicate,
+                keysToFetch: [
+                    CNContactIdentifierKey as CNKeyDescriptor,
+                    CNContactGivenNameKey as CNKeyDescriptor
+                ]
+            )
+
+            // If contact exists, return its identifier
+            if let existingContact = existingContacts.first {
+                debugPrint("Found existing contact with name: \(name)")
+                return existingContact.identifier
+            }
+
+            // If no existing contact, create a new one
             let contact = CNMutableContact()
             contact.givenName = name
 
@@ -296,25 +314,39 @@ final class BaseContactTrickManager: NSObject, ContactTrickManager, Injectable {
 
             try contactStore.execute(saveRequest)
 
-            // Re-fetch the contact to retrieve its `identifier`.
-            let predicate = CNContact.predicateForContacts(matchingName: name)
-            let contacts = try contactStore.unifiedContacts(
+            // Re-fetch to get the identifier
+            let newContacts = try contactStore.unifiedContacts(
                 matching: predicate,
                 keysToFetch: [CNContactIdentifierKey as CNKeyDescriptor]
             )
 
-            guard let createdContact = contacts.first else {
-                debugPrint("Contact creation failed: No contact found after save.")
+            guard let createdContact = newContacts.first else {
+                debugPrint("\(DebuggingIdentifiers.failed) Contact creation failed: No contact found after save.")
                 return nil
             }
 
             return createdContact.identifier
         } catch {
-            print("Error creating contact: \(error)")
+            debugPrint("\(DebuggingIdentifiers.failed) Error creating/finding contact: \(error)")
             return nil
         }
     }
 
+    /// Validates if a contact still exists in iOS Contacts.
+    func validateContactExists(withIdentifier identifier: String) async -> Bool {
+        let store = CNContactStore()
+        let predicate = CNContact.predicateForContacts(withIdentifiers: [identifier])
+        let keys = [CNContactIdentifierKey] as [CNKeyDescriptor]
+
+        do {
+            let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: keys)
+            return !contacts.isEmpty
+        } catch {
+            debugPrint("\(DebuggingIdentifiers.failed) Error validating contact: \(error)")
+            return false
+        }
+    }
+
     /// Deletes a contact from the Apple contact list using its `identifier`.
     /// - Parameter identifier: The unique identifier of the contact.
     /// - Returns: `true` if the contact was successfully deleted, `false` otherwise.