Home telegram - deviceContactDataManagerImpl
Post
Cancel

telegram - deviceContactDataManagerImpl

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
    }
}
This post is licensed under CC BY 4.0 by the author.