How to subscribe to changes for a public database in CloudKit? - ios

What is the best way to subscribe to a public database in CloudKit?
I have a table with persons. Every person contains a name and a location.
Once the location changes, the location is updated in CloudKit.
That part is working fine.
But I am not able to make it work to get a notification when there is a record update.
Some example would be really helpful, as I have looked into the possible option already.
I have looked into the options where I save the subscription in the database and also the CKModifySubscriptionsOperation option.
Currently, my code to subscribe looks like this:
let predicate = NSPredicate(format: "TRUEPREDICATE")
let newSubscription = CKQuerySubscription(recordType: "Person", predicate: predicate, options: [.firesOnRecordCreation, .firesOnRecordDeletion, .firesOnRecordUpdate])
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
newSubscription.notificationInfo = info
database.save(newSubscription, completionHandler: {
(subscription, error) in
if error != nil {
print("Error Creating Subscription")
print(error)
} else {
userSettings.set(true, forKey: "subscriptionSaved")
}
})
Can someone also show me how my AppDelegate should look like?
I have added the didReceiveRemoteNotification function to my AppDelegate. I also called application.registerForRemoteNotifications(). This is how my didReceiveRemoteNotification function looks like:
The print is not even coming for me.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Notification!!")
let notification = CKNotification(fromRemoteNotificationDictionary: userInfo) as? CKDatabaseNotification
if notification != nil {
AppData.checkUpdates(finishClosure: {(result) in
OperationQueue.main.addOperation {
completionHandler(result)
}
})
}
}

Here are a few other things you can check:
= 1 =
Make sure the CloudKit container defined in your code is the same one you are accessing in the CloudKit dashboard. Sometimes we overlook what we selected in Xcode as the CloudKit container when we create and test multiple containers.
= 2 =
Check the Subscriptions tab in the CloudKit dashboard and make sure your Person subscription is being created when you launch your app. If you see it, try deleting it in the CK Dashboard and then run your app again and make sure it shows up again.
= 3 =
Check the logs in the CK Dashboard. They will show a log entry of type push whenever a push notification is sent. If it's logging it when you update/add a record in the CK Dashboard, then you know the issue lies with your device.
= 4 =
Remember that push notifications don't work in the iOS simulator. You need an actual device (or a Mac if you are making a macOS app).
= 5 =
Through extensive testing, I've found notifications are more reliable if you always set the alertBody even if it's blank. Like this:
let info = CKSubscription.NotificationInfo()
info.shouldSendContentAvailable = true
info.alertBody = "" //This needs to be set or pushes don't always get sent
subscription.notificationInfo = info
= 6 =
For an iOS app, my app delegate handles notifications like this:
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//Ask Permission for Notifications
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { authorized, error in
DispatchQueue.main.async {
if authorized {
UIApplication.shared.registerForRemoteNotifications()
}
}
})
return true
}
//MARK: Background & Push Notifications
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]{
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
if let sub = notification.subscriptionID{
print("iOS Notification: \(sub)")
}
}
//After we get permission, register the user push notifications
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
//Add your CloudKit subscriptions here...
}
}
Getting permission for notifications isn't required if you are only doing background pushes, but for anything the user sees in the form of a popup notification, you must get permission. If your app isn't asking for that permission, try deleting it off your device and building again in Xcode.
Good luck! : )

I am using RxCloudKit library, here's an a code snippet of how it handles query notifications -
public func applicationDidReceiveRemoteNotification(userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
switch notification.notificationType {
case CKNotificationType.query:
let queryNotification = notification as! CKQueryNotification
self.delegate.query(notification: queryNotification, fetchCompletionHandler: completionHandler)
...
This method is called from func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void)
Before you can receive notifications, you will need to do the following -
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
application.registerForRemoteNotifications()
...
UPDATE:
Info.plist should contain the following -
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>

Update: As Reinhard mentioned in his comment: you can in fact still subscribe to changes from the public database and manually import the changes into Core Data. Still I am unsure whether it is a good idea to rely on subscriptions in the public database if Apple specifically mentioned these differences
Original answer:
I don't think the accepted answer fully answers the question here.
Short answer would be that the CloudKit public database does not support subscriptions like the private database. Instead only a polling mechanism can be used. NSPersistentCloudKitContainer handles this automatically, but only updates very rarely.
This talk from WWDC2020 explains this in detail and I recommend watching it because there are other important details mentioned where public database differs from private database: https://developer.apple.com/wwdc20/10650
In the talk they mentioned thet a pull is initiated on each app start and after about 30 mins of application usage.

Related

AppDelegate Never Gets Its didReceiveRemoteNotification Called For CKQuerySubscription

I'm trying to let the iOS app listen to CKQuerySubscription changes. Data is transmitted by a remote iOS app. I already have a macOS application, which does receive data sent by the remote iOS app. The iOS app I have trouble with already has a subscription. Yet, its AppDelegate never receives a call in the didReceiveRemoteNotification method.
import UIKit
import UserNotifications
import CloudKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
/* notifications */
let center = UNUserNotificationCenter.current()
center.delegate = self
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
switch settings.authorizationStatus {
case .authorized:
print("You already have permission")
DispatchQueue.main.async() {
application.registerForRemoteNotifications()
}
case .denied:
print("setting has been disabled")
case .notDetermined:
print("Let me ask")
UNUserNotificationCenter.current().requestAuthorization(options: []) { (granted, error) in
if error == nil {
if granted {
print("you are granted permission")
DispatchQueue.main.async() {
application.registerForRemoteNotifications()
}
}
}
}
}
}
return true
}
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register notifications_ error:", error)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("Receiving data...") // never called...
}
}
I have some capabilities on as shown below. I don't know if the app needs push notifications. For now, it's turned on.
So why doesn't my iOS app get the remote notification call? I'm using the app with an actual device, not a simulator. Thanks.
EDIT: Creating a subscription to a record change
class HomeViewController: UIViewController {
override func viewDidLoad() {
registerSubscription()
}
func registerSubscription() {
let cloudContainer = CKContainer(identifier: "iCloud.com.xxx.XXXXX")
let privateDB = cloudContainer.privateCloudDatabase
let predicate = NSPredicate(format: "TRUEPREDICATE")
let subscription = CKQuerySubscription(recordType: "PrivateRecords", predicate: predicate, options: .firesOnRecordCreation)
let notification = CKNotificationInfo()
subscription.notificationInfo = notification
privateDB.save(subscription, completionHandler: ({returnRecord, error in
if let err = error {
print("Subscription has failed: \(err.localizedDescription)")
} else {
print("Subscription set up successfully")
print("Subscription ID: \(subscription.subscriptionID)")
}
}))
}
}
There are a few more things you can check.
First, make sure you implement didReceiveRemoteNotification in your app delegate:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
let dict = userInfo as! [String: NSObject]
let notification = CKNotification(fromRemoteNotificationDictionary: dict)
if let sub = notification.subscriptionID{
print("iOS Notification Received: \(sub)")
}
}
There are also a few other things you can check:
Try deleting your CKQuerySubscription in the CloudKit dashboard, then run your iOS code again that registers it. Does the subscription show up in the dashboard?
Does the CloudKit log show that a notification was sent? It lists all notifications that were pushed to a device.
If you are using silent push notifications, try enabling Background fetch in the Background Modes capability (right above Remote notifications).
If you do all that and it still doesn't work, can you share your CKQuerySubscription code?
-- Update --
Try setting some additional attributes on your CKNotificationInfo object. There are some obscure bugs with notifications that can usually be circumvented by setting a couple properties like this:
notification.shouldSendContentAvailable = true
notification.alertBody = "" //(Yes, a blank value. It affects the priority of the notification delivery)
You can also try setting your predicate to: NSPredicate(value: true)
Also, what does your privateDB.save method return? Does it say it succeeds or fails?

Apple Push Notifications (APN) Inconsistency?

We are running into a confusing issue when using Apple's Push Notifications via APN. We have the following scenario (quite standard i guess):
When our App, let's call it "MyApp" here, is installed and started for the first time we are asking the user for permissions to send him Push Notifications through "MyApp".
In this example the AppDelegate looks like this:
import UIKit
import UserNotifications
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Register Remote Notifications
UNUserNotificationCenter.current().delegate = self
self.registerForPushNotifications()
return true
}
// MARK: - Remote Notifications
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
guard granted else {
return
}
self.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
guard settings.authorizationStatus == .authorized else {
return
}
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { (data) -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("ApnToken: \(token)")
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("did Fail to Register for RemoteNotifications")
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("willPresentNotification!")
completionHandler([.badge, .sound, .alert])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
print("UserDidResponseToNotification!")
completionHandler()
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("DidReceiveRemoteNotification!")
completionHandler(.newData)
}
}
So the user installs and the starts the app and is asked if "MyApp" is allowed to send the user Push Notifications. If the user accepts the Push Notifications application(_:didRegisterForRemoteNotificationsWithDeviceToken:) is called and we give the received deviceToken to our API.
Now the part that confuses me:
The user also has the option to turn Push Notifications off later via the iPhone-Settings like this: Settings > "MyApp" > Notifications > Allow Notifications > Turns the Switch off
Our API has now the deviceToken for APN but the user turned Push Notifications off via iPhone-Settings.
The "problem":
After the user turned off Push Notifications we are still able to send silent Push Notifications to the device and "MyApp" gets the data correct without any problems.
But in the other scenario: The User installs and starts "MyApp" and declines at the first start Push Notifications it is not possible to get a deviceToken from Apple. I tried to get a deviceToken from Apple even if the user declined the Push Notification Alert like this: (But this doesn't work - I guess Apple doesn't provide my any if the user declines)
func registerForPushNotifications() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (granted, error) in
self.getNotificationSettings()
}
}
func getNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { (data) -> String in
return String(format: "%02.2hhx", data)
}
let token = tokenParts.joined()
print("ApnToken: \(token)")
}
It seems like it doesn't matter what the user does if he accepts Push Notifications at the first start. I mean ok, we cannot show the Information via Banner or anything to the user but we can transfer data to the device using APN's even if the user did turn off this setting at a later time. (But we cannot send anything if he declines at start of the app - we need a deviceToken once)
Am i misunderstanding a thing here? This seems inconsistent to me.
I tried to clarify my question so everyone can understand what i am asking for. Excuse my "bad" english it is not easy as a non-native-speaker to ask specific questions with a lot of context here. Anyway, if you need further informations or you didn't understand one or more points from what i am asking let me know i will provide detailed informations and will clarify my question.
I don't know if this matters but at the moment we are using an APN-Development-Certificate (not a distribution certificate yet)
Good question,
The thing is that if user allows you to send push notification (gives you his/her device token) you would be able to send pushes. The push data via notification could be send without notification for a user (silence notification) you could read more about it here: https://medium.com/#m.imadali10/ios-silent-push-notifications-84009d57794c
That's why you are able to send push even if user prevents notification from displaying. That settings controls only displaying appearance, but since he/she gives you the token you still could send data to them. There actually no way for a user to disable that token after it was granted.

CKDatabaseSubscription not pushing notifications

I'm trying to receive changes with CKDatabaseSubscription but I'm stuck that I don't receive any push notifications from that subscription.
I'm subscribing to changes in private record zone via such method:
let container = CKContainer.default()
func subscribeChanges() {
let subscription = CKDatabaseSubscription(subscriptionID: "test")
let notificationInfo = CKNotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: [])
operation.modifySubscriptionsCompletionBlock = { savedSubscriptions, deletedSubscriptionIDs, operationError in
if operationError != nil {
print(operationError)
return
} else {
print("Subscribed")
}
}
container.privateCloudDatabase.add(operation)
}
Subscribe is successful. But I don't receive any notifications on device, I've checked with such method:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
NSLog("Notification received")
}
I'm testing on real device connected to XCode, tried to push changes via simulator and Apple's dashboard.
Update
As I've found CKDatabaseSubscription only works for shared record zones. For public & private zone I can use something like that and it works:
CKQuerySubscription(recordType: "TestRecordType", predicate: NSPredicate(format: "TRUEPREDICATE"), options: CKQuerySubscriptionOptions.firesOnRecordCreation)
As for know CKDatabaseSubscription will fire notifications only for record zones with CKRecordZoneCapabilityFetchChanges, all private custom zones have that capability (shared I think also), default zones not.
So only way now is to create new custom zone.

CloudKit push notifications on record update stopped working

EDIT:
Retested today 27.08.2015 and it works again, Apple has fixed it.
I have an application in development mode. The application uses CKSubscription to get notified on changes on the server, configured for all three options: create, update, delete. Everything was working fine but recently during regression tests I have discovered the application does not receive notifications on record updates, the create and delete notifications are still working. The susbcription types are set correctly for all three options as I checked on the dashboard and the application is registered for CKSubscription as it was a couple of days ago when it was working like charm. I am not getting any errors from CloudKit. The reset of development environment did not help. I have re-tested with the version with which I am sure it was working and got the same results.
Any idea what might be causing this issue, what else should I check / try?
Additional Info:
I guess something might go wrong at the server side. I have not changed anything in the code where I am subscribing for CloudKit events and handling push notifications - anyway also the version where it was working is not getting update notifications any longer. The application I am working on is published, thus changing container is no go. Not sure if this might be causing the issue, just want to mention: the app is using the same container for storing the Core Data in the cloud - the goal of the app upgrade is to migrate data to the CloudKit and use it as the cloud storage exclusively. It is confusing that everything was working fine for weeks and suddenly stopped working without any clear reason, probably as the effect of the load by intensive testing, adding record types...
Test with app developed from scratch:
I have written a simple test app to check receiving notifications. I can receive only the notification on record creation. What is wrong with my code:
import UIKit
import CloudKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let container = CKContainer.defaultContainer()
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let settings = UIUserNotificationSettings(forTypes: .Alert, categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
return true
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
println("didFailToRegisterForRemoteNotificationsWithError: \(error)")
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
println("didRegisterForRemoteNotificationsWithDeviceToken: \(deviceToken)")
subscribe()
}
func subscribe() {
// let predicate = NSPredicate(format: "text != %#", argumentArray: [""])
// let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let predicate = NSPredicate(value: true)
let subscription = CKSubscription(recordType: "Note", predicate: predicate, options: .FiresOnRecordDeletion | .FiresOnRecordUpdate | .FiresOnRecordCreation)
let notificationInfo = CKNotificationInfo()
notificationInfo.alertBody = ""
subscription.notificationInfo = notificationInfo
let publicDatabase = container.publicCloudDatabase
println("subscribing with CloudKit...")
publicDatabase.saveSubscription(subscription, completionHandler: { (returnedSubscription, error) -> Void in
if let error = error {
println("subscription error \(error.localizedDescription)")
} else {
println("subscription ok")
}
})
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
let ckNotification = CKQueryNotification(fromRemoteNotificationDictionary: userInfo)
println("didReceiveRemoteNotification: \(ckNotification)")
}
func applicationWillResignActive(application: UIApplication) {}
func applicationDidEnterBackground(application: UIApplication) {}
func applicationWillEnterForeground(application: UIApplication) {}
func applicationDidBecomeActive(application: UIApplication) {}
func applicationWillTerminate(application: UIApplication) {}
}
I have experienced this behavior in the past. In my case I could solve it by just deleting the subscription and creating it again. You should do that from code and not the dashboard. Doing it from the dashboard only works for the account that you are loged in into the dashboard.

Not receiving Push Notifications from CloudKit Subscriptions

I'm not receiving Push Notifications I expect from CloudKit Subscriptions.
Here's what I've done so far:
Enabled the CloudKit and Remote Notifications capabilities.
Created a 'Test' Record Type using the CloudKit dashboard.
Created a subscription for the appropriate record type (Test), which
I can see in the CloudKit dashboard.
Use a physical device to test, which is signed in to iCloud and
connected to the internet.
Set up the app delegate to receive notifications.
Manually Inserted/Updated/Deleted records via the CloudKit portal.
Unfortunately I never receive any push notifications, ever. The code involved is shown below. Literally, this is the only code in a brand new blank project.
// MARK: - SUBSCRIPTIONS
func subscribeToRecordChangesWithRecordType (recordType:String, database:CKDatabase) {
let predicate = NSPredicate(value: true)
let subscription = CKSubscription(recordType: recordType, predicate: predicate, options: CKSubscriptionOptions.FiresOnRecordCreation|CKSubscriptionOptions.FiresOnRecordDeletion|CKSubscriptionOptions.FiresOnRecordUpdate)
database.saveSubscription(subscription, completionHandler: { (savedSubscription, error) -> Void in
if let _error = error {
NSLog("ERROR saving '%#' subscription %#",recordType, _error)
} else {
NSLog("SUCCESS creating '%#' subscription: %#", recordType, savedSubscription)
}
})
}
func createSubscriptions () {
let privateDB = CKContainer.defaultContainer().privateCloudDatabase
let publicDB = CKContainer.defaultContainer().publicCloudDatabase
// NOTE: create a Record Type called 'Test' in the CloudKit dashboard
self.subscribeToRecordChangesWithRecordType("Test", database: privateDB)
self.subscribeToRecordChangesWithRecordType("Test", database: publicDB)
}
// MARK: - PUSH NOTIFICATIONS
func registerForPushNotifications (application: UIApplication) {
self.createSubscriptions()
let settings = UIUserNotificationSettings(forTypes: .Alert, categories: nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
NSLog("Registered for Push Notifications with token: %#", deviceToken);
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
NSLog("FAILED to register for Push Notifications. %#", error)
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
NSLog("RECEIVED Push Notification")
NSNotificationCenter.defaultCenter().postNotificationName("PushNotificationReceived", object: userInfo)
}
func application(application: UIApplication, didReceiveLocalNotification notification: UILocalNotification) {
NSLog("RECEIVED LOCAL Push Notification")
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
NSLog("RECEIVED Push Notification with fetchCompletionHandler")
NSNotificationCenter.defaultCenter().postNotificationName("PushNotificationReceived", object: userInfo)
}
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.registerForPushNotifications(application)
return true
}
Thanks in advance for any tips or suggestions. I hope this isn't a bug and that I'm doing something wrong here ... it should 'just work'!
Cheers
Make sure
You have enabled Push Notification besides CloudKit (and Background Mode if needed) in App's Capabilities tab. And if needed, find the push certificates (one for Dev, one for production) from Developer Portal, download them and install them (by double clicking on them);
You're testing the app on a device. Apple does not push to the simulator.

Resources