swift 如何检查CNContact自我的iOS应用上次将其保存在联系人商店后是否已更改?

6za6bjd0  于 2023-03-28  发布在  Swift
关注(0)|答案(1)|浏览(135)

bounty将在4天后过期。回答此问题可获得+200声望奖励。daniel希望引起更多人对此问题的关注:我没有在这个项目上工作,目前,但我想任何方式,将工作是可靠的和道德的(没有越狱)。

我想将数据存储在CNContact的联系人存储中。NSObject或CNContact是否有属性可以存储Data结构或NSDate对象的内容?我想能够跟上CNContact上次修改的时间。我还没有找到Apple专门为此提供的任何方法。我不知道我不想将修改日期保存在UserDefaults或CloudKit或Core Data或任何其他持久化数据的方式中。我不想使用CNContact的Note属性,因为它可以由用户更改。CNContact的dates示例属性是get only,我还没有找到任何方法来使用这个属性来做我想做的事情。
另一种方法是比较散列值,或者使用CNContact的.isEqual方法,或者使用===或==运算符。这可行吗?

wko9yo5t

wko9yo5t1#

似乎有一个issueCNContactStore,而enumeratorForChangeHistoryFetchRequest:error:在Objective-C中不可用。
可以将CNContactStore的示例 Package 在Objective-C类中:

ContactStoreWrapper.h
// ContactStoreWrapper.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@class CNContactStore;
@class CNChangeHistoryFetchRequest;
@class CNFetchResult;

@interface ContactStoreWrapper : NSObject
- (instancetype)initWithStore:(CNContactStore *)store NS_DESIGNATED_INITIALIZER;

- (CNFetchResult *)changeHistoryFetchResult:(CNChangeHistoryFetchRequest *)request
                                      error:(NSError *__autoreleasing  _Nullable * _Nullable)error;

@end

NS_ASSUME_NONNULL_END
ContactStoreWrapper.m
#import "ContactStoreWrapper.h"
@import Contacts;

@interface ContactStoreWrapper ()
@property (nonatomic, strong) CNContactStore *store;
@end
@implementation ContactStoreWrapper

- (instancetype)init {
    return [self initWithStore:[[CNContactStore alloc] init]];
}
- (instancetype)initWithStore:(CNContactStore *)store {
    if (self = [super init]) {
        _store = store;
    }
    return self;
}

- (CNFetchResult *)changeHistoryFetchResult:(CNChangeHistoryFetchRequest *)request
                                      error:(NSError *__autoreleasing  _Nullable * _Nullable)error {
    CNFetchResult *fetchResult = [self.store enumeratorForChangeHistoryFetchRequest:request error:error];
    return fetchResult;
}

@end

接下来,我在Swift中创建了一个小的helper类来获取历史更改:

class ContactHistoryFetcher: ObservableObject {
    @MainActor @Published private(set) var addedContactsNames: [Row] = []
    @MainActor @Published private(set) var updatedContactsNames: [Row] = []
    @MainActor @Published private(set) var deletedContactsId: [Row] = []
    
    private let savedTokenUserDefaultsKey = "CNContactChangeHistoryToken"
    private let store: CNContactStore
        
    init(store: CNContactStore = .init()) {
        self.store = store
    }
    
    @MainActor func reset() {
        UserDefaults.standard.set(nil, forKey: savedTokenUserDefaultsKey)
        addedContactsNames = []
        updatedContactsNames = []
        deletedContactsId = []
    }
    
    @MainActor func fetchChanges() async {
        let fetchHistoryRequest = CNChangeHistoryFetchRequest()
        // At first launch, the startingToken will be nil and all contacts will be retrieved as additions
        fetchHistoryRequest.startingToken = UserDefaults.standard.data(forKey: savedTokenUserDefaultsKey)
        // We only need the given name for this simple use case
        fetchHistoryRequest.additionalContactKeyDescriptors = [CNContactGivenNameKey] as [CNKeyDescriptor]
        let wrapper = ContactStoreWrapper(store: store)
        await withCheckedContinuation { continuation in
            DispatchQueue.global(qos: .userInitiated).async { [self] in
                let result = wrapper.changeHistoryFetchResult(fetchHistoryRequest, error: nil)
                // Saving the result's token as stated in CNContactStore documentation, ie:
                // https://developer.apple.com/documentation/contacts/cncontactstore/3113739-currenthistorytoken
                // When fetching contacts or change history events, use the token on CNFetchResult instead.
                UserDefaults.standard.set(result.currentHistoryToken, forKey: savedTokenUserDefaultsKey)
                guard let enumerator = result.value as? NSEnumerator else { return }
                enumerator
                    .compactMap {
                        $0 as? CNChangeHistoryEvent
                    }
                    .forEach { event in
                        Task { @MainActor in
                            switch event {
                            case let additionEvent as CNChangeHistoryAddContactEvent:
                                self.addedContactsNames.append(.init(text: additionEvent.contact.givenName))
                            case let updateEvent as CNChangeHistoryUpdateContactEvent:
                                self.updatedContactsNames.append(.init(text: updateEvent.contact.givenName))
                            case let deletionEvent as CNChangeHistoryDeleteContactEvent:
                                self.deletedContactsId.append(.init(text: deletionEvent.contactIdentifier))
                            default: break
                            }
                        }
                    }
                continuation.resume()
            }
        }
    }
}

现在我们可以在一个快速而脏的UI中显示结果

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct Row: Identifiable {
    let id = UUID()
    let text: String
}

struct ContentView: View {
    @StateObject private var fetcher = ContactHistoryFetcher()
    var body: some View {
        List {
            Section(header: Text("added")) {
                ForEach(fetcher.addedContactsNames) { row in
                    Text(row.text)
                }
            }
            Section(header: Text("updated")) {
                ForEach(fetcher.updatedContactsNames) { row in
                    Text(row.text)
                }
            }
            Section(header: Text("deleted")) {
                ForEach(fetcher.deletedContactsId) { row in
                    Text(row.text)
                }
            }
            Button("Reset") {
                fetcher.reset()
            }
            Button("Fetch") {
                Task {
                    await fetcher.fetchChanges()
                }
            }
        }
        .padding()
        .task {
            await fetcher.fetchChanges()
        }
    }
}

相关问题