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.
Related
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.
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?
I'm trying to setup subscriptions for CloudKit Records, the subscription creates OK, I can retrieve it with CKFetchSubscriptionsOperation successfully. But the function
application(_:didReceiveRemoteNotification:) doesn't get called...
(I'm changing the records by hand in Dashboard)
The subscription is setted in this way:
let database = CKContainer.default().privateCloudDatabase
let subscription = CKSubscription(recordType: "Device", predicate: NSPredicate(format: "TRUEPREDICATE"), options: .firesOnRecordUpdate)
let notificationInfo = CKNotificationInfo()
notificationInfo.alertLocalizationKey = "DEVICE_UPDATED"
notificationInfo.alertBody = "Device updated in database"
subscription.notificationInfo = notificationInfo
database.save(subscription) { subscription, error in
if error != nil {
print(error?.localizedDescription as Any)
} else {
print(subscription)
}
}
And Registration registration for Notification is:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let notificationSettings = UIUserNotificationSettings(types: [.alert, .badge], categories: nil)
application.registerUserNotificationSettings(notificationSettings)
application.registerForRemoteNotifications()
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
print(userInfo)
}
Ok, I had to enable push notifications and generate "Apple Push Notification service SSL Certificate" and attach it to my app at Apple Developer Provisioning Profiles page
For some reason my didReceiveRemoteNotification is never called. I have done the following:
checklist of APNS:
Create AppId allowed with Push Notification
Create SSL certificate with valid certificate and app id
Create Provisioning profile with same certificate and make sure to add device
With Code:
Register app for push notification
Handle didRegisterForRemoteNotificationsWithDeviceToken method
Set targets> Capability> background modes> Remote Notification
Handle didReceiveRemoteNotification
Yet my function does not seem to get called. My code looks like this:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// Override point for customization after application launch.
if (application.respondsToSelector("isRegisteredForRemoteNotifications"))
{
// iOS 8 Notifications
application.registerUserNotificationSettings(UIUserNotificationSettings(forTypes: (.Badge | .Sound | .Alert), categories: nil));
application.registerForRemoteNotifications()
}
else
{
// iOS < 8 Notifications
application.registerForRemoteNotificationTypes(.Badge | .Sound | .Alert)
}
return true
}
func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
let tokenChars = UnsafePointer<CChar>(deviceToken.bytes)
var tokenString = ""
for var i = 0; i < deviceToken.length; i++ {
tokenString += String(format: "%02.2hhx", arguments: [tokenChars[i]])
}
apnsID = tokenString
println("******apnsID is \(apnsID)")
dToken = deviceToken
println("******dToken is \(dToken)")
NSUserDefaults.standardUserDefaults().setObject(deviceToken, forKey: "deviceToken")
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: NSError) {
println("***********didFailToRegisterForRemoteNotificationsWithError")
println(error)
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
println("Getting notification")
var message: NSString = ""
var alert: AnyObject? = userInfo["aps"]
println(userInfo["aps"])
if((alert) != nil){
var alert = UIAlertView()
alert.title = "Title"
alert.message = "Message"
alert.addButtonWithTitle("OK")
alert.show()
}
}
add content_available:true in your request . for iOS payload data.You will get notification in didReceiveRemoteNotification. Now handle it.
As I found out having the same problem myself, in order for didReceiveRemoteNotificationto be called, the incoming notification music carry an "available_content" : true parameter with the notification payload. So in case of you sending the notification in a dictionary form , as was my case in device to device pushes, you just have to add it in the dictionary as you would specify other parameters as you would for sound or badge, also needed if you're using some sort of push service as Postman
Three steps:
Add logic here to handle incoming notification:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// Add your logic here
completionHandler(.newData)
}
Add Background mode to capabilities in target, and ensure to check 'Remote notifications'
While sending a push notification, add "content_available" : true
This worked for me in iOS 14/Xcode 12.3.
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.