Not Getting CloudKit Push Notifications - ios

I had push notifications from CloudKit working and I'm afraid I've done something to break them. If anyone can see something I don't, please help.
When the app launches, I call setupSubscriptions(), which has this code:
let predicate = NSPredicate(value: true)
let subscriptionID = "public-new-changes-deleted"
let subscription = CKQuerySubscription(recordType: recordType, predicate: predicate, subscriptionID: subscriptionID, options: [.firesOnRecordCreation, .firesOnRecordUpdate, .firesOnRecordDeletion])
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
publicDB.save(subscription) { subscription, error in
if error != nil {
print("subscription was set up")
}
The setup message does fire. I've also tried making the notificationInfo CKQuerySubscription.NotificationInfo, but there's no discernible difference whether it's that or CKSubscription.
In my app delegate:
application.registerForRemoteNotifications()
I do get a message from application(_ application:, didRegisterForRemoteNotificationsWithDeviceToken:) that the application has registered for notifications.
Then I have:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
print("application did receive remote notification")
}
Next I got to my CloudKit Dashboard and create, modify, or delete a record but nothing happens. I'd expect a message from didReceiveRemoteNotification. but nothing. This was working earlier but I can't think of what I changed to break it.
I can create records there and query for them in the app, so I'm sure it's able to see them, but I can't get a push when they're altered.
Other stuff:
In my target's Capabilities tab:
Background Fetch and Remote Notifications are both checked under Background Modes.
iCloud is on and it's using the correct container -- I can do fetches just fine from CloudKit using the same recordType and publicDB CKDatabase object.
Push Notifications are turned on and my entitlements file has a flag for "APS Environment" with a value of development.
On my Apple Developer account page, under the App ID, iCloud and Push Notifications both have green lights for both "Development" and "Distribution."
I can see in the CloudKit dashboard that the subscription types are created once the app's been run.
I'm testing on a device, not in the simulator.
I've tried:
Changing whether I create the subscription before or after I register for notifications.
Adding a message body, alert sound, and shouldBadge, and requesting notifications using UNUserNotificationCenter, and making the App Delegate a UNUserNotificationCenterDelegate. I get the prompt when I first run the app but the notifications don't arrive.
Splitting the subscriptions up into one for .firesOnRecordCreation and one for update and delete.
Adding the subscriptions using a CKModifySubscriptionsOperation instead of the database's save method.
Please let me know if you have any ideas. Thank you.

Related

Push notification works only when app launched with Xcode

I have a big problem, I set up (with great difficulty) push notifications on my iOS project. I decided to receive the data of the notification in the "didReceiveRemoteNotification" method of the "AppDelegate" then to create it programmatically (in order to carry out mandatory personal treatment). Everything works perfectly, only when I launch my application without the help of Xcode, I no longer receive the notifications, the notification creation code is not executed. I do not know what to do ...
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: #escaping
(UIBackgroundFetchResult) -> Void) {
let body = userInfo["body"]
print(userInfo)
if #available(iOS 10.0, *) {
let content = UNMutableNotificationContent()
content.body = body! as! String
let trigger = UNTimeIntervalNotificationTrigger(timeInterval:
1, repeats: false)
let request = UNNotificationRequest(identifier: "idt", content:
content, trigger: trigger)
UNUserNotificationCenter.current().add(request,
withCompletionHandler: nil)
} else {
// Fallback on earlier versions
}
completionHandler(UIBackgroundFetchResult.newData)
}
Thanks you very much
When you launch your app from a notification you need to check the launchOptions in application:didFinishLaunchingWithOptions: to see if UIApplicationLaunchOptionsRemoteNotificationKey is present.
According to the Documentation:
The value of this key is an NSDictionary containing the payload of the remote notification. See the description of application:didReceiveRemoteNotification: for further information about handling remote notifications.
This key is also used to access the same value in the userInfo dictionary of the notification named UIApplicationDidFinishLaunchingNotification.
Once you determine that the launch contains a notification, invoke your notification handler method with the notification payload obtained from launchOptions.
What you have now, which is application(_:didReceiveRemoteNotification:) is only triggered when the app is already running:
If the app is running, the app calls this method to process incoming remote notifications. The userInfo dictionary contains the aps key whose value is another dictionary with the remaining notification
You need to check in your project settings/Capabilities - Background Modes - Remote notifications
Enable Remote notifications in Capabilities.
{
"aps" : {
"content-available" : 1,
"badge" : 0,
"Priority" : 10
},
"acme1" : "bar",
"acme2" : 42
}
The notification’s priority:
10 The push message is sent immediately.
The remote notification must trigger an alert, sound, or badge on the device. It is an error to use this priority for a push that contains only the content-available key.
https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/BinaryProviderAPI.html#//apple_ref/doc/uid/TP40008194-CH13-SW1
When the device is running and connected to Xcode, the system treats silent notifications as high-priority simply because the OS is smart enough to know "You must be debugging your app, so I will treat your app's tasks as high priority".
When the device is disconnected from Xcode, the system then treats silent notifications as low-priority. Therefore, it doesn't guarantee their delivery in order to improve battery life.

iOS: Do something when a UserNotification is received?

Is it possible to do something when the UserNotification message is delivered to the user every time?
let tdate = Date(timeIntervalSinceNow: 10)
let triggerDate = Calendar.current.dateComponents([.year,.month,.day,.hour,.minute,.second,], from: tdate)
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDate, repeats: true)
let identifier = "hk.edu.polyu.addiction.test"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
UNUserNotificationCenter.current().add(request, withCompletionHandler: { (error) in
if error != nil {
debugPrint("center.add(request, withCompletionHandler: { (error)")
} else {
debugPrint("no error?! at request added place")
}
})
This is the code i did to deliver a notification when the user press a button. (i.e., after 10s, the notification appear to the user.)
Even the user not press on the notification, i want some variable of the app updated (assume the app is at the background).
Possible?
Or, as long as this notification is done. another new notification is scheduled, according to some calculations in my app, possible? (not fixed time, cannot use repeat)
Yes, it is possible! Need to modify the capabilities :
Enable background fetch in background mode.
Enable remote notification in background mode.
Implement delegate method : application:didReceiveRemoteNotification:fetchCompletionHandler:
This delegate method gets called when the notification arrives on the phone, so you can refresh your data.
When user taps on notification delegate method: didReceiveRemoteNotification
gets called and opens the app and user will get latest data in the app.
Apple API reference:
If your payload is not configured properly, the notification might be
displayed to the user instead of being delivered to your app in the
background. In your payload, make sure the following conditions are
true:
The payload’s aps dictionary must include the content-available key
with a value of 1. The payload’s aps dictionary must not contain the
alert, sound, or badge keys. When a silent notification is delivered
to the user’s device, iOS wakes up your app in the background and
gives it up to 30 seconds to run.

CloudKit Not Sending Update Notifications

I am trying to develop a simple messaging app to learn the basics of CloudKit.
I've got it almost figured out, except I am not able to receive notifications for record update events.
To test the app, I am running it simultaneously on the device and on a simulator.
Both instances are logged into the same iCloud account (haven't gotten around to create a dedicated account for testing...); however the app distinguishes the local user from the remote one using UUIds, so that is not a problem.
When one instance of the app wants to send a message, it creates a record and saves it to CloudKit.
I am aware that APNs is not supported on the Simulator, but if I send a message from the Simulator I can get notified on the device.
This works.
Next, I want to mark the messages as "read": That is, flag them when they are first displayed on a device that is not the one that authored them.
I do this by fetching the corresponding record, modifying it, and saving it. So, the message I sent from the device is displayed on the simulator, and flagged there as 'read'. I sync that change with cloudKit and expect the device to receive a notification:
publicDatabase.perform(query, inZoneWith: nil, completionHandler: {(records, error) in
guard let records = records, records.count > 0 else {
return print("Error - Message: \(message.text) by \(message.userName) NOT found on the cloud!")
}
let record = records[0]
// HERE THE RECORD IS MODIFIED LOCALLY:
record["readTimestamp"] = (message.readTimestamp! as NSDate)
// Now, save it:
self.publicDatabase.save(record, completionHandler: {record, error in
guard error == nil else {
return print("Error - Message: \(message.text) by \(message.userName) could NOT be saved as read!")
}
print("Message: \(message.text) by \(message.userName) SAVED as read at \(message.readTimestamp!)")
})
})
However, on the other end, the 'Update' notification is never received.
Is it because both instances are logged into iCloud as the same user? I find this hard to believe, since the "Create" notifications are delivered without problems.
Subscriptions for both notifications ("Message Create" and "Message Update") are registered successfully and appear listed on the CloudKit Dashboard (triggers INSERT and UPDATE).
Update: After a long time thinking what can possibly be different between my "create" subscription and my "update" subscription that could cause only one of them to fire a notification, I realized that only the "create" subscription had a notification body:
let subscription = CKQuerySubscription(recordType: "Message",
predicate: NSPredicate(value: true),
subscriptionID: Bundle.main.bundleIdentifier! + ".subscription.message.create",
options: .firesOnRecordCreation
)
let notificationInfo = CKNotificationInfo()
// THIS LINE MAKES ALL THE DIFFERENCE:
notificationInfo.alertBody = "You've Got Mail!"
subscription.notificationInfo = notificationInfo
publicDatabase.save(subscription, completionHandler: {savedSubscription, error in
...whereas the "update" subscription did not:
let notificationInfo = CKNotificationInfo()
subscription.notificationInfo = notificationInfo
Once I added an alert body, the "update" notifications started arriving.
However, this is just a workaround: I need silent notifications for my read updates. It doesn't make sense to display a banner just to alert the user that his message has been read on the other end.
Also, the CloudKit programming Guide does not mention this limitation.
Is there a way to subscribe with silent (i.e., empty) push notifications?
Update 2: Actually, I found this bit on the API Reference for CKNotificationInfo:
Note
If you don’t set any of the alertBody, soundName, or shouldBadge
properties, the push notification is sent at a lower priority that
doesn’t cause the system to alert the user.
(emphasis mine)
I still fail to see how this "lower priority that doesn’t cause the system to alert the user" is equivalent to "application(:didReceiveRemoteNotification:fetchCompletionHandler:) not being called at all".
The solution appears to be to use an info object with shouldSendContentAvailable = true, like this:
let info = CKNotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
That has solved it for me. It causes didReceiveRemoteNotification: to fire, without any user visible notification appearing. So it's a silent notification, as desired, and it's actually arriving, as desired.
If I leave subscription.notificationInfo nil the app is never notified of changes. But with an [effectively silent] info object I get the desired results.

How to test that silent notifications are working in iOS?

I have a program using subscriptions with silent notification:
let predicate = NSPredicate(format: "recordID == %#", CKRecordID(recordName: "ListName"))
let silentNotification = CKNotificationInfo()
silentNotification.shouldSendContentAvailable = true
silentNotification.desiredKeys = ["Update"]
let subscription = CKSubscription(recordType: "Lists", predicate: predicate, options: .FiresOnRecordUpdate)
subscription.notificationInfo = silentNotification
saveSubscription(subscription)
I now can see the subscription in CloudKit dashboard but, when updating the Update value of the record followed, my app doesn't receive a notification.
As silent notifications are based on best-effort is it normal to don't receive it immediately? I'm using the iOS simulator and would like to be able to debug my app, how can I do that when using silent notifications?
This is a CloudKit bug. Update notifications are still not working.
See:
CKSubscription of type CKSubscriptionOptionsFiresOnRecordUpdate doesn't work
please file a bug report at apple at https://bugreport.apple.com/
many people already have done the same, but apparently it still need more attention from Apple.

CloudKit CKSubscription Includes Mandatory Notifications?

I'm writing a CloudKit-based iOS and Mac application that uses a CKSubscription to get notified when an update occurs in the remote data set. I've got the subscription setup correctly and the notifications are being received. Everything works great! The only issue is that the device receives a user-facing notification.
I would prefer that the remote update notification be an application internal implementation detail; I don't want the user receiving a notification every time they update their own collection of objects. I can't seem to find anything to address this in the documentation. Apple's own docs here talk about this like "duh, of course you want to do a notification." Well, I don't.
If you leave the alertBody of the CKNotificationInfo blank, then you won't get a user facing notification. The notification will be received in your app where you can handle it as usual.
var subscription = CKSubscription(recordType: recordType, predicate: predicate, options: .FiresOnRecordCreation | .FiresOnRecordUpdate | .FiresOnRecordDeletion)
subscription.notificationInfo = CKNotificationInfo()
subscription.notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo.soundName = UILocalNotificationDefaultSoundName
subscription.notificationInfo.alertBody = ""

Resources