IOS9 Contacts framework fails to update linked contact - ios

In AddressBook on device I have a record linked with Facebook contact record.
I fetch it into CNContact with CNContactFetchRequest with:
contactFetchRequest.mutableObjects = true
contactFetchRequest.unifyResults = false
After getting, I modify it, then I trying to update it with:
let store = CNContactStore()
let saveRequest = CNSaveRequest()
if contact != nil {
mutableContact = contact!.mutableCopy() as! CNMutableContact
saveRequest.updateContact( mutableContact )
} else {
mutableContact = CNMutableContact()
saveRequest.addContact( mutableContact, toContainerWithIdentifier:nil )
}
// Modify mutableContact
mutableContact.jobTitle = "Worker";
do {
// Will fails with error
try store.executeSaveRequest(saveRequest)
} catch let error as NSError {
BCRLog(error)
self.isFailed = true
} catch {
self.isFailed = true
}
On execute executeSaveRequest, I caught an error:
NSError with domain:CNErrorDomain, code:500 (witch is
CNErrorCodePolicyViolation), _userInfo: {"NSUnderlyingError" :
{"ABAddressBookErrorDomain" - code 0}} witch is
kABOperationNotPermittedByStoreError
The question: Is it possible to modify linked contact (not unified), and if it is, what i do wrong?
If I modifying not linked contact - all OK!

I have this error when the Contacts app is configured to store contacts in an Exchange account. When I choose an iCloud account as a default it immediately saves a contact well. I can check what is set on your device in Settings -> Contacts -> Default Account

Related

How do I update a contact with a contact unpacked from a vCard Data object if I don't know the value of the identifier of the contact to update?

I wrote code to pack a single contact into a vCard Data object, and then unpack it, then add a family name where there was not one before, and then save the unpacked contact back to the same container and add it to the same group the original contact was in. The result when I look in the Contacts iOS app is that the unpacked contact I saved shows as a different contact as the original contact when I fetch the unified contacts in that group. I was expecting a unified contact that combined the two contacts.
I have seen vCard data update a contact in my iPhone. How do they do that? The identifier is changed when a vCard Data object is unpacked. There is no way to know if the existing contact in the iPhone is the same contact in the vCard Data object by looking at the identifier. If I examine all the properties of the unified contact in the iPhone, what if the properties on the iPhone have been changed since that contact was saved in the contact store? If I do find the contact in the contact store that is the same as the unpacked contact, when I save the unpacked contact in the contact store, it will be a whole new contact and when enumerateContacts is called next time with unifyResults set to true, I believe the new contact would not be unified with the original contact, as my code has proven.
Is the only solution to enumerateContacts for all the unified contacts in the contact store and compare all the properties and take the unified contact that has the most properties equal in value to the same properties in the unpacked contact, and update that unified contact with the values of the properties in the unpacked contact?
do {
print("contact.identifier: \(contact.identifier)")
var data = Data()
try data = CNContactVCardSerialization.data(with: [contact])
let contacts = try CNContactVCardSerialization.contacts(with: data)
let unpackedContact = contacts.first!
print("unpackedContact.identifier: \(unpackedContact.identifier)")
if contact.identifier == unpackedContact.identifier {
print("identifier didn't change when unpacking from vCard data")
} else {
print("identifier changed when unpacking from vCard data")
}
let mutableCopyOfUnpackedContact: CNMutableContact = unpackedContact.mutableCopy() as! CNMutableContact
mutableCopyOfUnpackedContact.familyName = "Lee"
let saveContactRequest = CNSaveRequest()
saveContactRequest.add(mutableCopyOfUnpackedContact, toContainerWithIdentifier: contactStore.defaultContainerIdentifier())
saveContactRequest.addMember(mutableCopyOfUnpackedContact, to: group)
try contactStore.execute(saveContactRequest)
} catch {
print(error.localizedDescription)
}
Debug window:
contact.identifier: 82A9D82C-7B97-402B-82AC-D1D2D7036C3A:ABPerson
unpackedContact.identifier: 06FB07FB-17F7-473C-BB38-F34A260948C4:ABPerson
identifier changed when unpacking from vCard data
do {
let predicateGroups = CNGroup.predicateForGroupsInContainer(withIdentifier: contactStore.defaultContainerIdentifier())
let groups = try contactStore.groups(matching: predicateGroups)
var groupFamily: CNGroup! = nil
for group in groups {
if group.name == "Family" {
groupFamily = group
}
}
let predicateContactsInGroup = CNContact.predicateForContactsInGroup(withIdentifier: groupFamily.identifier)
let contactFetchRequest = CNContactFetchRequest(keysToFetch: keysToFetch)
contactFetchRequest.predicate = predicateContactsInGroup
contactFetchRequest.unifyResults = true
do {
try contactStore.enumerateContacts(with: contactFetchRequest) {
contact, stop in
print("contact givenName: \(contact.givenName) familyName: \(contact.familyName)")
} // try contactStore.enumerateContacts
} catch {
print(error.localizedDescription)
}
} catch {
print(error.localizedDescription)
}
This results in a totally new contact identical to the original contact, except the new contact has the last name assinged it, which is "Lee".

Accessing another app's CloudKit database?

Suppose John developed App A and Heather developed App B. They each have different Apple Developer's accounts and they are not on the same team or associated in any way. App B is backed by a public CloudKit database. Is there any way for App A to write to App B's public CloudKit database? Namely, can App A do this:
let DB = CKContainer(identifier: "iCloud.com.Heather.AppB").publicCloudDatabase
and then write to or read from this DB?
I'm guessing that this is not allowed out of the box, but is there a way to set up authentication so that this is possible?
This looks/sounds like the solution you seek.
CloudKit share Data between different iCloud accounts but not with everyone as outlined by https://stackoverflow.com/users/1878264/edwin-vermeer an iCloud specialist on SO.
There is third party explaination on this link too. https://medium.com/#kwylez/cloudkit-sharing-series-intro-4fc82dad7a9
Key steps shamelessly cut'n'pasted ... make sure you read and credit Cory on medium.com!
// Add an Info.plist key for CloudKit Sharing
<key>CKSharingSupported</key>
<true/>
More code...
CKContainer.default().discoverUserIdentity(withPhoneNumber: phone, completionHandler: {identity, error in
guard let userIdentity: CKUserIdentity = identity, error == nil else {
DispatchQueue.main.async(execute: {
print("fetch user by phone error " + error!.localizedDescription)
})
return
}
DispatchQueue.main.async(execute: {
print("user identity was discovered \(identity)")
})
})
/// Create a shred the root record
let recordZone: CKRecordZone = CKRecordZone(zoneName: "FriendZone")
let rootRecord: CKRecord = CKRecord(recordType: "Note", zoneID: recordZone.zoneID)
// Create a CloudKit share record
let share = CKShare(rootRecord: rootRecord)
share[CKShareTitleKey] = "Shopping List” as CKRecordValue
share[CKShareThumbnailImageDataKey] = shoppingListThumbnail as CKRecordValue
share[CKShareTypeKey] = "com.yourcompany.name" as CKRecordValue
/// Setup the participants for the share (take the CKUserIdentityLookupInfo from the identity you fetched)
let fetchParticipantsOperation: CKFetchShareParticipantsOperation = CKFetchShareParticipantsOperation(userIdentityLookupInfos: [userIdentity])
fetchParticipantsOperation.fetchShareParticipantsCompletionBlock = {error in
if let error = error {
print("error for completion" + error!.localizedDescription)
}
}
fetchParticipantsOperation.shareParticipantFetchedBlock = {participant in
print("participant \(participant)")
/// 1
participant.permission = .readWrite
/// 2
share.addParticipant(participant)
let modifyOperation: CKModifyRecordsOperation = CKModifyRecordsOperation(recordsToSave: [rootRecord, share], recordIDsToDelete: nil)
modifyOperation.savePolicy = .ifServerRecordUnchanged
modifyOperation.perRecordCompletionBlock = {record, error in
print("record completion \(record) and \(error)")
}
modifyOperation.modifyRecordsCompletionBlock = {records, recordIDs, error in
guard let ckrecords: [CKRecord] = records, let record: CKRecord = ckrecords.first, error == nil else {
print("error in modifying the records " + error!.localizedDescription)
return
}
/// 3
print("share url \(url)")
}
CKContainer.default().privateDB.add(modifyOperation)
}
CKContainer.default().add(fetchParticipantsOperation)
And on the other side of the fence.
let acceptShareOperation: CKAcceptSharesOperation = CKAcceptSharesOperation(shareMetadatas: [shareMeta])
acceptShareOperation.qualityOfService = .userInteractive
acceptShareOperation.perShareCompletionBlock = {meta, share, error in
Log.print("meta \(meta) share \(share) error \(error)")
}
acceptShareOperation.acceptSharesCompletionBlock = {error in
Log.print("error in accept share completion \(error)")
/// Send your user to wear that need to go in your app
}
CKContainer.default().container.add(acceptShareOperation)
Really I cannot hope to do the article justice, go read it... its in three parts!
If the apps were in the same organization, there is a way to set up shared access. But as you described the situation, it is not possible.

iOS9 Contacts Framework get identifier from newly saved contact

I need the identifier of a newly created contact directly after the save request. The use case: Within my app a user creates a new contact and give them some attributes (eg. name, address ...) after that he can save the contact. This scenario is working as aspected. My code looks like this:
func createContact(uiContact: Contact, withImage image:UIImage?, completion: String -> Void)
{
let contactToSave = uiContact.mapToCNContact(CNContact()) as! Cnmutablecontawctlet
if let newImage = image
{
contactToSave.imageData = UIImageJPEGRepresentation(newImage, 1.0)
}
request = CNSaveRequest()
request.addContact(contactToSave, toContainerWithIdentifier: nil)
do
{
try self.contactStore.executeSaveRequest(request)
print("Successfully saved the CNContact")
completion(contactToSave.identifier)
}
catch let error
{
print("CNContact saving faild: \(error)")
completion(nil)
}
}
The Contact Object (uiContact) is just an wrapper of CNContact.
In the closure completion I need to return the identifier but on this time I have no access to them, because he is creating by the system after the write process.
One solution could be to fetch the newly saved CNContact with predicate
public func unifiedContactsMatchingPredicate(predicate: NSPredicate, keysToFetch keys: [CNKeyDescriptor]) throws -> [CNContact]
but this seems to me like a bit unclean because this contact could have only a name and more than one could exist. Something like a callback with the created identifier would be nice. But there isn´t.
Is there a other way to solve this problem?
This may be a little late but in case someone needs this.
By using the latest SDK (iOS 11), I was able to get the identifier just by:
NSError *error = nil;
saveReq = [[CNSaveRequest alloc] init];
[saveReq addContact:cnContact toContainerWithIdentifier:containerIdentifier];
if (![contactStore executeSaveRequest:saveReq error:&error]) {
NSLog(#"Failed to save, error: %#", error.localizedDescription);
}
else
{
if ([cnContact isKeyAvailable:CNContactIdentifierKey]) {
NSLog(#"identifier for new contact is: %#", cnContact.identifier);
// this works for me everytime
} else {
NSLog(#"CNContact identifier still isn't available after saving to address book");
}
}
swift 4
This is the way to get contact id when creating contact
do {
try store.execute(saveRequest)
if contactToAdd.isKeyAvailable(CNContactIdentifierKey) {
print(contactToAdd.identifier) // here you are getting identifire
}
}
catch {
print(error)
}

How to retrieve emails from iPhone Contacts using swift?

I am new to swift . I used APAdressBook Framework for retrieving contacts in iPhone .Every thing working fine up to IOS 8.4 but when checked in IOS 9 of Simulator and iPhone devices it is not accessing contacts and even it not showing alert message like would you like to access contacts any one can face this type of problem if yes please give me your valuable answer?
Apple Inc. introduce Contacts.framework from iOS 9. So, it would be better use this framework to retrieve contacts.
Here is way to fetch email or whole contact from Contacts app.
Add Contacts.framework to your project.
Create or add new file of type header and give name like yourProjectName-Bridging-Header.h write #import <Contacts/Contacts.h> statement into file and save and set appropriate path of this file from build setting.
Now create method
func getAllContacts() {
let status = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts) as CNAuthorizationStatus
if status == CNAuthorizationStatus.Denied {
let alert = UIAlertController(title:nil, message:"This app previously was refused permissions to contacts; Please go to settings and grant permission to this app so it can use contacts", preferredStyle:UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title:"OK", style:UIAlertActionStyle.Default, handler:nil))
self.presentViewController(alert, animated:true, completion:nil)
return
}
let store = CNContactStore()
store.requestAccessForEntityType(CNEntityType.Contacts) { (granted:Bool, error:NSError?) -> Void in
if !granted {
dispatch_async(dispatch_get_main_queue(), { () -> Void in
// user didn't grant access;
// so, again, tell user here why app needs permissions in order to do it's job;
// this is dispatched to the main queue because this request could be running on background thread
})
return
}
let arrContacts = NSMutableArray() // Declare this array globally, so you can access it in whole class.
let request = CNContactFetchRequest(keysToFetch:[CNContactIdentifierKey, CNContactEmailAddressesKey, CNContactBirthdayKey, CNContactImageDataKey, CNContactPhoneNumbersKey, CNContactFormatter.descriptorForRequiredKeysForStyle(CNContactFormatterStyle.FullName)])
do {
try store.enumerateContactsWithFetchRequest(request, usingBlock: { (contact:CNContact, stop:UnsafeMutablePointer<ObjCBool>) -> Void in
let arrEmail = contact.emailAddresses as NSArray
if arrEmail.count > 0 {
let dict = NSMutableDictionary()
dict.setValue((contact.givenName+" "+contact.familyName), forKey: "name")
let emails = NSMutableArray()
for index in 0...arrEmail.count {
let email:CNLabeledValue = arrEmail.objectAtIndex(index) as! CNLabeledValue
emails .addObject(email.value as! String)
}
dict.setValue(emails, forKey: "email")
arrContacts.addObject(dict) // Either retrieve only those contact who have email and store only name and email
}
arrContacts.addObject(contact) // either store all contact with all detail and simplifies later on
})
} catch {
return;
}
}
}
Call this method where you want self.getAllContacts()
And when you want to retrieve
for var index = 0; index < self.arrContacts.count; ++index {
let dict = self.arrContacts[index] as! NSDictionary
print(dict.valueForKey("name"))
print(dict.valueForKey("email"))
}

SSKeychain: Accounts not stored in iCloud?

I'm using sskeychain (https://github.com/soffes/sskeychain) to store my accounts and passwords in the IOS keychain. I assume, that if I store an account, it should be available on my other device. But it doesn't appear there.
I read my accounts with this code:
NSArray *arr=[SSKeychain accountsForService:#"Login"];
for (NSString *s in arr) {
NSLog(#"Account: %#",s);
}
and get this (only shown one entry, the others are similar):
Account: {
acct = "friXXXXXter#XXXX.com";
agrp = "3B4384Z34A.de.gondomir.LocalButler";
cdat = "2014-05-09 22:55:08 +0000";
mdat = "2014-05-09 22:55:08 +0000";
pdmn = ak;
svce = Login;
sync = 0;
tomb = 0;
}
But this doesn't appear on the other device. Both devices have IOS 7.1.1.
I store the password with this line:
[SSKeychain setPassword:self.passwortField.text forService:#"Login" account:self.userField.text];
I have switched on keychain sharing in Xcode and have a keychain group "de.gondomir.LocalButler" listed there.
Am I missing something? Must the service name something special?
Thanks!
In case this is still relevant for you I managed to find the solution. (works for >=iOS7)
Don't use the static methods of SSKeychain to write your credentials. Instead use SSKeychainQuery and set the synchronizationMode to SSKeychainQuerySynchronizationModeYes like this
NSError *error;
[SSKeychain setAccessibilityType:self.keychainAccessibilityType];
SSKeychainQuery *query = [[SSKeychainQuery alloc] init];
query.service = service;
query.account = account;
query.password = password;
query.synchronizationMode = SSKeychainQuerySynchronizationModeYes;
[query save:&error];
if (error) {
NSLog(#"Error writing credentials %#", [error description]);
}
The static convenience methods on SSKeychain use the default synchronization mode SSKeychainQuerySynchronizationModeAny causing credentials not to be synchronized with the iCloud keychain.
Additionally, make sure your devices have Keychain via iCloud enabled (Settings>iCloud>Keychain). You might also want to enable Keychain Sharing in your targets Capabilities.
After I have a new project I tried the answer of MarkHim, it works.
I used swift now, so here is my working code:
let account = defaults.objectForKey("Sync_toPhoneNumber") as? String
SSKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlock)
var error:NSError?
let lookupQuery = SSKeychainQuery()
lookupQuery.synchronizationMode = .Yes
lookupQuery.service = "DasDing"
lookupQuery.account = account
let password = SSKeychain.passwordForService("DasDing", account: account, error: &error)
if error == nil {
commandKey = password!
} else {
print("Error für \(account): \(error!.localizedDescription)")
commandKey = ""
}
// query all accounts for later use
let allQuery = SSKeychainQuery()
allQuery.service = "DasDing"
do {
let dict = try allQuery.fetchAll()
print("Accounts:")
for acc in dict {
print(acc["acct"]!)
}
} catch let error as NSError {
print("keine Accounts")
print("Error: \(error.localizedDescription)")
}
That's for reading, for writing you must delete the account first (if you want to change the password):
let account = defaults.objectForKey("Sync_toPhoneNumber") as? String
SSKeychain.setAccessibilityType(kSecAttrAccessibleAfterFirstUnlock)
SSKeychain.deletePasswordForService("DasDing", account: account)
let newQuery = SSKeychainQuery()
newQuery.service = "DasDing"
newQuery.account = account
newQuery.password = str?.uppercaseString
newQuery.synchronizationMode = .Yes
try! newQuery.save()

Resources