Can't create calendar on iPhone but can on the simulator - ios

I would like to create a custom calendar in which to create all the app related events. I would like to this calendar to be of EKSourceType Local.
I can create such a calendar on the simulator and add events to it but when I try to add it on my iPhone it does't work.
My code to create the calendar:
if (eventManager.eventStore?.calendarWithIdentifier(NSUserDefaults.standardUserDefaults().objectForKey("calendar_identifier") as? String) == nil){
//The calendar doesn't exist, create a new one with the correct properties.
calendar = EKCalendar(forEntityType: EKEntityTypeEvent, eventStore: eventManager.eventStore)
calendar.title = "My Calendar"
calendar.CGColor = UIColor(red: 0, green: 0.62, blue: 0.10, alpha: 1).CGColor
// Find the proper source type value.
var localSource:EKSource? = nil
if let eventStore = eventManager.eventStore{
//get the correct source for the calendar
for source in eventStore.sources() as [EKSource] {
println("eventStore.source - \(source)")
switch (source.sourceType.value){
/*
case (EKSourceTypeSubscribed.value):
localSource = source
*/
case (EKSourceTypeLocal.value):
localSource = source
default:
break
}
}
calendar.source = localSource
}
//save the newly created calendar
eventManager.eventStore!.saveCalendar(calendar, commit: true, error: &error)
//check for any errors.
if(error == nil){
//no errors were encountered save identifier
NSUserDefaults.standardUserDefaults().setObject(calendar.calendarIdentifier, forKey: "calendar_identifier")
println("Making calendar")
}else{
println("error in making calendar - \(error?.localizedDescription)")
return
}
}
I first check if a calendar identifier exists in the NSUserDefaults. I then create the new calendar and assign the local source to it once it is found. Then I save the calendar (which works and returns true.) No errors are returned and when I print the source it gives:
EKSource <0x1700dad30> {UUID = BD42B102-A185-4A93-9114-737001A4C408; type = Local; title = Default; externalID = (null)}
What am I doing wrong here, or what is a better way?

It seems to be a bug/strange behavior with iCloud.
That happens if you have iCloud activated, but you have disabled to sync the Reminders. This Guy had the same problem(but couldn't resolve it).
Try to enable Reminder-sync.

Related

Trying to Update Calendar, Getting Various Errors

I can add a new calendar to the user's calendars like this, using the saveCalendar(_:commit:) method:
let ekStore = EKEventStore()
func saveCalendar(calendar: EKCalendar) throws {
try ekStore.saveCalendar(calendar, commit: true)
}
and
let newList = EKCalendar(for: .reminder, eventStore: ekStore)
newList.source = ekStore.defaultCalendarForNewReminders()?.source
newList.title = newListName
newList.cgColor = listColor
do {
try saveCalendar(calendar: newList)
} catch {
print("Error adding list: \(error.localizedDescription)")
}
I then store the calendar object.
When the user finishes editing a calendar (reminders list) in my app, I try to save it like this, using the stored calendar as a starting point:
let updatedList = existingCalendar
updatedList.title = newListName
updatedList.cgColor = listColor
do {
try saveCalendar(calendar: updatedList)
} catch {
print("Error saving list: \(error.localizedDescription)")
}
But the calendar doesn't save and I get this error: That account does not support reminders..
I have also tried explicitly setting the calendar's source:
updatedList.source = ekStore.defaultCalendarForNewReminders()?.source
but then I get this error: That calendar may not be moved to another account..
My Question: How do I update calendars (reminder lists) from my app?

CKShare "Server record changed" error

I have run into an issue when saving a CKShare. When executing the share everything works as it should, but if the share is cancelled and tried to share again I receive the error:
"Server Record Changed" (14/2004); server message = "client oplock error updating record"
The following is my sharing code:
func share(_ deck: Deck) {
let zoneID = CKManager.defaultManager.sharedZone?.zoneID
if let deckName = deck.name {
selectedDeckForPrivateShare = deckName
}
var records = [CKRecord]()
let deckRecord = deck.deckToCKRecord(zoneID)
records.append(deckRecord)
let reference = CKReference(record: deckRecord, action: .none)
for index in deck.cards {
let cardRecord = index.cardToCKRecord(zoneID)
cardRecord["deck"] = reference
cardRecord.parent = reference
records.append(cardRecord)
}
let share = CKShare(rootRecord: deckRecord)
share[CKShareTitleKey] = "\(String(describing: deck.name)))" as CKRecordValue?
records.append(share)
let sharingController = UICloudSharingController { (controller, preparationCompletion) in
let modifyOP = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
modifyOP.qualityOfService = .userInteractive
modifyOP.modifyRecordsCompletionBlock = { (savedRecords,deletedRecordIDs,error) in
preparationCompletion(share, CKContainer.default(), error)
controller.delegate = self
if let error = error {
print(error)
}
}
self.privateDatabase.add(modifyOP)
}
sharingController.delegate = self
self.present(sharingController, animated: true, completion: {
})
}
The two methods deckToCKRecord() and cardToCKRecord() are what I use to convert from Core Data to CKRecord so that they can be shared. Thank you.
It's possible that even though the sharing operation is cancelled (since you weren't explicit on what you mean by cancelled), the CKShare record you set up is actually created and saved in your private database. You simply cancelled changing the participation status (e.g. invited) of the other user. Therefore when you go to repeat this procedure, you are trying to re-save the same CKShare, albeit set up again thus likely modifying the change tag. CloudKit sees this and returns a "Server Record Changed" error.
If I'm correct, you could handle this one of two ways. First, set the value of the CKModifyRecordsOperation variable savePolicy to .allKeys, for example, right where you are setting the qualityOfService var. This will mandate that even if CloudKit finds the record on your server, it will automatically overwrite it. If you are sure that the uploaded version should ALWAYS win, this is a good approach (and only results in a single network call).
A second more general approach that most people use is to first make a CKFetchRecordsOperation with the root record's share var recordID. All CKRecord objects have a share var which is a CKReference, but this will be nil if the object hasn't been shared already. If I am right, on your second try after cancelling, you will find the root record's share reference to contain the CKShare you set up on the first try. So grab that share, then with it, initiate the rest of your parent and other references as you did before!

Mark Notifications as Read in iOS 11 with CloudKit

I have an app that uses CloudKit and everything worked perfectly until iOS 11.
In previous iOS versions, I used a CKQuerySubscription with NSPredicate to receive notifications whenever a user changes a specific table, matching the NSPredicate properties.
After doing so, whenever the server sent notifications, I would iterate through them, fetching its changes and afterwards marking them as READ so I would parse through them again (saving a serverToken).
Now in iOS 11 , Xcode informs me that those delegates are deprecated, and I should change them, but this is where I'm having trouble with - I cannot figure out how to do it in the non-deprecated way, for iOS 11.
Here's my code:
Saving a subscription
fileprivate func setupCloudkitSubscription() {
let userDefaults = UserDefaults.standard
let predicate = NSPredicate(format: /*...*/) // predicate here
let subscription = CKQuerySubscription(recordType: "recordType", predicate: predicate, subscriptionID: "tablename-changes", options: [.firesOnRecordUpdate, .firesOnRecordCreation])
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true // if true, then it will push as a silent notification
subscription.notificationInfo = notificationInfo
let publicDB = CKContainer.default().publicCloudDatabase
publicDB.save(subscription) { (subscription, err) in
if err != nil {
print("Failed to save subscription:", err ?? "")
return
}
}
}
Check for pending notifications
fileprivate func checkForPendingNotifications() {
let serverToken = UserDefaults.standard.pushNotificationsChangeToken
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
var notificationIDsToMarkRead = [CKNotificationID]()
operation.notificationChangedBlock = { (notification) -> Void in
if let notificationID = notification.notificationID {
notificationIDsToMarkRead.append(notificationID)
}
}
operation.fetchNotificationChangesCompletionBlock = {(token, err) -> Void in
if err != nil {
print("Error occured fetchNotificationChangesCompletionBlock:", err ?? "")
print("deleting existing token and refetch pending notifications")
UserDefaults.standard.pushNotificationsChangeToken = nil
return
}
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
markOperation.markNotificationsReadCompletionBlock = { (notificationIDsMarkedRead: [CKNotificationID]?, operationError: Error?) -> Void in
if operationError != nil {
print("ERROR MARKING NOTIFICATIONS:", operationError ?? "")
return
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(markOperation)
if token != nil {
UserDefaults.standard.pushNotificationsChangeToken = token
}
}
let operationQueue = OperationQueue()
operationQueue.addOperation(operation)
}
As you can see, the code above works perfectly on iOS until 11;
Now Xcode prompts warning on the following lines:
let operation = CKFetchNotificationChangesOperation(previousServerChangeToken: serverToken)
Warning:
'CKFetchNotificationChangesOperation' was deprecated in iOS 11.0: Instead of iterating notifications to enumerate changed record zones, use CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation
AND
let markOperation = CKMarkNotificationsReadOperation(notificationIDsToMarkRead: notificationIDsToMarkRead)
Warning:
'CKMarkNotificationsReadOperation' was deprecated in iOS 11.0: Instead of iterating notifications, consider using CKDatabaseSubscription, CKFetchDatabaseChangesOperation, and CKFetchRecordZoneChangesOperation as appropriate
I tried applying CKDatabaseSubscription but by doing so I cannot apply a NSPredicate to filter the subscription as I can do with CKQuerySubscription, and if I try to fetch and mark pending notifications
as Read it shows those warnings.
What's the best approach for iOS 11 in this case? Any hint?
Thank you.
So as I've mentioned in the comment above in the openradar bug report , which can be found here this is a known issue in iOS 11 when fetching changes for public records and then mark those changes as read, saving the given token.
As there's not a true solution for this issue, because Apple hasn't gave a workaround for this, or maybe not marking those delegate-functions as deprecated until a solution is given I had to go through a different path, which was the follwowing:
I had to create a custom CKRecordZone in the Private Database, then I had to subscribe database changes in that zoneID, by doing so, whenever the user changes something in that database the desired push-notifications and/or silent-push-notifications fire as expected and then I can parse the new data.
My issue here is that I had the User Profile in a public database, so whenever the user changed something related to him (Name, Bio, etc) it saved in CloudKit and silent notifications would fire to the user's other devices to update this data - this I can do perfectly with the private database as well - but my problem was that other users could search for app-users to follow-unfollow them and if that data is stored in the private-database it will be out of general users search scope.
In order to overcome this I had to semi-duplicate the User Profile data.
The user fetches and edits its data through the private-database, and on save it also update a semi-related table in the public-database so it is available to search for general-users.
Until Apple allows us to fetch changes from public-database as we used to do in iOS 10 this solution will work for me temporarily.

Core Spotlight Userinfo is always empty

I am using a combination of CoreSpotlight api and NSUserActivity api to index app content. Everything goes well until I tap a search result. The userInfo passed with userActivity in continueUserActivity method contains only one item i.e kCSSearchableItemActivityIdentifier. My other custom keys are nil.
Here is my code for indexing items..
class MyTestViewController:UIViewController{
viewDidLoad(){
searchHandler = SearchHandler()
searchHandler.index(items)
}
}
class SearchHandler{
var activity: NSUserActivity!
func index(items:[Item]){
for item in items{
let attributeSet = getSearchItemAttribute(item)
if let attributeSet = attributeSet{
let searchableItem = CSSearchableItem(uniqueIdentifier: item.uniqueId, domainIdentifier:itemType.groupId(), attributeSet: attributeSet)
searchableItem.expirationDate = item.expirationDate
addToSpotlight([searchableItem])
}
activity = NSUserActivity(activityType: searchPrivacy.activity())
activity.delegate = delegate
//meta data
activity.title = item.title
var userInfoDic = [NSObject:AnyObject]()
userInfoDic["indexItemType"] = itemType.rawValue
userInfoDic["address"] = item.title
activity.userInfo = userInfoDic
if item.expirationDate != nil { activity.expirationDate = item.expirationDate! }
if item.keywords != nil { activity.keywords = item.keywords! }
activity.contentAttributeSet = attributeSet
//eligibility
activity.eligibleForHandoff = false
activity.eligibleForSearch = true
activity.eligibleForPublicIndexing = true
activity.requiredUserInfoKeys = Set(["indexItemType","address"])
activity.becomeCurrent()
}
}
private func getSearchItemAttribute(item:Item) ->CSSearchableItemAttributeSet?{
if item.contentType != nil { // add an entry to core spot light
let attributeSet = CSSearchableItemAttributeSet(itemContentType: item.contentType!)
attributeSet.relatedUniqueIdentifier = item.uniqueId
HALog.i("item.uniqueId= \(item.uniqueId)")
attributeSet.title = item.title
attributeSet.thumbnailData = item.thumbnailData
attributeSet.thumbnailURL = item.thumbnailUrl
attributeSet.rating = item.ratings
attributeSet.ratingDescription = item.ratingDescription
attributeSet.contentDescription = item.contentDescription
return attributeSet
}
return nil
}
private func addToSpotlight(searchableItems:[CSSearchableItem]) {
CSSearchableIndex.defaultSearchableIndex().indexSearchableItems(searchableItems) { (error) -> Void in
if let error = error {
HALog.e("Deindexing error: \(error.localizedDescription)")
} else {
HALog.i("Search item successfully indexed!")
}
}
}
}
Whenever I try to access indexItemType or address keys in userInfo its always nil.
I have tried all the solutions from these threads:
iOS 9 - NSUserActivity userinfo property showing null
http://helpprogramming.xyz/question/31328032/ios-9-nsuseractivity-userinfo-property-showing-null
None of the above solved my problem.
I am currently using Xcode 7 with iOS 9.
If you use CSSearchableIndex then you will only receive the kCSSearchableItemActivityIdentifier in the userInfo. To set and retrieve custom properties in the userInfo you should only set the userActivity to becomeCurrent.
Stub out your call for the CSSearchableIndex and see if it works (you should also make sure your nsuseractivity object is a strong property on your view/model, as it can get deallocated before it has a chance to save the userInfo).
The info on the thread below helped me:
https://forums.developer.apple.com/thread/9690
I had the same issue here, and i searched so many answers not working for me...
fortunately i got my solution in ios-9-tutorial-series-search-api
I recommend you read all the content above. I will give you two alternative solutions:
use NSUserActivity instead of CSSearchableItem: when you done your data setting, call userActivity.becomeCurrent, then your can found your data in spotlight(just two more things: I don't have a iOS 9 device, so only tested in iOS 10 Device, and the article I mentioned above said NSUserActivity is limited to one activity per navigation point, but I don't have such kind issue...)
use CSSearchableItem, set the uniqueIdentifier to a JSON string that contains your custom userInfo.
NOTE: Do not index a CSSearchableItem and call userActivity.becomeCurrent both, otherwise you will got two spotlight results. One of them is com.apple.corespotlightitem, other one is your custom activityType

Error getting new calendar for new reminders

I'm trying to add a feature in my application so the user can get a list of reminders using the following method.
The following method is the main method I'm using for retrieving the reminders:
func getReminders(){
var eventStore : EKEventStore = EKEventStore()
// This lists every reminder
var calender = getCalender(eventStore)
let calendars = eventStore.calendarsForEntityType(EKEntityTypeReminder)
as! [EKCalendar]
//cals.append(calender)
var predicate = eventStore.predicateForRemindersInCalendars([calender])
eventStore.fetchRemindersMatchingPredicate(predicate) { reminders in
for reminder in reminders {
println(reminder.title)
self.remindersTitles.append(reminder.title!!)
}}
var startDate=NSDate().dateByAddingTimeInterval(-60*60*24)
var endDate=NSDate().dateByAddingTimeInterval(60*60*24*3)
var predicate2 = eventStore.predicateForEventsWithStartDate(startDate, endDate: endDate, calendars: nil)
println("startDate:\(startDate) endDate:\(endDate)")
var eV = eventStore.eventsMatchingPredicate(predicate2) as! [EKEvent]!
if eV != nil {
for i in eV {
println("Title \(i.title!)" )
println("stareDate: \(i.startDate)" )
println("endDate: \(i.endDate)" )
}
}
}
As you notice I'm creating a calendar and assign it the return value of a method called 'getCalender':
func getCalender(store: EKEventStore) -> EKCalendar {
let defaults = NSUserDefaults.standardUserDefaults()
if let id = defaults.stringForKey("GSCalender") {
return store.calendarWithIdentifier(id)
} else {
var calender = EKCalendar(forEntityType: EKEntityTypeReminder, eventStore: store)
calender.title = "Genie Sugar Calender!"
calender.CGColor = UIColor.redColor().CGColor
calender.source = store.defaultCalendarForNewReminders().source!
var error: NSError?
store.saveCalendar(calender, commit: true, error: &error)
if error == nil {
defaults.setObject(calender.calendarIdentifier, forKey: "GSCalender")
}
if calender == nil {
println("nothing here")
}
return calender
}
}
But the issue is that the application is stuck at this line of the second method:
calender.source = store.defaultCalendarForNewReminders().source!
And returns me this error:
Error getting default calendar for new reminders: Error Domain=EKCADErrorDomain Code=1013 "The operation couldn’t be completed.
Any ideas please to overcome this problem? with my advanced thanks
I noticed the iPhone simulator, after it being reset - returns nil for store.defaultCalendarForNewReminders(). I believe it is a simulator bug.
By doing lots of tests, I came up with an observation that the simulators can fetch defaultCalendarForNewReminders() smoothly (with calendar usage permission)....but with the testing on a real device with iOS 14.6 it is returning nil, even an iPhone has the default calendar for events already!
I had also tried with EKCalendarChooser in order to select other calendars, but still it not worked!
So I think it is an iOS bug.

Resources