How to dismiss 3D-touched notification with custom action? - ios

I am implementing a kind of alert notification for my app, using UserNotifications. When 3D-touching this notification, I want it to display some 'alert information', and the option to "Snooze" the alert, which will effectively repeat the notification in 5 minutes.
This is all working fine, except that the notification doesn't get dismissed when I click the Snooze button.
I am using a NotificationContentExtension to modify the content, with the categoryIdentifier "ReminderNotificationExtensionCategory". Then I have created my category in code like this:
let snooze = UNNotificationAction(identifier: "snoozeActionIdentifier", title: NSLocalizedString("SNOOZE", comment: ""), options: .foreground)
let reminderCategory = UNNotificationCategory(identifier: "ReminderNotificationExtensionCategory", actions: [snooze], intentIdentifiers: [], options: .customDismissAction)
UNUserNotificationCenter.current().setNotificationCategories([reminderCategory])//Not sure if I should set this every time or just once..
Then I create my notification object
let content = UNMutableNotificationContent()
content.categoryIdentifier = "ReminderNotificationExtensionCategory"
content.title = "test"
content.body = "test"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 3, repeats: false) //Fire in 3 seconds
let request = UNNotificationRequest(identifier: "someIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { (error:Error?) in /**/}
Now, my notification will be sent in 3 seconds.
I lock my screen and receive it, and it looks great. When I 3D-touch it, my extension is launched, and I get a sneak peak on a UIView I have set up, along with my "Snooze"-button below the notification, as shown in the image:(content removed)
When I click the "Snooze"-button, a UNNotificationContentExtension delegate-method will be called, specifically func didReceive(_ response: completion:)
The completion only accepts a value of UNNotificationContentExtensionResponseOption, which would be either .dismiss, .doNotDismiss, or .dismissAndForwardAction.
Just for testing, I have done this:
func didReceive(_ response: UNNotificationResponse, completionHandler completion: #escaping (UNNotificationContentExtensionResponseOption) -> Void) {
print("This is happening. ActionIdentifier: ", response.actionIdentifier)
completion(.dismiss)
}
And when I debug the notification extension, the print does really happen. It prints out "This is happening. ActionIdentifier: snoozeActionIdentifier". However, the notification is not being dismissed.
If I change from .dismiss to .doNotDismiss, nothing changes. It still does not dismiss. If I change to .dismissAndForwardAction, it dismisses and opens my app.
I can't find anyone with this problem. I want .dismiss to dismiss my notification. Why isn't it? Am I doing something wrong?
Edit
I see now that these two scenarios are happening:
I 3D-touch my notification from the lock screen, then tap outside it, my notification 'collapses' and the regular notification is visible as it was before I 3D-touched it.
I 3D-touch my notification from the lock screen, tap 'Snooze' (and nothing happens), then tap outside the notification, and the regular notification disappears.
So the completion callback actually does dismiss the regular notification, but not the extension. Why? How can I dismiss the extension? Is that a manual method I must call?

I see my stupid mistake now..
As you can see in my code above, I have instantiated the action button in my category as such:
let snooze = UNNotificationAction(identifier: "snoozeActionIdentifier", title: NSLocalizedString("SNOOZE", comment: ""), options: .foreground)
At the very end, I specify the option .foreground. This should simply be an empty array [] for my purpose.
I thought I had to specify an option, and that the input type was of UNNotificationActionOptions, which only had the options .authenticationRequired ("no I don't want that") .destructive ("no I don't want a red button") and .foreground ("well, if I have to pick one if these.."). Turns out that simply specifying an empty array [] did the trick.

Related

Problem in scheduling repeating local notification every hour

Im trying to schedule local notification every hour. The problem is that im getting the notification in such irregular time. Sometime in 10 minutes, or in second, or 30 minutes. Sometime i get like 10 notifications in the same second suddenly. I have no idea what is the mistake im doing here. I apologize in advance, i just started learning swift.
This is my code.
#objc func LocalNotificationHour() {
let user = UNUserNotificationCenter.current()
user.requestAuthorization(options: [.alert,.sound]) { (granted, error) in}
let content = UNMutableNotificationContent()
content.title = "Local Notification"
content.body = "Test."
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: (60*60), repeats: true)
let uuid = UUID().uuidString
let request = UNNotificationRequest(identifier: uuid, content: content, trigger: trigger)
user.add(request) { (error) in print("Error")}
}
Is it possible that your function is being run multiple times, and therefore scheduling more than one notification to repeat? That would account for receiving multiple notifications at once/at times when you're not expecting them.
If that is the case, try adding the line user.removeAllPendingNotificationRequests() under let user = UNUserNotificationCenter.current(). This will remove any pending notifications your app may have scheduled before scheduling a new one.
It's because you are running your code once with one hour logic.
And then you are changing logic for two hour logic.
So basically both notification is triggering.
Either delete the app or previously scheduled notifications or change the identifier.

How do I make a UNUserNotification that's consistent through app states?

I make a UNUserNotification like this:
let content = UNMutableNotificationContent()
content.title = "a title"
content.body = "a body"
content.sound = .default
content.categoryIdentifier = "\(type(of: self))"
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 0.01, repeats: false)
)
let category = UNNotificationCategory(
identifier: content.categoryIdentifier,
actions: [
UNNotificationAction(identifier: Strings.accept.id, title: Strings.accept.title, options: [.authenticationRequired, .foreground]),
UNNotificationAction(identifier: Strings.reject.id, title: Strings.reject.title, options: [.authenticationRequired, .destructive])
],
intentIdentifiers: [],
options: .customDismissAction
)
It will show a notification with a title and body, and two custom action buttons (accept and reject, it's an incoming call). It plays a sound too, but for some reason not if the app is foregrounded.
I also try to fake a persistent notification with it, like the way Whatsapp does it by repeating a local notification every ~4 seconds, but the second time it shows the notification the sound is cut off a bit. It's not the sound of the first one, the second one shows with its own sound playing but this time it doesn't fully play out. The sound is only ~1-2 seconds.
I don't understand why there are these differences, how do I make it always play a sound (foreground, background, not running) and how do I make sure the sound is not cut off the second time the notification is shown?
When notifications are received whilst the app is running the notifications are normally silenced so that the app can handle them in a more native way.
If I remember rightly, receiving the notification will call:
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
#escaping (UNNotificationPresentationOptions) -> Void) {
if someCondition {
completionHandler(.alert)
} else {
completionHandler(.init(rawValue: 0))
}
}
This method is called when the notification is received and if the app is running in the foreground, it will normally be silenced. The completion handler determines what action should be taken and whether or not a notification should show or a sound made etc.
In this case if someCondition is true then it should show the notification natively as it normally would, otherwise dont do anything.
Try calling completionHandler(.alert) in this method in your app and your notifications should show
Checkout the documentation here

iOS : How to remove remote notifications if app is not active?

I want to clear remote notifications so they don't add up in the Notification Center (like when you get a video call in WhatsApp or Messenger, only the last notification is displayed).
I tried to call (in didReceiveRemoteNotification):
let center = UNUserNotificationCenter.current()
center.removeDeliveredNotifications(withIdentifiers: ["notification_identifier"])
But it gets called only if the app is active. How can I do this if the app is in another state?
Thanks for your help.
After some research and thanks to Paulw1's answer, I found out there are two ways of doing this:
Remote only
Notifications can be collapsed remotely, you only have to send the notification with apns-collapse-id as a request header. Please note that it's only supported in HTTP/2 though. More information here.
Silent remote + local notification
The other way consists in sending a silent remote notification, with this kind of payload:
{
"type": "notification_type",
"aps" : {
"content-available": 1
}
}
It will call didReceiveRemoteNotification even if the app's state is inactive or background. Then, I create a local notification request (needs using UserNotifications, available from iOS10) :
let content = UNMutableNotificationContent()
content.body = "Notification message"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.25, repeats: false)
let request = UNNotificationRequest(identifier: "identifierToUpdate", content: content, trigger: trigger)
self.center.add(request, withCompletionHandler: nil)
The key to update the previous notification is to use the same request identifier.

Schedule a local notification(silent) to perform a custom task in background + swift ios

In our app, the user will clock-in when he starts his work. If the user forgets to clock-out, we will have to automatically clock him out after 24 hours from the clock-in time. The app might not be in the active/background state for such a long time. It might be terminated. So our idea is to post a local notification through which will execute the code in the background to clock him out. This notification has to be a silent notification. But our understanding from the research is that local notifications cannot be silent. So is there any other way we could achieve this? Or can we actually schedule a silent local notification?
class func generateLocalNotificationWith(timeInterval : TimeInterval, title : String, message : String, mobileTimeClockId: Int)
{
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
let content = UNMutableNotificationContent()
//adding title, subtitle, body and badge
content.title = title
content.body = message
let userInfoDictionary = ["badge":"0","content-available": "1"]
let dict = ["aps": userInfoDictionary, "MobileTimeClockId": mobileTimeClockId] as [String : Any]
content.userInfo = dict
//getting the notification trigger
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
//getting the notification request
let request = UNNotificationRequest(identifier: "SimplifiedIOSNotification", content: content, trigger: trigger)
//adding the notification to notification center
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
If you want a notification to be silent and do background work it has to be a push notification. A local notification can't wake your app without some user interaction.
The best way to do what you want is to use Core Location region monitoring to wake your app when the user physically leaves their workplace. Region monitoring can wake your app silently in the background and let you do some work. The problem is that it's not 100% assured that it will be triggered and the user will need to accept background location permissions.

app is sending multiple notifications

I want my app to send a single notification at a given time and repeat daily. The notification sends fine but it sends multiple notifications. So instead of sending 1, it sends 5. How can I fix this with code. I have 2 viewcontrollers if that is meaningful.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// ask for permission for notifications
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound], completionHandler: {didAllow, error in
})
// making the content of the notification
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = "Get Motivated"
content.body = "Need Some Motivation? We've Got Plenty!"
content.sound = UNNotificationSound.default()
//triggering notification
var dateComponents = DateComponents()
dateComponents.hour = 13
dateComponents.minute = 35
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
center.add(request)
It's a little hard to be precise without seeing more of your code so forgive me if I misunderstood something. Nevertheless, here are some suggestions:
You are sending notifications every time your view controller's "viewDidLoad" method is called. For a bit of context, in earlier versions of iOS, in order to save memory, views were unloaded from memory when views were removed from view hierarchy (say, after a transition to a different full screen view); it is not done anymore, but "viewDidLoad" is not guaranteed to be called only once during the life time of a given view controller. In addition to that, if you instantiate and present a view controller of this class more than once, "viewDidLoad" will be called multiple times.
Your code, also, does not seem to take into account a possibility that the app might be launched more than once.
In order to ensure that only one notification is scheduled, you can:
Check for already scheduled notifications and see if the one you are trying to add is a duplicate (you can use UNUserNotificationCenter.getPendingNotificationRequestsWithCompletionHandler method for that);
Remove all already scheduled notifications and add new ones in their place (see UNUserNotificationCenter.removeAllPendingNotificationRequests).
The method you choose, of course, depends on the logic of your app.
Good luck!

Resources