New to Xcode and Swift. My app has a timer that counts down. I'd like for the countdown to be visible from the lock screen as a notification, but I can't figure out how to (of if it's even possible to) update the content of an existing local notification.
The only solution I've found so far is to cancel the current notification and show a new one every second, which is not ideal.
Code:
struct TimerApp: View {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
#State private var isActive: Bool = true // whether not timer is active
#State private var timeRemaining: Int = 600 // 60 seconds * 10 mins = 10 min-countdown timer
var body: some View {
// body stuff
// toggle isActive if user stops/starts timer
}.onReceive(timer, perform: { _ in
guard isActive else { return }
if timeRemaining > 0 {
// would like to update current notification here
// *******
// instead, removing and adding a new one right now
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
addNotification()
timeRemaining -= 1
} else {
isActive = false
timeRemaining = 0
}
}
func addNotification() {
let center = UNUserNotificationCenter.current()
let addRequest = {
let content = UNMutableNotificationContent()
content.title = "App Title"
content.body = "Time: \(timeFormatted())"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.0001, repeats: false)
let request = UNNotificationRequest(identifier: "onlyNotification", content: content, trigger: trigger)
center.add(request)
}
center.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
addRequest()
} else {
center.requestAuthorization(options: [.alert, .badge]) { success, error in
if success {
addRequest()
} else if let error = error {
print("error :( \(error.localizedDescription)")
}
}
}
}
}
func timeFormatted() -> String {
// converts timeRemaining to 00:00 format and returns string
}
}
And here is what the hilariously bad solution looks like right now.
Currently it's not possible to update a pending or delivered local notification request.
Just like you guessed, in order to deliver a notification with a different content instead, you need to remove the pending request using UNNotificationCenter's methods removePendingNotificationRequests(withIdentifiers:) or removeAllPendingNotificationRequests() and add a new request with the updated content.
Related
Im making a standalone Apple Watch app, testing on device, and its meant to play several bells with vibration (app can be closed and alerts should play) in a period of like 5 seconds or so, Im using local notifications for this and no matter what I try I only hear the first notification bell or if I extend in time maybe 2 bells, in the past alerts summary these consecutive notifications are listed but the bells are not played. I know there is a way to do this as I saw an app that plays several bells like this and is standalone Apple Watch app too. Im using swift, how can I do that? Thanks for any help
I made a separate code only to trigger some alerts in 10 seconds from now to show better my issue here:
import SwiftUI
import UserNotifications
struct ContentView: View
{
var body: some View
{
VStack
{
Button("Request Permission")
{
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
if success {
print("All set!")
} else if let error = error {
print(error.localizedDescription)
}
}
}
Button("Schedule Notification")
{
let content = UNMutableNotificationContent()
content.title = "First notification"
content.subtitle = "Whatever"
content.sound = UNNotificationSound.defaultCritical
// show this notification 10 seconds from now
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 10, repeats: false)
// choose a random identifier
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
//----------/
var content2 = [UNMutableNotificationContent](repeating: UNMutableNotificationContent(), count:100)
var trigger2 = [UNNotificationTrigger](repeating: UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)
, count:100)
var request2 = [UNNotificationRequest](repeating: UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger),count:100)
// add notification request
UNUserNotificationCenter.current().add(request)
for i in 0..<100
{
content2[i] = UNMutableNotificationContent()
content2[i].title = "Chained notifications"
content2[i].subtitle = "whatever"
content2[i].sound = UNNotificationSound.defaultCritical
//content2[i].interruptionLevel = .active
content2[i].interruptionLevel = .critical
content2[i].categoryIdentifier="category\(i)"
content2[i].threadIdentifier="asadasaa\(i)"
trigger2[i] = UNTimeIntervalNotificationTrigger(timeInterval: 10+Double(i/4), repeats: false)
// choose a random identifier
request2[i] = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request2[i])
}
//-----------/
}
Button("Stop")
{
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I tried several intervals between notifications, making them more separate or closer to each other and only first one plays or 2 or more bells if there is some longer intervals but most are grouped and only hear like one every second at the most I think, I read somewhere these are meant to be payed in 0.9 seconds at the shortest time but as said I have seen an app able to do this so there has to be a way
I was suggested to use different thead or so, but didnt work, tried other parameters of this too like to change revelance or so but nothing worked
The issue may work the same in iPhone if you are more familiar with it, so if you know a workaround for this let me know
In my app, a user will set date and time for reminder, and the phone needs to send local notification at that particular time even when the app is closed.
Able to send a notification at particular time using the below method. but it doesn't work when app is closed:
public func simpleAddNotification(hour: Int, minute: Int, identifier: String, title: String, body: String) {
// Initialize User Notification Center Object
let center = UNUserNotificationCenter.current()
// The content of the Notification
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.badge = 1
// The selected time to notify the user
var dateComponents = DateComponents()
dateComponents.calendar = Calendar.current
dateComponents.hour = hour
dateComponents.minute = minute
// The time/repeat trigger
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
// Initializing the Notification Request object to add to the Notification Center
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
// Adding the notification to the center
center.add(request) { (error) in
if (error) != nil {
print(error!.localizedDescription)
}
}
}
I set up a dummy project to reproduce the issue I'm seeing. In my ContentView, I schedule some repeating notifications.
struct ContentView: View {
var body: some View {
VStack {
Button("Schedule notifications") {
let content = UNMutableNotificationContent()
content.title = "Title"
content.body = "body"
content.sound = UNNotificationSound.default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60, repeats: true)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
Button("Request Permission") {
let current = UNUserNotificationCenter.current()
current.requestAuthorization(
options: [.sound, .alert],
completionHandler: { completed, wrappedError in
guard let error = wrappedError else {
return
}
})
}
}
}
}
Then in my AppDelegate, I attempt to cancel those repeating notifications before the app terminates.
func applicationWillTerminate(_ application: UIApplication) {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
print("============== removing all notifications")
}
What I'm finding is that my scheduled notifications are still delivered, even though I can see my print statement in the Xcode console. But if I run the same test on an iPhone, my notification is not delivered, as expected.
Am I doing something wrong, or is this a bug? I'm using 13.4.1 on iPad, and 13.3.1 on iOS
I am also facing the same problem. Found out that applicationWillTerminate method has approximately five seconds to perform any tasks and return. And removeAllPendingNotificationRequests returns immediatly but removes scheduled notification asynchronously in secondary thread.
I think 5 seconds are more than necessary to clear notification.
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.
I have this function below:
#objc func decrementBadges(){
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests(completionHandler: { (notifications) in
print("count", notifications.count)
for notification in notifications{
//center.removePendingNotificationRequests(withIdentifiers: notification.title)
print(notification.content.badge)
print(notification.description)
}
})
}
I am trying to decrement the badge numbers on all the pending notifications. Is this possible? The notification.content.badge is read only and I can't figure out a way to set it.
What you will probably have to do is to cancel the notifications that you want to change and then schedule new ones with the new badge numbers. You can do this by getting the UNNotificationRequest identifiers from each of the notifications in that array you have and then calling center.removePendingNotificationRequests(withIdentifiers: [request, identifiers, of, notifications, to, remove])
Then schedule the updated notifications.
The documentation for UNNotificationRequest.identifier does say
If you use the same identifier when scheduling a new notification, the system removes the previously scheduled notification with that identifier and replaces it with the new one.
So you shouldn't have to remove them first, but that's up to you.
#objc func decrementBadges(eventId: String){
let center = UNUserNotificationCenter.current()
var arrayOfNotifications: [UNNotificationRequest] = []
center.getPendingNotificationRequests(completionHandler: { (notifications) in
print("notifications.count", notifications.count)
for notification in notifications{
print(notification.description)
let content = UNMutableNotificationContent()
content.title = notification.content.title
content.subtitle = notification.content.subtitle
content.body = notification.content.body
content.sound = notification.content.sound
content.userInfo = notification.content.userInfo
content.categoryIdentifier = notification.content.categoryIdentifier
if notification.content.badge != nil {
var int : Int = Int(notification.content.badge!)
int -= 1
content.badge = NSNumber(value: int)
}
let infoDict = content.userInfo as NSDictionary
let notificationId:String = infoDict.object(forKey: "IDkey") as! String
if notificationId != eventId {
let request = UNNotificationRequest(
identifier: notification.identifier,
content: content,
trigger: notification.trigger
)
arrayOfNotifications.append(request)
}
}
})
self.clearNotifications()
for notification in arrayOfNotifications {
UNUserNotificationCenter.current().add(
notification, withCompletionHandler: nil)
}
}