Access iCloud Calendar Events - macOS App - swift

I want access the users iCloud Calendar events in an app for macOS. While researching I have found some tutorials for iOS but I couldn't find one that works on macOS. I tried to understand Apples Developer Documentation for EventKit but didn't manage to get it running.
Thats what I did:
1 - Accessing the Event Store
1.1 I have changed the 'com.apple.security.personal-information.calendars' key to YES in the entitlements file (Stack Overflow Question regarding this).
Screenshot of the .entitlement of the Project:
1.2 Afterwards I tried to request the access (in the viewDidLoad)
let eventStore = EKEventStore()
switch EKEventStore.authorizationStatus(for: .event) {
case .authorized:
print("Acess granted")
case .denied:
print("Access denied")
case .notDetermined:
eventStore.requestAccess(to: .event, completion: {
(granted, error) in
if granted {
print("granted \(granted)")
}else {
print("error \(String(describing: error))")
}
})
default:
print("Case default")
}
2 - Getting the Calendar Events
let sources = eventStore.sources
for source in sources{
print(source.title)
for calendar in source.calendars(for: .event){
print(calendar.title)
}
}
// create dates
let formatter = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd HH:mm"
let startDate = formatter.date(from: "2019/9/12 0:01")!
let endDate = formatter.date(from: "2019/9/12 23:59")!
let calendars = eventStore.calendars(for: .event)
let predicate = eventStore.predicateForEvents(withStart: startDate, end: endDate, calendars: calendars)
let events = eventStore.events(matching: predicate)
print(calendars)
print(events)
When I run this app I get the following console output:
getCalendarEvents[1970:100712] CoreData: XPC: Unable to load metadata: Error Domain=NSCocoaErrorDomain Code=134070 "An error occurred in the persistent store." UserInfo={Problem=request failed, insufficient permission}
2019-09-23 18:35:24.981947+0200 getCalendarEvents[1970:100712] [error] error: -addPersistentStoreWithType:NSXPCStore configuration:(null) URL:file:///Users/henri/Library/Calendars/Calendar%20Cache options:{
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
NSPersistentHistoryTrackingKey = {
NSPersistentHistoryTrackingEntitiesToExclude = (
ChangeRequest
);
};
agentOrDaemon = 1;
serviceName = "com.apple.CalendarAgent.database";
} ... returned error Error Domain=NSCocoaErrorDomain Code=134070 "An error occurred in the persistent store." UserInfo={Problem=request failed, insufficient permission} with userInfo dictionary {
Problem = "request failed, insufficient permission";
}
CoreData: error: -addPersistentStoreWithType:NSXPCStore configuration:(null) URL:file:///Users/henri/Library/Calendars/Calendar%20Cache options:{
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
NSPersistentHistoryTrackingKey = {
NSPersistentHistoryTrackingEntitiesToExclude = (
ChangeRequest
);
};
agentOrDaemon = 1;
serviceName = "com.apple.CalendarAgent.database";
} ... returned error Error Domain=NSCocoaErrorDomain Code=134070 "An error occurred in the persistent store." UserInfo={Problem=request failed, insufficient permission} with userInfo dictionary {
Problem = "request failed, insufficient permission";
}
[]
[]
error nil
I expected two arrays:
[EKCalendar] and [EKEvent]
I think I really need help here, I have tried a lot but I am relatively new to Swift development, could somebody please help me out?
Thank you!

The reason why you are not being able to access the EKEventStore is because you need to provide a description string as to why you want to do that. This string will be used by the MacOS to provide an explanation to the user why your app wants to have access to the user’s calendar. This string should be provided with the NSCalendarsUsageDescription key in the info.plist file of your app as described here. Even though it is often said in the documentation that is needed for iOS applications, that has also been needed for Mac applications since MacOS Mojave as described here.

Related

CloudKit was Working but Getting a Permission Error All of a Sudden

I had CloudKit working for the last couple weeks and this morning when I was trying to save a record, something I had done a number of times before, I started getting a Permission Error
Error saving to CloudKit: <CKError 0x600001f48ed0: "Permission Failure"
(10/2007); server message = "Operation not permitted"; uuid = 78FA3DD1-
EA44-4701-9A7E-8291F076DD8F; container ID = "[CloudKit Container Name]"> - Error fetching auth tokens from server:
Operation not permitted'
So I have triple checked that it's requesting to the right container, which it is (I only have two containers setup on my CloudKit so it's an easy check.
I have turned off and turned CloudKit on again in XCode as an attempt to force reset it (not sure if that did anything helpful but figured I would give it a shot)
In case this is needed, this is how I am saving to CloudKit (had no problem with this bit of code in the past.
func createRecord(title: String, type: String, comment: String) {
let audioRecord = CKRecord(recordType: "Audio")
audioRecord["title"] = title as CKRecordValue
audioRecord["type"] = type as CKRecordValue
audioRecord["comment"] = comment as CKRecordValue
let audioURL = audioRecorder.getAudioURL()
let audioAsset = CKAsset(fileURL: audioURL)
audioRecord["audio"] = audioAsset
DispatchQueue.main.async {
CKContainer.default().publicCloudDatabase.save(audioRecord) { [self] record, error in
//print(CKContainer.default())
if let error = error {
print("Error saving to CloudKit: \(error.self) - \(error.localizedDescription)")
} else {
print("Record has been successfully saved to CloudKit")
}
}
}
}

CNContactStore execute fails

I am working out how to use the Contacts framework, however some fairly simple code to create a contact is failing with an unexpected result. This is my code:
let Store = CNContactStore()
Store.requestAccess(for: .contacts, completionHandler:{ success, error in
if success {
let Contact = CNMutableContact()
Contact.givenName = "Dave"
Contact.familyName = "Nottage"
let SaveRequest = CNSaveRequest()
SaveRequest.add(Contact, toContainerWithIdentifier: nil)
do {
try Store.execute(SaveRequest)
print("Success")
}
catch let error as NSError {
print(error.localizedDescription)
}
} else {
print("No access")
}
})
..and this is the result:
2019-02-22 10:30:56.050344+1030 ContactsTest[30329:25254955] [default] Unable to load Info.plist exceptions (eGPUOverrides)
2019-02-22 10:30:57.973724+1030 ContactsTest[30329:25254955] Could not get real path for Address Book lock folder: open() for F_GETPATH failed.
2019-02-22 10:30:57.973954+1030 ContactsTest[30329:25254955] Unable to open file lock: <ABProcessSharedLock: 0x600001752ac0: name=(null), lockFilePath=(null), localLock=<NSRecursiveLock: 0x600002914a80>{recursion count = 0, name = nil}, fileDescriptor=-1> Error Domain=NSPOSIXErrorDomain Code=14 "Bad address" UserInfo={ABFileDescriptor=-1}
The operation couldn’t be completed. (Foundation._GenericObjCError error 0.)
Any ideas on what might be causing this?
Edit: Note also that this is being compiled for macOS 10.14 SDK, and is running on macOS 10.14.3
The answer is to check the Contacts checkbox of App Data in the App Sandbox section in Capabilities and turn on the switch for App Sandbox.
Make sure you added key NSContactsUsageDescription in Info.plist.
Please refer to link.
Important
An iOS app linked on or after iOS 10.0 must include in its Info.plist
file the usage description keys for the types of data it needs to
access or it will crash. To access Contacts data specifically, it must
include NSContactsUsageDescription.

CloudKit Error: Change Token Expired, Reset Needed

Swift 3.1, Xcode 8.3.3
I keep getting an error from CloudKit and I don't know what to do about it.
I'm tracking notifications from CloudKit like this:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: previousChangeToken)
//Hold the notification IDs we processed so we can tell CloudKit to never send them to us again
var notificationIDs = [CKNotificationID]()
operation.notificationChangedBlock = { [weak self] notification in
guard let notification = notification as? CKQueryNotification else { return }
if let id = notification.notificationID {
notificationIDs.append(id)
}
}
operation.fetchNotificationChangesCompletionBlock = { [weak self] newToken, error in
if let error = error{
print(error) //<-- <!> This is the error <!>
}else{
self?.previousChangeToken = newToken
//All records are in, now save the data locally
let fetchOperation = CKFetchRecordsOperation(recordIDs: recordIDs)
fetchOperation.fetchRecordsCompletionBlock = { [weak self] records, error in
if let e = error {
print("fetchRecordsCompletionBlock Error fetching: \(e)")
}
//Save records to local persistence...
}
self?.privateDB.add(fetchOperation)
//Tell CloudKit we've read the notifications
let operationRead = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDs)
self?.container.add(operationRead)
}
}
container.add(operation)
And the error says:
<CKError 0x174241e90: "Change Token Expired" (21/1016); server message
= "Error code: RESET_NEEDED"; uuid = ...; container ID = "...">
The CKServerChangeToken documentation don't mention anything about resetting the token, and the CloudKit dashboard doesn't offer any such option.
Any idea what I'm supposed to do?
This error code is CKErrorCodeChangeTokenExpired, and it's an indication that you need to re-sync your changes.
https://developer.apple.com/documentation/cloudkit/ckerror/2325216-changetokenexpired
This error code gets returned when the change token is too old, or the container has been reset (resetting the container invalidates old change tokens).
The comments related to this error code include:
(Describing the code itself):
The previousServerChangeToken value is too old and the client must re-sync from scratch
(On various fetch operation completion/updated blocks):
If the server returns a CKErrorChangeTokenExpired error, the serverChangeToken used for this record zone when initting this operation was too old and the client should toss its local cache and re-fetch the changes in this record zone starting with a nil serverChangeToken.

CKContainer.discoverAllIdentities always fails

The CKContainer.discoverAllIdentities request always fails in my CloudKit app. It has continually failed over the course of several days.
A simplified version of the code that is failing (which results in the same error) is:
private func getContacts(completion: (([CKUserIdentity]?) -> Void)?) {
container.status(forApplicationPermission: .userDiscoverability) { [weak self] status, error in
if let error = error {
print(error)
}
switch status {
case .granted:
self?.discover(completion: completion)
default:
print("status not granted")
}
}
}
private func discover(completion: (([CKUserIdentity]?) -> Void)?) {
let op = CKDiscoverAllUserIdentitiesOperation()
op.qualityOfService = .userInitiated
op.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print(error)
}
}
op.userIdentityDiscoveredBlock = { identity in
print(identity)
}
op.start()
}
It results in an error being passed to the op.discoverAllUserIdentitiesCompletionBlock. The description of the error in the log is:
<CKError 0x1c4a51a60: "Server Rejected Request" (15/2000); server message = "Internal server error"; uuid = F67453B9-712D-4E5E-9335-929123E3C978; container ID = "iCloud.com.huntermaximillionmonk.topdraw">
Previously, this operation would work, but only for certain iCloud users. Now it's not for both of my test users.
Problem:
This was a problem in iOS 11.0
Based on my testing:
This works ok in Xcode 9.2 / iOS 11.2.1 on the device (not simulator)
After resetting the simulator works for the first time, doesn't work subsequently, however on the device it works repeatedly.
Code:
let queue = OperationQueue()
func requestPermissions(for permissions: CKApplicationPermissions,
completionHandler: #escaping (CKApplicationPermissionStatus, Error?) -> ()) {
CKContainer.default().requestApplicationPermission(permissions) { status, error in
if let error = error {
print("Error for requesting \(permissions) - \(error)")
}
let statusMessage : String
switch status {
case .granted:
statusMessage = "Granted"
case .denied:
statusMessage = "Denied"
case .couldNotComplete:
statusMessage = "Could not complete"
case .initialState:
statusMessage = "Initial state"
}
print("Permission - \(statusMessage)")
completionHandler(status, error)
}
}
private func discoverAllUsers() {
let operation = CKDiscoverAllUserIdentitiesOperation()
operation.userIdentityDiscoveredBlock = { userIdentity in
print("userIdentity = \(userIdentity)")
}
operation.discoverAllUserIdentitiesCompletionBlock = { error in
if let error = error {
print("Discover all users Error: \(error) ")
}
else {
print("Discover all users completed successfully")
}
}
queue.addOperation(operation)
}
Edit:
Apple fixed this issue day after this answer was posted, coincidence?! I don't think so :)
This is not actually the answer to the question, but a fix that helped me to cross over this error. It will require you to change your app UI interaction and add ContactsUI framework to your project, moreover your user will be responsible for selecting a contact with iCloud related email.
Good news is that the method discoverUserIdentity is still works. So, you can use it to get CKUserIdentity from manually selected contact.
func addContact(_ contact:CNContact) {
var lookUpEmails = [CKUserIdentityLookupInfo]()
for email in contact.emailAddresses {
lookUpEmails.append(CKUserIdentityLookupInfo(emailAddress: (email.value as String)))
}
let checkUserOperation = CKDiscoverUserIdentitiesOperation()
checkUserOperation.userIdentityLookupInfos = lookUpEmails
checkUserOperation.userIdentityDiscoveredBlock = { [unowned self] (identity, info) -> Void in
if identity.hasiCloudAccount {
if let recordID = identity.userRecordID {
//do something with discovered user
}
checkUserOperation.cancel()
}
}
checkUserOperation.queuePriority = Operation.QueuePriority.high
CKContainer.default().add(checkUserOperation)
}
It might sound useless, but in my case, it helped me to solve the Server Rejected Request" (15/2000) error, to fix one of the features of my app and continue to use the other feature related code with less efforts than I thought.
I hope someone will find this helpful.
Just another data point on this that might help with the overall picture. I was still seeing this error on 11.2.5 when I used my own iCloud AppleID (with hundreds of contacts) while running a Test App that called discoverAllIdentitiesWithCompletionHandler. I'd get the dreaded
CKError 0x1c0051730: "Server Rejected Request" (15/2000); server message = "Internal server error".
When I switched to run the exact same code on my daughters iOS11.2.5 device (with just a handful of contacts) the code worked fine.
Leads me to believe there is some rate limiting going on when there are a lot of contacts with iOS11.
(P.S. No errors at all running on iOS10)

Problems accessing Calendar using EKEventStore on OSX Sierra with Swift 3

This appears very simple, but I've been struggling for several days to get access to the Calendar on OSX. I have switched on the App Sandbox capability, and I've ticked the "Calendar" box in App Data. I have created very simple app with the following view controller class:
import Cocoa
import EventKit
class ViewController: NSViewController {
var eventControl = EKEventStore()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
}
As you can see, the only lines of code I've added are to import EventKit and to initialise eventControl.
When I run this in debug, I get an error at the eventControl initialisation line
2016-10-28 15:02:00.056521 calendarTest[4105:847101] CoreData: XPC: Unable to load metadata: Error Domain=NSCocoaErrorDomain Code=134070 "An error occurred in the persistent store." UserInfo={Problem=request failed, insufficient permission}
2016-10-28 15:02:00.057742 calendarTest[4105:847101] [error] error: -addPersistentStoreWithType:NSXPCStore configuration:(null) URL:file:///Users/patrickramsden/Library/Calendars/Calendar%20Cache options:{
NSInferMappingModelAutomaticallyOption = 1;
NSMigratePersistentStoresAutomaticallyOption = 1;
agentOrDaemon = 1;
serviceName = "com.apple.CalendarAgent.database";
} ... returned error Error Domain=NSCocoaErrorDomain Code=134070 "An error occurred in the persistent store." UserInfo={Problem=request failed, insufficient permission} with userInfo dictionary {
Problem = "request failed, insufficient permission";
}
I can't work out how to get the right permissions.
I am using Xcode 8.1 and macOS Sierra 10.12.1
You need to add a usage description to your info.plist. This is a small description of why you need access to the services that is presented to the user.
<key>NSCalendarsUsageDescription</key>
<string>Description of why you need access to the Calendar</string>
Has anyone else figured this out?
I am running into the same issue, however trying to access anything in the event Store returns nil. Even though "access is granted". Such as:
let eventStore = EKEventStore()
switch EKEventStore.authorizationStatus(for: .event) {
case .authorized:
print("Access Granted")
break
case .denied:
print("Access denied")
case .notDetermined:
eventStore.requestAccess(to: .event, completion:
{(granted: Bool, error: Error?) -> Void in
if granted {
print("Access Granted")
} else {
print("Access denied")
}
})
break
default:
print("Case Default")
break
}
print(eventStore.calendars(for: .event))
Getting the same messages: How to prevent EventStore access error on first run
However the access is working correctly once you start the application again after the user grants permission! My app is not sandboxed. Have you set the "Privacy - Calendars Usage Description" key in your plist?