CNContactStore
The object that fetches and saves contacts, groups, and containers from the user’s contacts database.
The CNContactStore
object represents the user’s contacts store database, and you use it to fetch information from that database and save changes back to it. There are a few recommended ways you can implement fetch and save requests in your app:
- Fetch only the properties that you need for contacts.
- When fetching all contacts and caching the results, first fetch all contacts identifiers, then fetch batches of detailed contacts by identifiers as required.
- To aggregate several contacts fetches, first collect a set of unique identifiers from the fetches. Then fetch batches of detailed contacts by those unique identifiers.
- If you cache the fetched contacts, groups, or containers, you need to refetch these objects (and release the old cached objects) when
CNContactStoreDidChange
is posted.
Because CNContactStore
fetch methods perform I/O, it’s recommended that you avoid using the main thread to execute fetches.
CNContainer
CNContainerType
1
2
3
4
5
6
7
8
9
case unassigned
case local
//A container for contacts only stored locally on the device. There is only one local container for a device.
case exchange
//A container for contacts stored in an Exchange folder from an Exchange server.
case cardDAV
//A container for contacts stored in an CardDAV server, such as iCloud.
An immutable object that represents a collection of contacts.
A contact can be in only one container. CardDAV accounts usually have only one container whereas Exchange accounts may have multiple containers, where each container represents an Exchange folder.
CNContainer
objects are thread-safe, and you may access their properties from any thread of your app.
CNGroup
An immutable object that represents a group of contacts.
Contacts may be members of one or more groups, depending upon their accounts.
CNGroup
objects are thread-safe, and you may access their properties from any thread of your app.
DeviceContactDataContext
1
2
3
4
5
6
7
8
private protocol DeviceContactDataContext {
func personNameDisplayOrder() -> PresentationPersonNameOrder
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData?
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData?
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData?
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)?
func deleteContactWithAppSpecificReference(peerId: PeerId)
}
DeviceContactPhoneNumberData
1
2
public let label: String
public let value: String
DeviceContactBasicData
1
2
3
public let firstName: String
public let lastName: String
public let phoneNumbers: [DeviceContactPhoneNumberData]
DeviceContactStableId
1
public typealias DeviceContactStableId = String
DeviceContactBasicDataWithReference
1
2
public let stableId: DeviceContactStableId
public let basicData: DeviceContactBasicData
DeviceContactEmailAddressData
1
2
public let label: String
public let value: String
DeviceContactUrlData
1
2
public let label: String
public let value: String
1
2
3
4
5
public extension DeviceContactUrlData {
convenience init(appProfile: PeerId) {
self.init(label: "FinanceNews", value: "\(phonebookUsernamePrefix)\(appProfile.id)")
}
}
DeviceContactAddressData
1
2
3
4
5
6
7
public let label: String
public let street1: String
public let street2: String
public let state: String
public let city: String
public let country: String
public let postcode: String
DeviceContactSocialProfileData
1
2
3
4
public let label: String
public let service: String
public let username: String
public let url: String
DeviceContactInstantMessagingProfileData
1
2
3
public let label: String
public let service: String
public let username: String
DeviceContactExtendedData
1
2
3
4
5
6
7
8
9
10
11
12
13
public let basicData: DeviceContactBasicData
public let middleName: String
public let prefix: String
public let suffix: String
public let organization: String
public let jobTitle: String
public let department: String
public let emailAddresses: [DeviceContactEmailAddressData]
public let urls: [DeviceContactUrlData]
public let addresses: [DeviceContactAddressData]
public let birthdayDate: Date?
public let socialProfiles: [DeviceContactSocialProfileData]
public let instantMessagingProfiles: [DeviceContactInstantMessagingProfileData]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
public extension DeviceContactExtendedData {
// deserialize vccard data to contact
convenience init?(vcard: Data) {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
guard let contact = (try? CNContactVCardSerialization.contacts(with: vcard))?.first else {
return nil
}
self.init(contact: contact)
} else {
return nil
}
}
// map self as a CNMutableContact
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
func asMutableCNContact() -> CNMutableContact {
let contact = CNMutableContact()
contact.givenName = self.basicData.firstName
contact.familyName = self.basicData.lastName
contact.namePrefix = self.prefix
contact.nameSuffix = self.suffix
contact.middleName = self.middleName
contact.phoneNumbers = self.basicData.phoneNumbers.map { phoneNumber -> CNLabeledValue<CNPhoneNumber> in
return CNLabeledValue<CNPhoneNumber>(label: phoneNumber.label, value: CNPhoneNumber(stringValue: phoneNumber.value))
}
contact.emailAddresses = self.emailAddresses.map { email -> CNLabeledValue<NSString> in
CNLabeledValue<NSString>(label: email.label, value: email.value as NSString)
}
contact.urlAddresses = self.urls.map { url -> CNLabeledValue<NSString> in
CNLabeledValue<NSString>(label: url.label, value: url.value as NSString)
}
contact.socialProfiles = self.socialProfiles.map({ profile -> CNLabeledValue<CNSocialProfile> in
return CNLabeledValue<CNSocialProfile>(label: profile.label, value: CNSocialProfile(urlString: nil, username: profile.username, userIdentifier: nil, service: profile.service))
})
contact.instantMessageAddresses = self.instantMessagingProfiles.map({ profile -> CNLabeledValue<CNInstantMessageAddress> in
return CNLabeledValue<CNInstantMessageAddress>(label: profile.label, value: CNInstantMessageAddress(username: profile.username, service: profile.service))
})
contact.postalAddresses = self.addresses.map({ address -> CNLabeledValue<CNPostalAddress> in
let value = CNMutablePostalAddress()
value.street = address.street1 + "\n" + address.street2
value.state = address.state
value.city = address.city
value.country = address.country
value.postalCode = address.postcode
return CNLabeledValue<CNPostalAddress>(label: address.label, value: value)
})
if let birthdayDate = self.birthdayDate {
contact.birthday = Calendar(identifier: .gregorian).dateComponents([.day, .month, .year], from: birthdayDate)
}
return contact
}
// serialize self as vccard and encoding as utf strings
func serializedVCard() -> String? {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
guard let data = try? CNContactVCardSerialization.data(with: [self.asMutableCNContact()]) else {
return nil
}
return String(data: data, encoding: .utf8)
}
return nil
}
// init from a CNContact
@available(iOSApplicationExtension 9.0, iOS 9.0, *)
convenience init(contact: CNContact) {
var phoneNumbers: [DeviceContactPhoneNumberData] = []
for number in contact.phoneNumbers {
phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue))
}
var emailAddresses: [DeviceContactEmailAddressData] = []
for email in contact.emailAddresses {
emailAddresses.append(DeviceContactEmailAddressData(label: email.label ?? "", value: email.value as String))
}
var urls: [DeviceContactUrlData] = []
for url in contact.urlAddresses {
urls.append(DeviceContactUrlData(label: url.label ?? "", value: url.value as String))
}
var addresses: [DeviceContactAddressData] = []
for address in contact.postalAddresses {
addresses.append(DeviceContactAddressData(label: address.label ?? "", street1: address.value.street, street2: "", state: address.value.state, city: address.value.city, country: address.value.country, postcode: address.value.postalCode))
}
var birthdayDate: Date?
if let birthday = contact.birthday {
if let date = birthday.date {
birthdayDate = date
}
}
var socialProfiles: [DeviceContactSocialProfileData] = []
for profile in contact.socialProfiles {
socialProfiles.append(DeviceContactSocialProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username, url: profile.value.urlString))
}
var instantMessagingProfiles: [DeviceContactInstantMessagingProfileData] = []
for profile in contact.instantMessageAddresses {
instantMessagingProfiles.append(DeviceContactInstantMessagingProfileData(label: profile.label ?? "", service: profile.value.service, username: profile.value.username))
}
let basicData = DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers)
self.init(basicData: basicData, middleName: contact.middleName, prefix: contact.namePrefix, suffix: contact.nameSuffix, organization: contact.organizationName, jobTitle: contact.jobTitle, department: contact.departmentName, emailAddresses: emailAddresses, urls: urls, addresses: addresses, birthdayDate: birthdayDate, socialProfiles: socialProfiles, instantMessagingProfiles: instantMessagingProfiles)
}
// seems like `isEmpty`
var isPrimitive: Bool {
if self.basicData.phoneNumbers.count > 1 {
return false
}
if !self.organization.isEmpty {
return false
}
if !self.jobTitle.isEmpty {
return false
}
if !self.department.isEmpty {
return false
}
if !self.emailAddresses.isEmpty {
return false
}
if !self.urls.isEmpty {
return false
}
if !self.addresses.isEmpty {
return false
}
if self.birthdayDate != nil {
return false
}
if !self.socialProfiles.isEmpty {
return false
}
if !self.instantMessagingProfiles.isEmpty {
return false
}
return true
}
}
parseAppSpecificContactReference
1
2
public let phonebookUsernamePathPrefix = "@id"
private let phonebookUsernamePrefix = "https://hotchat.im/" + phonebookUsernamePathPrefix
1
2
3
4
5
6
7
8
9
10
public func parseAppSpecificContactReference(_ value: String) -> PeerId? {
if !value.hasPrefix(phonebookUsernamePrefix) {
return nil
}
let idString = String(value[value.index(value.startIndex, offsetBy: phonebookUsernamePrefix.count)...])
if let id = Int32(idString) {
return PeerId(namespace: Namespaces.Peer.CloudUser, id: id)
}
return nil
}
DeviceContactDataModernContext
class
DeviceContactDataModernContext:
DeviceContactDataContext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
let store = CNContactStore()
var updateHandle: NSObjectProtocol?
var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:]
var currentAppSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:]
init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void, appSpecificReferencesUpdated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) {
let (contacts, references) = self.retrieveContacts()
self.currentContacts = contacts
self.currentAppSpecificReferences = references
updated(self.currentContacts)
appSpecificReferencesUpdated(self.currentAppSpecificReferences)
// listen store change
let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in
queue.async {
guard let strongSelf = self else {
return
}
// retrieve contacts
let (contacts, references) = strongSelf.retrieveContacts()
if strongSelf.currentContacts != contacts {
strongSelf.currentContacts = contacts
updated(strongSelf.currentContacts)
}
if strongSelf.currentAppSpecificReferences != references {
strongSelf.currentAppSpecificReferences = references
appSpecificReferencesUpdated(strongSelf.currentAppSpecificReferences)
}
}
})
self.updateHandle = handle
}
deinit {
if let updateHandle = updateHandle {
NotificationCenter.default.removeObserver(updateHandle)
}
}
// parse contact
private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) {
var phoneNumbers: [DeviceContactPhoneNumberData] = []
for number in contact.phoneNumbers {
phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue))
}
return (contact.identifier, DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers))
}
// retrieve contacts
private func retrieveContacts() -> ([DeviceContactStableId: DeviceContactBasicData], [PeerId: DeviceContactBasicDataWithReference]) {
let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor, CNContactUrlAddressesKey as CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
request.unifyResults = true
var result: [DeviceContactStableId: DeviceContactBasicData] = [:]
var references: [PeerId: DeviceContactBasicDataWithReference] = [:]
let _ = try? self.store.enumerateContacts(with: request, usingBlock: { contact, _ in
let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact)
result[stableIdAndContact.0] = stableIdAndContact.1
for address in contact.urlAddresses {
if address.label == "FinanceNews", let peerId = parseAppSpecificContactReference(address.value as String) {
references[peerId] = DeviceContactBasicDataWithReference(stableId: stableIdAndContact.0, basicData: stableIdAndContact.1)
}
}
})
return (result, references)
}
// name display order
func personNameDisplayOrder() -> PresentationPersonNameOrder {
switch CNContactFormatter.nameOrder(for: CNContact()) {
case .givenNameFirst:
return .firstLast
default:
return .lastFirst
}
}
// get extended contact data
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
let keysToFetch: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor,
CNContactSocialProfilesKey as CNKeyDescriptor,
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactUrlAddressesKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactDepartmentNameKey as CNKeyDescriptor
]
guard let contact = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
return nil
}
return DeviceContactExtendedData(contact: contact)
}
// append contact data
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
let keysToFetch: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor,
CNContactSocialProfilesKey as CNKeyDescriptor,
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactUrlAddressesKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactDepartmentNameKey as CNKeyDescriptor
]
guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
return nil
}
let contact = contactData.asMutableCNContact()
let mutableContact = current.mutableCopy() as! CNMutableContact
// update given name
mutableContact.givenName = contact.givenName
// update familyName
mutableContact.familyName = contact.familyName
// update phoneNumbers
var phoneNumbers = mutableContact.phoneNumbers
for phoneNumber in contact.phoneNumbers.reversed() {
var found = false
inner: for n in phoneNumbers {
if n.value.stringValue == phoneNumber.value.stringValue {
found = true
break inner
}
}
if !found {
phoneNumbers.insert(phoneNumber, at: 0)
}
}
mutableContact.phoneNumbers = phoneNumbers
// update url addresses
var urlAddresses = mutableContact.urlAddresses
for urlAddress in contact.urlAddresses.reversed() {
var found = false
inner: for n in urlAddresses {
if n.value.isEqual(urlAddress.value) {
found = true
break inner
}
}
if !found {
urlAddresses.insert(urlAddress, at: 0)
}
}
mutableContact.urlAddresses = urlAddresses
// update email addresses
var emailAddresses = mutableContact.emailAddresses
for emailAddress in contact.emailAddresses.reversed() {
var found = false
inner: for n in emailAddresses {
if n.value.isEqual(emailAddress.value) {
found = true
break inner
}
}
if !found {
emailAddresses.insert(emailAddress, at: 0)
}
}
mutableContact.emailAddresses = emailAddresses
// update postal addresses
var postalAddresses = mutableContact.postalAddresses
for postalAddress in contact.postalAddresses.reversed() {
var found = false
inner: for n in postalAddresses {
if n.value.isEqual(postalAddress.value) {
found = true
break inner
}
}
if !found {
postalAddresses.insert(postalAddress, at: 0)
}
}
mutableContact.postalAddresses = postalAddresses
// update birthday
if contact.birthday != nil {
mutableContact.birthday = contact.birthday
}
// update social profiles
var socialProfiles = mutableContact.socialProfiles
for socialProfile in contact.socialProfiles.reversed() {
var found = false
inner: for n in socialProfiles {
if n.value.username.lowercased() == socialProfile.value.username.lowercased() && n.value.service.lowercased() == socialProfile.value.service.lowercased() {
found = true
break inner
}
}
if !found {
socialProfiles.insert(socialProfile, at: 0)
}
}
mutableContact.socialProfiles = socialProfiles
// update instant message message
var instantMessageAddresses = mutableContact.instantMessageAddresses
for instantMessageAddress in contact.instantMessageAddresses.reversed() {
var found = false
inner: for n in instantMessageAddresses {
if n.value.isEqual(instantMessageAddress.value) {
found = true
break inner
}
}
if !found {
instantMessageAddresses.insert(instantMessageAddress, at: 0)
}
}
mutableContact.instantMessageAddresses = instantMessageAddresses
// save request
let saveRequest = CNSaveRequest()
saveRequest.update(mutableContact)
let _ = try? self.store.execute(saveRequest)
return DeviceContactExtendedData(contact: mutableContact)
}
// append phone number
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
let keysToFetch: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor,
CNContactSocialProfilesKey as CNKeyDescriptor,
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactUrlAddressesKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactDepartmentNameKey as CNKeyDescriptor
]
guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
return nil
}
let mutableContact = current.mutableCopy() as! CNMutableContact
var phoneNumbers = mutableContact.phoneNumbers
let appendPhoneNumbers: [CNLabeledValue<CNPhoneNumber>] = [CNLabeledValue<CNPhoneNumber>(label: phoneNumber.label, value: CNPhoneNumber(stringValue: phoneNumber.value))]
for appendPhoneNumber in appendPhoneNumbers {
var found = false
inner: for n in phoneNumbers {
if n.value.stringValue == appendPhoneNumber.value.stringValue {
found = true
break inner
}
}
if !found {
phoneNumbers.insert(appendPhoneNumber, at: 0)
}
}
mutableContact.phoneNumbers = phoneNumbers
let saveRequest = CNSaveRequest()
saveRequest.update(mutableContact)
let _ = try? self.store.execute(saveRequest)
return DeviceContactExtendedData(contact: mutableContact)
}
// create contact with extended data
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? {
let saveRequest = CNSaveRequest()
let mutableContact = contactData.asMutableCNContact()
saveRequest.add(mutableContact, toContainerWithIdentifier: nil)
let _ = try? self.store.execute(saveRequest)
return (mutableContact.identifier, contactData)
}
// delete contact with app specific reference
func deleteContactWithAppSpecificReference(peerId: PeerId) {
guard let reference = self.currentAppSpecificReferences[peerId] else {
return
}
guard let current = try? self.store.unifiedContact(withIdentifier: reference.stableId, keysToFetch: []) else {
return
}
let saveRequest = CNSaveRequest()
// delete the contact
saveRequest.delete(current.mutableCopy() as! CNMutableContact)
let _ = try? self.store.execute(saveRequest)
}
ExtendedContactDataContext
1
2
3
4
private final class ExtendedContactDataContext {
var value: DeviceContactExtendedData?
let subscribers = Bag<(DeviceContactExtendedData) -> Void>()
}
DeviceContactNormalizedPhoneNumber
struct
DeviceContactNormalizedPhoneNumber:
Hashable,
RawRepresentable
1
public let rawValue: String
BasicDataForNormalizedNumberContext
1
2
var value: [(DeviceContactStableId, DeviceContactBasicData)]
let subscribers = Bag<([(DeviceContactStableId, DeviceContactBasicData)]) -> Void>()
ImportableDeviceContactData
class
ImportableDeviceContactData:
Equatable,
PostboxCoding
1
2
public let firstName: String
public let lastName: String
DeviceContactDataManagerPrivateImpl
class
DeviceContactDataManagerPrivateImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
private let queue: Queue
private var accessInitialized = false
// data context
private var dataContext: DeviceContactDataContext?
// name display order promise
let personNameDisplayOrder = ValuePromise<PresentationPersonNameOrder>()
// stableId: extended data
private var extendedContexts: [DeviceContactStableId: ExtendedContactDataContext] = [:]
// stableId: basic contact data
private var stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData] = [:]
// normalized phone number: stableId
private var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
// peerId: basic data reference
private var appSpecificReferences: [PeerId: DeviceContactBasicDataWithReference] = [:]
// stableId: app specific reference
private var stableIdToAppSpecificReference: [DeviceContactStableId: PeerId] = [:]
// normalized phone number: importable device contact data
private var importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData] = [:]
private var accessDisposable: Disposable?
private let dataDisposable = MetaDisposable()
private let basicDataSubscribers = Bag<([DeviceContactStableId: DeviceContactBasicData]) -> Void>()
// normalized phone number: normalized number context
private var basicDataForNormalizedNumberContexts: [DeviceContactNormalizedPhoneNumber: BasicDataForNormalizedNumberContext] = [:]
// importable contacts subscribers
private let importableContactsSubscribers = Bag<([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void>()
// app specific references subscribers
private let appSpecificReferencesSubscribers = Bag<([PeerId: DeviceContactBasicDataWithReference]) -> Void>()
// init
init(queue: Queue) {
self.queue = queue
self.accessDisposable = (DeviceAccess.authorizationStatus(subject: .contacts)
|> delay(2.0, queue: .mainQueue())
|> deliverOn(self.queue)).start(next: { [weak self] authorizationStatus in
guard let strongSelf = self, authorizationStatus != .notDetermined else {
return
}
strongSelf.accessInitialized = true
if authorizationStatus == .allowed {
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
// initialize data context
let dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in
guard let strongSelf = self else {
return
}
// handle change notification
strongSelf.updateAll(stableIdToBasicContactData)
}, appSpecificReferencesUpdated: { appSpecificReferences in
guard let strongSelf = self else {
return
}
strongSelf.updateAppSpecificReferences(appSpecificReferences: appSpecificReferences)
})
strongSelf.dataContext = dataContext
strongSelf.personNameDisplayOrder.set(dataContext.personNameDisplayOrder())
} else {
let dataContext = DeviceContactDataLegacyContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in
guard let strongSelf = self else {
return
}
strongSelf.updateAll(stableIdToBasicContactData)
})
strongSelf.dataContext = dataContext
strongSelf.personNameDisplayOrder.set(dataContext.personNameDisplayOrder())
}
} else {
strongSelf.updateAll([:])
}
})
}
// update all
private func updateAll(_ stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData]) {
self.stableIdToBasicContactData = stableIdToBasicContactData
var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
for (stableId, basicData) in self.stableIdToBasicContactData {
for phoneNumber in basicData.phoneNumbers {
// normalize phone number
let normalizedPhoneNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
if normalizedPhoneNumberToStableId[normalizedPhoneNumber] == nil {
normalizedPhoneNumberToStableId[normalizedPhoneNumber] = []
}
normalizedPhoneNumberToStableId[normalizedPhoneNumber]!.append(stableId)
}
}
self.normalizedPhoneNumberToStableId = normalizedPhoneNumberToStableId
// notify basic data subscribers
for f in self.basicDataSubscribers.copyItems() {
f(self.stableIdToBasicContactData)
}
for (normalizedNumber, context) in self.basicDataForNormalizedNumberContexts {
var value: [(DeviceContactStableId, DeviceContactBasicData)] = []
if let ids = self.normalizedPhoneNumberToStableId[normalizedNumber] {
for id in ids {
if let basicData = self.stableIdToBasicContactData[id] {
value.append((id, basicData))
}
}
}
var updated = false
if value.count != context.value.count {
updated = true
} else {
for i in 0 ..< value.count {
if value[i].0 != context.value[i].0 || value[i].1 != context.value[i].1 {
updated = true
break
}
}
}
if updated {
context.value = value
for f in context.subscribers.copyItems() {
f(value)
}
}
}
// phonenumber; (stableId: basicData)
var importableContactData: [String: (DeviceContactStableId, ImportableDeviceContactData)] = [:]
for (stableId, basicData) in self.stableIdToBasicContactData {
for phoneNumber in basicData.phoneNumbers {
var replace = false
if let current = importableContactData[phoneNumber.value] {
if stableId < current.0 {// 最终选择stableId较小的
replace = true
}
} else {
replace = true
}
if replace {
importableContactData[phoneNumber.value] = (stableId, ImportableDeviceContactData(firstName: basicData.firstName, lastName: basicData.lastName))
}
}
}
// phone number: importable contact data
var importabledContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData] = [:]
for (number, data) in importableContactData {
importabledContacts[DeviceContactNormalizedPhoneNumber(rawValue: number)] = data.1
}
self.importableContacts = importabledContacts
for f in self.importableContactsSubscribers.copyItems() {
f(importableContacts)
}
}
// update app specific reference
private func updateAppSpecificReferences(appSpecificReferences: [PeerId: DeviceContactBasicDataWithReference]) {
self.appSpecificReferences = appSpecificReferences
var stableIdToAppSpecificReference: [DeviceContactStableId: PeerId] = [:]
for (peerId, value) in appSpecificReferences {
stableIdToAppSpecificReference[value.stableId] = peerId
}
// updaate
self.stableIdToAppSpecificReference = stableIdToAppSpecificReference
// notify subscribers
for f in self.appSpecificReferencesSubscribers.copyItems() {
f(appSpecificReferences)
}
}
// add listen to basic data
func basicData(updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable {
let queue = self.queue
// listen
let index = self.basicDataSubscribers.add({ data in
updated(data)
})
// notify current value
updated(self.stableIdToBasicContactData)
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
strongSelf.basicDataSubscribers.remove(index)
}
}
}
// add listen to phonenumber: BasicDataForNormalizedNumberContext ([contact])
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber, updated: @escaping ([(DeviceContactStableId, DeviceContactBasicData)]) -> Void) -> Disposable {
let queue = self.queue
let context: BasicDataForNormalizedNumberContext
if let current = self.basicDataForNormalizedNumberContexts[normalizedNumber] {
context = current
} else {
var value: [(DeviceContactStableId, DeviceContactBasicData)] = []
if let ids = self.normalizedPhoneNumberToStableId[normalizedNumber] {
for id in ids {
if let basicData = self.stableIdToBasicContactData[id] {
value.append((id, basicData))
}
}
}
context = BasicDataForNormalizedNumberContext(value: value)
self.basicDataForNormalizedNumberContexts[normalizedNumber] = context
}
// notify current value
updated(context.value)
// subscriber
let index = context.subscribers.add({ value in
updated(value)
})
return ActionDisposable { [weak self, weak context] in
queue.async {
if let strongSelf = self, let foundContext = strongSelf.basicDataForNormalizedNumberContexts[normalizedNumber], foundContext === context {
foundContext.subscribers.remove(index)
if foundContext.subscribers.isEmpty {
strongSelf.basicDataForNormalizedNumberContexts.removeValue(forKey: normalizedNumber)
}
}
}
}
}
// fetch extended data without listen
func extendedData(stableId: String, updated: @escaping (DeviceContactExtendedData?) -> Void) -> Disposable {
let current = self.dataContext?.getExtendedContactData(stableId: stableId)
updated(current)
return ActionDisposable {
}
}
// listen importable contacts
func importable(updated: @escaping ([DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]) -> Void) -> Disposable {
let queue = self.queue
let index = self.importableContactsSubscribers.add({ data in
updated(data)
})
if self.accessInitialized {
updated(self.importableContacts)
}
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
strongSelf.importableContactsSubscribers.remove(index)
}
}
}
// listen app specific references (就是url address符合app specific url)
func appSpecificReferences(updated: @escaping ([PeerId: DeviceContactBasicDataWithReference]) -> Void) -> Disposable {
let queue = self.queue
// subscriber
let index = self.appSpecificReferencesSubscribers.add({ data in
updated(data)
})
// notify current value if accessInitialized
if self.accessInitialized {
updated(self.appSpecificReferences)
}
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
strongSelf.appSpecificReferencesSubscribers.remove(index)
}
}
}
// search
func search(query: String, updated: @escaping ([DeviceContactStableId: (DeviceContactBasicData, PeerId?)]) -> Void) -> Disposable {
let normalizedQuery = query.lowercased()
var result: [DeviceContactStableId: (DeviceContactBasicData, PeerId?)] = [:]
for (stableId, basicData) in self.stableIdToBasicContactData {
// firstNam or lastName contains the query (Ignoring Case)
if basicData.firstName.lowercased().hasPrefix(normalizedQuery) || basicData.lastName.lowercased().hasPrefix(normalizedQuery) {
result[stableId] = (basicData, self.stableIdToAppSpecificReference[stableId])
}
}
updated(result)
return EmptyDisposable
}
// append contact data to stableId (update)
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) {
let result = self.dataContext?.appendContactData(contactData, to: stableId)
completion(result)
}
// append phone number to stableId
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) {
let result = self.dataContext?.appendPhoneNumber(phoneNumber, to: stableId)
completion(result)
}
// create contact
func createContactWithData(_ contactData: DeviceContactExtendedData, completion: @escaping ((DeviceContactStableId, DeviceContactExtendedData)?) -> Void) {
let result = self.dataContext?.createContactWithData(contactData)
completion(result)
}
// delete app specific reference contact
func deleteContactWithAppSpecificReference(peerId: PeerId, completion: @escaping () -> Void) {
self.dataContext?.deleteContactWithAppSpecificReference(peerId: peerId)
completion()
}
DeviceContactDataManager
protocol
DeviceContactDataManager: AnyObject
1
2
3
4
5
6
7
8
9
10
11
func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError>
func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError>
func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError>
func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>
func appSpecificReferences() -> Signal<[PeerId: DeviceContactBasicDataWithReference], NoError>
func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, PeerId?)], NoError>
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError>
func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError>
func deleteContactWithAppSpecificReference(peerId: PeerId) -> Signal<Never, NoError>
DeviceContactDataManagerImpl
class
DeviceContactDataManagerImpl: DeviceContactDataManager
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
private let queue = Queue()
private let impl: QueueLocalObject<DeviceContactDataManagerPrivateImpl>
init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return DeviceContactDataManagerPrivateImpl(queue: queue)
})
}
public func personNameDisplayOrder() -> Signal<PresentationPersonNameOrder, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.personNameDisplayOrder.get().start(next: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// basic data
public func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.basicData(updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// basic data for normalized phone number
public func basicDataForNormalizedPhoneNumber(_ normalizedNumber: DeviceContactNormalizedPhoneNumber) -> Signal<[(DeviceContactStableId, DeviceContactBasicData)], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.basicDataForNormalizedPhoneNumber(normalizedNumber, updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// extended data
public func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.extendedData(stableId: stableId, updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// importable data (phonenumber, contact)
public func importable() -> Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.importable(updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// contacts with app specific url address
public func appSpecificReferences() -> Signal<[PeerId: DeviceContactBasicDataWithReference], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.appSpecificReferences(updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
// search
public func search(query: String) -> Signal<[DeviceContactStableId: (DeviceContactBasicData, PeerId?)], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.search(query: query, updated: { value in
subscriber.putNext(value)
subscriber.putCompletion()
}))
})
return disposable
}
}
// appedn data (update)
public func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.appendContactData(contactData, to: stableId, completion: { next in
subscriber.putNext(next)
subscriber.putCompletion()
})
})
return disposable
}
}
// append or update phone number
public func appendPhoneNumber(_ phoneNumber: DeviceContactPhoneNumberData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.appendPhoneNumber(phoneNumber, to: stableId, completion: { next in
subscriber.putNext(next)
subscriber.putCompletion()
})
})
return disposable
}
}
// create contact
public func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.createContactWithData(contactData, completion: { next in
subscriber.putNext(next)
subscriber.putCompletion()
})
})
return disposable
}
}
// delete contact
public func deleteContactWithAppSpecificReference(peerId: PeerId) -> Signal<Never, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.deleteContactWithAppSpecificReference(peerId: peerId, completion: {
subscriber.putCompletion()
})
})
return disposable
}
}