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.
Related
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.
When user a sends a push notification to user b, user b will receive the notification, but once they click on it the payload from that notification is gone. Instead when user b opens the notification, the payload in the notification will get saved to parse, so he can view all the notifications that were sent to him. It's like a notification history. Below is my code to send notifications. I need help on how to save the payload within those notifications, so users can look at notification history.
func pushNotifications(){
let userQuery: PFQuery = PFUser.query()!
userQuery.whereKey("objectId", containedIn: Array(setOfSelectedFriends))
let pushQuery: PFQuery = PFInstallation.query()!
pushQuery.whereKey("user", matchesQuery: userQuery)
let push = PFPush()
push.setQuery(pushQuery)
push.setMessage("\(username!) wants you to meet them at \(location!)")
push.sendPushInBackgroundWithBlock {
success, error in
if success {
print("The push succeeded.")
} else {
print("The push failed.")
}
}
}
You probably should do it the other way around, make a table where you save new "MeetingInvites" and whenever a new entry gets added send a push notification to the corresponding user.
Make the push notification a side effect of the actual new entry. Not make the entry a side effect of the push notification.
That would also save you the trouble of having to deal with notifications that get lost - maybe the receiving user opens the app, then something crashes and your push notification and all the connected data would be lost.
A little bit more detail: Create a column for the location, one column for the "requesting" user and one for the "partner". In your code, set the currentUser as the requesting one, and the partner as the ... well ... partner. Then either on your client device or on the server create a push message. In both cases send the push message to the "partner".
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.
I've been reading about and trying to use the CKSubscription feature for weeks, but I can't find info about some general questions. I've read Apple docs, online tutorials, books I bought, and questions here on SO, but I still don't understand the fundamentals I think. Any help is very much appreciated.
Here are questions I cannot find answers to:
1). What is the purpose of the subscriptionID? The convenience init does not include it, so why is it needed in the designated init? If you use it, is it the same for all users of the app?
2). I saw someone mention here that you can unregister a subscriptionID. Why or how would you do this?
3). Can subscriptions be setup in both public or the user's private database?
4). If I have a query based subscription that is the same for all users, will there only ever be 1 subscription listed in the database?
For instance, I'm having trouble getting notifications to work with my
specific use case. It's not a problem in my setup, as I can get a True predicate to work and the notification comes. So I must not understand the fundamentals of how subscriptions work still.
I'm trying to setup a subscription that fires whenever a new record is created when a different user makes a comment on a post. This new record will then contain a reference to the user who created the post. The only subscription I see in the database for both users is - Notifications.user (equals reference). So, I'm assuming I'll only ever see this one subscription.(?) But how does the server keep track of every user's recordID or know when to send it to a specific device?
The problem is I can't get the notification to work. I manually add a record in the dashboard, and I put the other user's recordID as the CKReference. While I'm adding the record, I have the app running in the background on a device under the user's account whom I added as the CKReference in the field. I'd expect the query to trigger and send a push notification since someone commented on this user's post.
Here's my code to setup the subscription:
func subscribe(userID: CKRecordID) {
let options = CKSubscriptionOptions.FiresOnRecordCreation
let userRef = CKReference(recordID: userID, action: .DeleteSelf)
let predicate = NSPredicate(format: "userRef == %#", userRef)
let predicateTwo = NSPredicate(format: "read = %#", "")
let compoundPred = NSCompoundPredicate(type: .AndPredicateType, subpredicates: [predicate, predicateTwo])
let subscription = CKSubscription(recordType: "Notifications", predicate: compoundPred, subscriptionID: subscriptionID,
options: options)
subscription.notificationInfo = CKNotificationInfo()
subscription.notificationInfo.desiredKeys = ["userPost"]
subscription.notificationInfo.alertBody = "Someone replied to your Pod post"
publicDB.saveSubscription(subscription, completionHandler: {
subscription, error in
if (error != nil) {
println("error subscribing: \(error)")
} else {
println("subscribed!")
}
})
}
Let me try and answer:
SubscriptionId allows you to identify later a subscription for example to delete it using the CKDatabase method deleteSubscriptionWithID
For how see answer 1. As to why, well maybe you do not want to get notifications on this subscription any more. This depends on what you are trying to achieve with your app.
Yes
Yes, if you register only one subscription it should work for all users of your app
Regarding your issues, please note that a user recordIDs are special, so you may have issues specifically related to that due to the privacy issues around them. I would suggest to try a simple case that does not involve users and see if subscriptions are working for you. Then think again about how you use user record IDs
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 = ""