Unable to update local scheduled notification content - ios

In one of the WWDC sessions I got code snippet for updating existing notifications. I don't think it works. Trying to update notification content.
First I request pending notifications from UNUserNotificationCenter which always works. Then I am creating new request to update notification with existing unique identifier.
There's 1 new variable content: String.
// Got at least one pending notification.
let triggerCopy = request!.trigger as! UNTimeIntervalNotificationTrigger
let interval = triggerCopy.timeInterval
let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: interval, repeats: true)
// Update notificaion conent.
let notificationContent = UNMutableNotificationContent()
notificationContent.title = NSString.localizedUserNotificationString(forKey: "Existing Title", arguments: nil)
notificationContent.body = content
let updateRequest = UNNotificationRequest(identifier: request!.identifier, content: notificationContent, trigger: newTrigger)
UNUserNotificationCenter.current().add(updateRequest, withCompletionHandler: { (error) in
if error != nil {
print("🚫 Couldn't update notification \(error!.localizedDescription)")
}
})
I am unable to catch error. The problem is that notification content body doesn't change.
Update.
I also tried to change trigger with different repeat interval. It doesn't work, notification is repeated with the same original interval it was created with.
Update 2.
Read Chris' answer, trying to go with first option.
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: { (requests) in
for request in requests {
if request.identifier == notificationIdentifier {
// Got at least one pending notification,
// update its content.
let notificationContent = UNMutableNotificationContent()
notificationContent.title = NSString.localizedUserNotificationString(forKey: "new title", arguments: nil)
notificationContent.body = "new body"
request.content = notificationContent // ⛔️ request.content is read only.
}
}
})
As you can see I can't modify original request.
Update 3.
Had go with second "delete first" option. Noticed that calling removePendingNotificationRequests and schedule after, still gives me old notification version. I had to add 1 second delay between calling removePendingNotificationRequests and center.add(request).
Marked Chris' answer as accepted but feel free to share better option.

The problem is that you're not modifying the existing notification, and instead adding a new notification with a duplicate identifier.
Let's tackle the duplicate issue first, the reason this duplicate notification doesn't show up is because the identifier isn't unique. From the docs:
(if identifier is not unique, notifications are not delivered).
You have two options. You can 1) modify the existing Notification, or 2) remove it and add the new one.
For 1, you already have the request, instead of pulling the trigger and identifier out of it, just replace request.content with your updated notificationContent.
For 2, you would just need to add a line before your Add:
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [request!.identifier])

After I've requested to Allow Notifications:
I trigger a notification right from my viewDidLoad but then also trigger another one with the same identifier. At the end the the updatedBody/updatedTitle show up.
import UIKit
import UserNotifications
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let content = UNMutableNotificationContent()
content.title = "Scheduled Task"
content.body = "dumbBody"
content.badge = 1
content.sound = UNNotificationSound.default()
content.categoryIdentifier = "alertCategory"
UNUserNotificationCenter.current().delegate = self
//Setting time for notification trigger
let trigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 3.0, repeats: false)
let request = UNNotificationRequest(identifier:"myIdentifier", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: {_ in print(" was registered")})
updateNotification()
}
My update function
func updateNotification(){
let center = UNUserNotificationCenter.current()
var request : UNNotificationRequest?
center.getPendingNotificationRequests{ notifications in
for notificationRequest in notifications{
if notificationRequest.identifier == "myIdentifier"{
request = notificationRequest
center.removeAllPendingNotificationRequests() // Removing this line or keeping it makes NO difference
}
}
let newTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 5.0, repeats: false)
// Update notificaion conent.
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "UpdatedTitle"
notificationContent.body = "updatedBody"
let updateRequest = UNNotificationRequest(identifier: request!.identifier, content: notificationContent, trigger: newTrigger)
UNUserNotificationCenter.current().add(updateRequest, withCompletionHandler: { (error) in
print("successfully updated")
if error != nil {
print("🚫 Couldn't update notification \(error!.localizedDescription)")
}
})
}
}
}
In the above snippet: Removing center.removeAllPendingNotificationRequests() would make no difference. Still I would receive the updatedNotification.
For handling incoming notifications
extension ViewController:UNUserNotificationCenterDelegate{
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
print("original identifier was : \(response.notification.request.identifier)")
print("original body was : \(response.notification.request.content.body)")
print("Tapped in notification")
switch response.actionIdentifier {
default:
print("some action was clicked")
}
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("Notification being triggered")
completionHandler( [.alert,.sound,.badge])
}
}

Related

Show Hyper Link in remote notification with Message

I need to show hyperlink in my remote notification along with Title and body. I have done some thing like this:
#IBAction func openPDFButtonPressed(_ sender: Any) {
self.scheduleNotification()
}
func scheduleNotification() {
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = "Download The Receipt"
content.body = "Kindly Download your purchase bill"
content.categoryIdentifier = "PDF"
content.userInfo = ["customData": "http://www.pdf995.com/samples/pdf.pdf"]
content.sound = UNNotificationSound.default
var dateComponents = DateComponents()
dateComponents.hour = 10
dateComponents.minute = 30
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
center.add(request)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
// pull out the buried userInfo dictionary
let userInfo = response.notification.request.content.userInfo
print("Test: \(userInfo)")
if let customData = userInfo["customData"] as? String {
print("Custom data received: \(customData)")
let storyBoard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let newViewController = storyBoard.instantiateViewController(withIdentifier: "PDFViewController") as! PDFViewController
newViewController.strURL = customData
self.present(newViewController, animated: true, completion: nil)
}
completionHandler()
}
In this I am sending url in user info but I need this url as hyperlink that shows when notification appears. and when I click on that hyperlink it will open this URL in webView.
Loading URL in webview part is done. Just need to know how can I show this url as hyperlink on notification.
kindly help me out.
You cannot control the way a notification is shown by iOS, but you can declare specific actions for your notifications. See here: https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types
This lets you add your custom 'Pay, Reject, Block' actions to the notification. iOS will offer the choices to the user, and notify your app in the background when the user selects one, but you will not be able to show the URL, only text.
The only way to show a custom dialog following a notification is if you get the notification while the app is in the foreground, because then the OS doesn't show the notification, it only notifies your app, and you can then decide to show whatever UI suits you. But that kind of goes against the idea of the notification, which can come in at any time, in particular when your app is not running.

Usernotification framework badge does not increase

I am using UserNotification framework in my app and sending local notifications (not push notifications), and I want to set the badge to the number of notifications received so what I did was to set the number of notifications received into a user default then I tried to assign the value to the badge to get me a badge number but the badge number would not increase. This is my code below
To set value of received notification
center.getDeliveredNotifications { notification in
UserDefaults.standard.set(notification.count, forKey: Constants.NOTIFICATION_COUNT)
print("notification.count \(notification.count)")
print(".count noti \(UserDefaults.standard.integer(forKey: Constants.NOTIFICATION_COUNT))")
}
This accurately prints the number of notification received and when I decided to set it to my badge it only shows 1
content.badge = NSNumber(value: UserDefaults.standard.integer(forKey: Constants.NOTIFICATION_COUNT))
I have no idea why the value does not increase every time. Any help would be appreciated.
Or if it is possible to always update the badge anywhere in the app.
Send the local notifications like so:
func sendNotification(title: String, subtitle: String, body: String, timeInterval: TimeInterval) {
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: { pendingNotificationRequests in
//Use the main thread since we want to access UIApplication.shared.applicationIconBadgeNumber
DispatchQueue.main.sync {
//Create the new content
let content = UNMutableNotificationContent()
content.title = title
content.subtitle = subtitle
content.body = body
//Let's store the firing date of this notification in content.userInfo
let firingDate = Date().timeIntervalSince1970 + timeInterval
content.userInfo = ["timeInterval": firingDate]
//get the count of pending notification that will be fired earlier than this one
let earlierNotificationsCount: Int = pendingNotificationRequests.filter { request in
let userInfo = request.content.userInfo
if let time = userInfo["timeInterval"] as? Double {
if time < firingDate {
return true
} else {
//Here we update the notofication that have been created earlier, BUT have a later firing date
let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
newContent.badge = (Int(truncating: request.content.badge ?? 0) + 1) as NSNumber
let newRequest: UNNotificationRequest =
UNNotificationRequest(identifier: request.identifier,
content: newContent,
trigger: request.trigger)
center.add(newRequest, withCompletionHandler: { (error) in
// Handle error
})
return false
}
}
return false
}.count
//Set the badge
content.badge = NSNumber(integerLiteral: UIApplication.shared.applicationIconBadgeNumber + earlierNotificationsCount + 1)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval,
repeats: false)
let requestIdentifier = UUID().uuidString //You probably want to save these request identifiers if you want to remove the corresponding notifications later
let request = UNNotificationRequest(identifier: requestIdentifier,
content: content, trigger: trigger)
center.add(request, withCompletionHandler: { (error) in
// Handle error
})
}
})
}
(You may need to save the requests' identifiers (either in user defaults or core data if you'd like to update them, or even cancel them via removePendingNotificationRequests(withIdentifiers:))
You can call the above function like so:
sendNotification(title: "Meeting Reminder",
subtitle: "Staff Meeting in 20 minutes",
body: "Don't forget to bring coffee.",
timeInterval: 10)
Declare your view controller as a UNUserNotificationCenterDelegate:
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
override func viewDidLoad() {
super.viewDidLoad()
UNUserNotificationCenter.current().delegate = self
}
//...
}
And to handle interacting with the notification, update the badge of the app, and the badge of the upcoming notifications:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
//UI updates are done in the main thread
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber -= 1
}
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: {requests in
//Update only the notifications that have userInfo["timeInterval"] set
let newRequests: [UNNotificationRequest] =
requests
.filter{ rq in
return rq.content.userInfo["timeInterval"] is Double?
}
.map { request in
let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
newContent.badge = (Int(truncating: request.content.badge ?? 0) - 1) as NSNumber
let newRequest: UNNotificationRequest =
UNNotificationRequest(identifier: request.identifier,
content: newContent,
trigger: request.trigger)
return newRequest
}
newRequests.forEach { center.add($0, withCompletionHandler: { (error) in
// Handle error
})
}
})
completionHandler()
}
This updates the app badge by decreasing it when a notification is interacted with ie tapped. Plus it updates the content badge of the pending notifications. Adding a notification request with the same identifier just updates the pending notification.
To receive notifications in the foreground, and increase the app badge icon if the notification is not interacted with, implement this:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber += 1
}
completionHandler([.alert, .sound])
}
Here are some gifs:
1st: Receiving local notifications increases the app badge. Whereas interacting with a notification decreases the app badge.
2nd: Receiving local notifications when the app is killed (I used a trigger timeInterval of 15s in this).
3rd: Receiving notification whilst in the foreground increases the app badge unless the user interacts with it.
The complete class used in my test project looks like this:
import UIKit
import UserNotifications
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
var bit = true
#IBAction func send(_ sender: UIButton) {
let time: TimeInterval = bit ? 8 : 4
bit.toggle()
sendNotification(title: "Meeting Reminder",
subtitle: "Staff Meeting in 20 minutes",
body: "Don't forget to bring coffee.",
timeInterval: time)
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
UNUserNotificationCenter.current().delegate = self
}
func sendNotification(title: String, subtitle: String, body: String, timeInterval: TimeInterval) {
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: { pendingNotificationRequests in
DispatchQueue.main.sync {
let content = UNMutableNotificationContent()
content.title = title
content.subtitle = subtitle
content.body = body
let firingDate = Date().timeIntervalSince1970 + timeInterval
content.userInfo = ["timeInterval": firingDate]
let earlierNotificationsCount: Int = pendingNotificationRequests.filter { request in
let userInfo = request.content.userInfo
if let time = userInfo["timeInterval"] as? Double {
if time < firingDate {
return true
} else {
let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
newContent.badge = (Int(truncating: request.content.badge ?? 0) + 1) as NSNumber
let newRequest: UNNotificationRequest =
UNNotificationRequest(identifier: request.identifier,
content: newContent,
trigger: request.trigger)
center.add(newRequest, withCompletionHandler: { (error) in
// Handle error
})
return false
}
}
return false
}.count
content.badge = NSNumber(integerLiteral: UIApplication.shared.applicationIconBadgeNumber + earlierNotificationsCount + 1)
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval,
repeats: false)
let requestIdentifier = UUID().uuidString //You probably want to save these request identifiers if you want to remove the corresponding notifications later
let request = UNNotificationRequest(identifier: requestIdentifier,
content: content, trigger: trigger)
center.add(request, withCompletionHandler: { (error) in
// Handle error
})
}
})
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber += 1
}
completionHandler([.alert, .sound])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber -= 1
}
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: {requests in
let newRequests: [UNNotificationRequest] =
requests
.filter{ rq in
return rq.content.userInfo["timeInterval"] is Double?
}
.map { request in
let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
newContent.badge = (Int(truncating: request.content.badge ?? 0) - 1) as NSNumber
let newRequest: UNNotificationRequest =
UNNotificationRequest(identifier: request.identifier,
content: newContent,
trigger: request.trigger)
return newRequest
}
newRequests.forEach { center.add($0, withCompletionHandler: { (error) in
// Handle error
})
}
})
completionHandler()
}
}
I'm assuming this all a local notification.
AFAIK there is solution to your question!
When the notification arrives, you're either in foreground or background.
foreground: you get the userNotificationCenter(_:willPresent:withCompletionHandler:) callback but I don't think in that case you'll want to increase the badge right? Because the user has just seen it. Though I can imagine where you might need to do such. Suppose your app is like WhatsApp and the user has the app opened and is sending a message to his mother. Then a message from his father arrives. At this point he hasn't opened the messages between him and his father yet he sees the notification. In your willPresent you could query the getDeliveredNotifications and and adjust your badge count.
background: for iOS10+ version for local notifications you're out of luck! Because there is NO callback for you. The notification gets delivered to the OS and that's it! There use to be a named application:didReceiveLocalNotification:
but that's deprecated. For more on that see here
When user taps (foreground or backend) then you'll get the userNotificationCenter(_:didReceive:withCompletionHandler:)
but that has no use again because the user has already acknowledged receiving the notification and increasing the badge in this case doesn't make sense.
Long story short AFAIK there is nothing you can do for local notifications.
If it's a remote notification then in the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) you can query the delivered notifications and increase the badge count...
EDIT:
Since the badgeCount is attached to the arriving notification, then if you can update its badgeCount prior to arrival then you're all good. e.g. at 12pm you can always query the list of pendingNotifications. It will give you all the notifications arriving after 12pm and update the badgeCount on them if necessary e.g. decrease their badgeCount if some delivered notifications are read. For a complete solution on this see see Carspen90's answer. The gist of his answer is
for any new notification you want to send:
get the pendingNotifications
filter notifications which their firingDate is sooner than the new to be sent notification and get its count
set the new notification's badge to app's badgeCount + filteredCount + 1
if any of the pending notifications have a firingDate greater than the new notification we just added then we will increase the badgeCount on the pending notification by 1.
obviously again whenever you interact with delivered notifications, then you have to get all pendingNotifications again and decrease their badgeCount by 1
CAVEAT:
You can't do such for notifications which their trigger is based on location because obviously they don't care about time.

How to show multiple local notifications?

I have an messaging app,I am using VoIP notifications to send acknowledgement to users. I am firing a local notification each time the PushKit delegate is called.
The current scenario is that the previous notification gets removed and is replaced by a newer one. Is there a way to manage local notifications such that the user can see multiple notifications in their device?
This is the code I have tried:
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Title"
notificationContent.subtitle = "Subtitle"
notificationContent.body = "Body"
// Add Trigger
let notificationTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
// Create Notification Request
let notificationRequest = UNNotificationRequest(identifier: "cocoacasts_local_notification", content: notificationContent, trigger: notificationTrigger)
// Add Request to User Notification Center
UNUserNotificationCenter.current().add(notificationRequest) { (error) in
if let error = error {
print("Unable to Add Notification Request (\(error), \(error.localizedDescription))")
}
}
P.S : I don't want to schedule local notification for a later period
Using for loop to register multiple Notification with the unique identifier https://developer.apple.com/documentation/usernotifications/unnotificationrequest/1649634-identifier?language=objc
let notificationRequest = UNNotificationRequest(identifier: "cocoacasts_local_notification", content: notificationContent, trigger: notificationTrigger)
you should change this identifier "cocoacasts_local_notification" to dynamically reset the unique identifier
let notification = UNMutableNotificationContent()
let notificationTrigger = UNCalendarNotificationTrigger(dateMatching: dayComponent, repeats: true)
let lnMessageId:String = messageDict["Id"] as! String
let dayRequest = UNNotificationRequest(identifier: lnMessageId , content: notification, trigger: notificationTrigger)
UNUserNotificationCenter.current().add(dayRequest, withCompletionHandler: {(_ error: Error?) -> Void in
if error == nil
{
//print("success")
}
else
{
//print("UNUserNotificationCenter Error : \(String(describing: error?.localizedDescription))")
}
})

How do I set up a repeating daily notification with different content each time?

I'm working on an app that gives you a notification at mid-day. This notification is supposed to be different every day.
I got the notifications themselves working:
let notificationOptions: UNAuthorizationOptions = [.alert, .sound];
UNUserNotificationCenter.current().requestAuthorization(options: notificationOptions) { (granted, error) in
if !granted {
print("Something went wrong")
} else {
let content = UNMutableNotificationContent()
content.body = getRandomDailyString()
content.sound = UNNotificationSound.default()
let date = DateComponents(hour: 12, minute: 15)
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
let request = UNNotificationRequest(identifier: "Daily String", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error.localizedDescription)
}
}
}
}
What's happening now is that the getRandomDailyString()-function is called, it returns a string, and a repeating notification is set which does appear the specified time, but always has the same content.
How would I go about making a notification that gives a unique content every day?
Cannot test it right now, but try it and tell me
If it is not inside a function I would place it in one. Then you call it from your delegate method. Don't forget to change it to not be repeatable.
You must have a class to handle its delegates methods, can be your AppDelegate or any other class you create.
Delegate UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
x()
}
func x() {
let notificationOptions: UNAuthorizationOptions = [.alert, .sound];
UNUserNotificationCenter.current().requestAuthorization(options: notificationOptions) { (granted, error) in
if !granted {
print("Something went wrong")
} else {
let content = UNMutableNotificationContent()
content.body = getRandomDailyString()
content.sound = UNNotificationSound.default()
let date = DateComponents(hour: 12, minute: 15)
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: false)
let request = UNNotificationRequest(identifier: "Daily String", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error) in
if let error = error {
print(error.localizedDescription)
}
}
}
}
}
This answer can help you too
Getting local notifications to show while app is in foreground Swift 3
Ok, here is the solution for this.
It is NOT possible to schedule a repeating notification with a random content each day. The content is defined when the notification is scheduled, and cannot be changed later.
What you CAN do it schedule up to 64 notifications in advance though. So you can schedule 64 unique notifications for the next 64 days, and then whenever you open the app check how many remain, and fill up the notification-schedule up to 64 again.
If you don't open the app after 64 days the notifications stop coming though :P

Declaring the title, subtitle, and body of a local notification

func scheduleNotification(inSeconds: TimeInterval, completion: #escaping (_ Success: Bool) -> ()) {
let notif = UNNotificationContent()
notif.title = NSString.localizedUserNotificationString(forKey: "New Notification", arguments: nil)
notif.subtitle = "These are great!"
notif.body = "The new notification options are awesome!"
let notifTrigger = UNTimeIntervalNotificationTrigger(timeInterval: inSeconds, repeats: false)
let request = UNNotificationRequest(identifier: "myNotification", content: notif, trigger: notifTrigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: {error in
if error != nil {
print(error)
completion(false)
} else {
completion(true)
}
})
}
I am following along with a Udemy video and I am having a problem setting the title, subtitle, and body of a local notification. I get the same error for all three assignment lines.
Cannot assign to property: 'xxx' is a get-only property.
I quickly looked it up in the documentation. It says:
Do not create instances of this class directly. (...) For local notifications, create a UNMutableNotificationContent object and configure the contents of that object instead.
Source: https://developer.apple.com/documentation/usernotifications/unnotificationcontent
I'm not really familiar with this class but I think UNNotificationContent fills it content automatically from received data.
I'm not entirely sure if this is what you're looking for, but maybe try using UNMutableNotificationContent instead of UNNotificationContent:
let notif = UNMutableNotificationContent()

Resources