UNCalendarNotificationTrigger not initiating - ios

Okay - I am totally frustrated with this piece of code right now and ready to give up! Basically when simulating to either Simulator or actual device I get the requestAuthorisation to work no problem but the trigger does not initiate ever. I have followed several guys online and their code worked with ease! When I use a button to initiate a UNTimeIntervalNotificationTrigger it works but that is not what I want. Currently testing in iOS 14.3 as target for build. Rest of the App builds no problem. What am I doing wrong?! Cannot help but think that somewhere along the line of trying to get it to work I might have damaged something in info.plist or similar?! I have tested to repeat the trigger and not to repeat but neither works.
override func viewDidLoad() {
super.viewDidLoad()
//NOTIFICATIONS
// Step 1 - Ask the use for permission to notify
let randVerseCenter = UNUserNotificationCenter.current()
randVerseCenter.requestAuthorization(options: [.alert, .sound]){ (granted, error) in
if granted {
print("Yay - request authorisation worked!")
} else {
print ("D'oH - request Authorisation did not work!")
}
}
// Step 2 - Create the Notification Content
let randVerseContent = UNMutableNotificationContent()
randVerseContent.title = "Random Reference"
randVerseContent.body = "Random Verse"
randVerseContent.sound = UNNotificationSound.default
// Step 3 - Create the trigger for the notification by delay
let randVerseDate = Date().addingTimeInterval(30)
let randVerseDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: randVerseDate)
let randVerseTrigger = UNCalendarNotificationTrigger(dateMatching: randVerseDateComponents, repeats: true)
// Step 4 - Creating the request
let randVerseUUIDString = UUID().uuidString
let randVerseRequest = UNNotificationRequest(identifier: randVerseUUIDString, content: randVerseContent, trigger: randVerseTrigger)
// Step 5 - Register the request
randVerseCenter.add(randVerseRequest) { (error) in
if let error = error{
print (error.localizedDescription)
}
//Check the error parameter and handle any errors
}
}

After getting more details, I guess I know why you still don't see the notifications being delivered. I'm making it in another answer to not have it too long, but I'll keep my previous answer for reference.
Maybe you were waiting for the notification with the application in foreground? I'll refer to another part of the documentation:
Scheduling and Handling Local Notifications
On the section about Handling Notifications When Your App Is in the Foreground:
If a notification arrives while your app is in the foreground, you can
silence that notification or tell the system to continue to display
the notification interface. The system silences notifications for
foreground apps by default, delivering the notification’s data
directly to your app...
So, if that's the case, you must implement a delegate for UNUserNotificationCenter.
I suggest you something like this, where on AppDelegate you assign the delegate for UNUserNotificationCenter since documentation says it must be done before application finishes launching:
// AppDelegate.swift
#main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
return true
}
// Rest of your code on AppDelegate...
}
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Here we actually handle the notification
print("Notification received with identifier \(notification.request.identifier)")
// So we call the completionHandler telling that the notification should display a banner and play the notification sound - this will happen while the app is in foreground
completionHandler([.banner, .sound])
}
}
On the view controller you have handling the notification authorization and request registration, you could do it like this:
class NotificationsViewController: UIViewController {
static let notificationAuthorizedNotification = NSNotification.Name(rawValue: "NotificationAuthorizedNotification")
let randVerseCenter = UNUserNotificationCenter.current()
override func viewDidLoad() {
super.viewDidLoad()
// We call this method when we know that the user granted permission, so we know we can then make notification requests
NotificationCenter.default.addObserver(self, selector: #selector(handleNotificationAuthorization), name: NotificationsViewController.notificationAuthorizedNotification, object: nil)
randVerseCenter.getNotificationSettings { [weak self] settings in
// We check current settings and asks for permission if not granted before
if settings.authorizationStatus == .notDetermined {
// Step 1 - Ask the use for permission to notify
self?.randVerseCenter.requestAuthorization(options: [.alert, .sound]){ (granted, error) in
if granted {
NotificationCenter.default.post(name: NotificationsViewController.notificationAuthorizedNotification, object: nil)
print("Yay - request authorisation worked!")
} else {
print ("D'oH - request Authorisation did not work!")
}
}
}
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
// We stop listening to those notifications here
NotificationCenter.default.removeObserver(self)
}
#objc
func handleNotificationAuthorization() {
// Step 2 - Create the Notification Content
let randVerseContent = UNMutableNotificationContent()
randVerseContent.title = "Random Reference"
randVerseContent.body = "Random Verse"
randVerseContent.sound = UNNotificationSound.default
// Step 3 - Create the trigger for the notification by delay
let randVerseDate = Date().addingTimeInterval(30)
let randVerseDateComponents = Calendar.current.dateComponents([.second], from: randVerseDate)
let randVerseTrigger = UNCalendarNotificationTrigger(dateMatching: randVerseDateComponents, repeats: true)
// Step 4 - Creating the request
let randVerseUUIDString = UUID().uuidString
let randVerseRequest = UNNotificationRequest(identifier: randVerseUUIDString, content: randVerseContent, trigger: randVerseTrigger)
// Step 5 - Register the request
randVerseCenter.add(randVerseRequest) { (error) in
if let error = error{
print (error.localizedDescription)
} else {
print("Successfully registered notification with id \(randVerseUUIDString) at every second \(randVerseDateComponents.second!) of a minute")
}
}
}
}
You might still have older notifications scheduled since your code was requesting them at the viewDidLoad and maybe you didn't remove them or delete the app.
You can check the pending notifications using this on your viewDidLoad for example:
randVerseCenter.getPendingNotificationRequests() { requests in
for request in requests {
guard let trigger = request.trigger as? UNCalendarNotificationTrigger else { return }
print("Notification registered with id \(request.identifier) is schedulled for \(trigger.nextTriggerDate()?.description ?? "(not schedulled)")")
}
}
And use randVerseCenter to remove them by their identifiers or remove all of them.

The problem is how the trigger was created. We can look at the documentation for UNCalendarNotificationTrigger to get more understanding:
Create a UNCalendarNotificationTrigger object when you want to
schedule the delivery of a local notification at the specified date
and time. You specify the temporal information using an
NSDateComponents object, which lets you specify only the time values
that matter to you. The system uses the provided information to
determine the next date and time that matches the specified
information.
https://developer.apple.com/documentation/usernotifications/uncalendarnotificationtrigger
So, you use UNCalendarNotificationTrigger when you want to create a trigger to match the date components. The code below will create a trigger which will deliver a notification every day at 8:30 in the morning, because the .hour and the .minute components were specified:
var date = DateComponents()
date.hour = 8
date.minute = 30
// This trigger will match these two components - hour and minute
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)
In your case, you created a trigger using all of the components of a date (year, month, dat, hour, minute, second):
let randVerseDateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute, .second], from: randVerseDate)
And that makes it an impossible condition to repeat the trigger - because there won't be another year 2021 - so it will not be triggered.
You need to think how you want this notification to be triggered. If your intention is to deliver a notification on the same second counting from a specific time, then you must use only the .second date component:
let randVerseDateComponents = Calendar.current.dateComponents([.second], from: randVerseDate)
Let's say randVerseDate is something like 2021-01-06-20:01:35, and we use the line of code above. Then this will trigger the notification every minute when the clock reaches 35 seconds: 20:02:35, then 20:03:35, then 20:04:35, and so on...

Related

Calendar Based Local Notification Not Working - Swift 3

I have setup a local notification system so that I can fire a notification at a certain time every day. This time is determined by the user and I store it as a string. I will break down all the steps I have done in the code to follow but basically, my problem is that the notification won't fire.
Step 1:
In this step I setup an alert to ask permission to send a notification:
let center = UNUserNotificationCenter.current()
let options: UNAuthorizationOptions = [.alert, .badge, .sound]
center.requestAuthorization(options: options) { (granted, error) in
if granted {
application.registerForRemoteNotifications()
}
}
Step 2:
In this step I setup the function I call to send a notification:
static func sendNotification(stringDate: String) {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm"
let content = UNMutableNotificationContent()
content.title = "Title"
content.body = "Detail"
content.badge = 1
if let date = dateFormatter.date(from: stringDate) {
let calendar = Calendar.current
let hour = calendar.component(.hour, from: date)
let minute = calendar.component(.minute, from: date)
var dateComponents = DateComponents()
dateComponents.hour = hour
dateComponents.minute = minute
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: "dateDone", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}
Step 3:
I then call this function in my app delegate file like so:
func applicationDidEnterBackground(_ application: UIApplication) {
let defaults = UserDefaults.standard
if defaults.object(forKey: "currentUser") != nil {
if User.current.desiredTimeForNews.characters.contains("A") {
let stringToBeConverted = User.current.desiredTimeForNews.replacingOccurrences(of: "AM", with: "")
HelperFunctions.sendNotification(stringDate: stringToBeConverted)
} else {
let stringToBeConverted = User.current.desiredTimeForNews.replacingOccurrences(of: "PM", with: "")
HelperFunctions.sendNotification(stringDate: stringToBeConverted)
}
}
}
IMPORTANT: As you can see within my function I take in a "stringDate" parameter. This variable has a string value of my date. I then convert this to a date value. Through using breakpoints and print statements I have seen that all my values including the date, hour, minute, etc are all NOT nil.
Overall, my problem is that the notification is never sent. However I know for a fact that it is called as I have used breakpoints to prove that. Any help would be appreciated!
For me personally, I ran it on a real device and it worked. Although it should work on a simulator it didn't for me. So I would try and run it on a real device before looking at anything else!
The code seems to be ok. Should work on the device and also on the simulator. My guess is that the trigger time just happens at a different time, then what you would expect.
Insert this check below after your setup to see the details:
UNUserNotificationCenter.current()
.getPendingNotificationRequests(completionHandler: { requests in
for (index, request) in requests.enumerated() {
print("notification: \(index) \(request.identifier) \(request.trigger)")
}
})
note 1:Why do you have this line in the code?
application.registerForRemoteNotifications()
note 2:You should check if the requestAuthorization went through ok. With something like this:
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
guard (settings.authorizationStatus == .authorized )
else { print("notifsetup not authorized");return }
guard (settings.soundSetting == .enabled) else {
print("notifsetup sound not enabled");return }
}

Wait till local notifications from UNUserNotificationCenter gets deleted using removePendingNotificationRequests ios 10 swift 3

Using new local notifications from UNUserNotificationCenter.
I try to delete notification with some identifiers:
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
and from documention:
This method executes asynchronously, removing the pending notification requests on a secondary thread.
Completion handler is not present. So how do I know when its really get deleted? Before moving ahead, I need to make sure that this identifier is not present anymore.
I know I can use next code
notificationCenter.getPendingNotificationRequests { (requests) in
for request in requests {
}
}
But if I run this code right after removing - they are still there. But after some time and later in the code they are gone. Especially its important before adding new one when you re about to rich the limit of 64 notifications
You don't have to do all that complex logic as you posted in your answer. You can simply call removeAllPendingNotificationRequests and wait for when it's done within getPendingNotificationRequests method. You can run this code below and see what happens. You will see that after remove will be printed immediately then numbers from 1..63 and then 0.
let notificationCenter = UNUserNotificationCenter.current()
for i in 1 ... 63 {
let components: Set<Calendar.Component> = [.year, .month, .day, .hour, .minute, .second]
let date = Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let dateComponents = Calendar.current.dateComponents(components, from: date)
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let content = UNMutableNotificationContent()
content.title = "Title"
content.body = "Body"
content.sound = UNNotificationSound.default()
let request = UNNotificationRequest(identifier: "id" + String(i),
content: content, trigger: trigger)
notificationCenter.add(request) {
(error) in
print(i)
}
}
notificationCenter.removeAllPendingNotificationRequests()
print("after remove")
notificationCenter.getPendingNotificationRequests() {
requests in
print(requests.count)
}
The reason why it works so is that because all them are tasks put into the queue and run one by one. So, first, it puts 63 notifications, then it removes them, and finally, it counts them. All those tasks go strictly one after each other
Ok looks like after awhile I found one of the ways - its DispatchGroup
Its done also in async way with userInteractive quality:
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
let group = DispatchGroup()
group.enter()
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: arr)
group.leave()
group.notify(queue: DispatchQueue.main) { () in
UNUserNotificationCenter.current().getPendingNotificationRequests { (requests) in
for request in requests {
print("||=> Existing identifier is \(request.identifier)")
}
}
}
}
And that deleted notifications does not exist in getPendingNotificationRequests

Swift: how to schedule local notifications with dynamic content

I have a weather app for iOS, and I'd like to allow the user to receive a notification each morning at a time of their choosing which would fetch the weather forecast for the day and display a notification.
I'd like to avoid using push notifications, and I thought I might be able to use local notifications, except I can't see a way to fetch the content to be shown from a server. It looks like the content has to be set at the time of scheduling. Is that right?
That makes me think I might be able to register my application to use background execution to periodically fetch the weather and schedule a notification with the latest content, but this seems wasteful.
In short, I'd like to tell iOS to run a specific function at a specific time. Is there a good option for this that I'm missing? Are push notifications the only/best way to accomplish this sort of thing?
Push notification is best option for your if you want to display weather forecast .
More about this : https://stackoverflow.com/a/41901767/3901620
You can schedule a local notification for a specific time and when a user sees it and if he wants he can open your app by tapping on that notification. At that time, you will able to know that, a user has tapped on a notification and thus the app is open, you can make a network call to fetch the data and show it inside the application. This will not require any background calls therefor and only make a network call to an action by a user.
Another option: You can create a widget of your app (like Weather Widget). Whenever a user goes into widget area you will get a delegate call and make a network call to get the latest weather data. If a user wants more information on it, he can simply tap on it and your app will open. Then, everything will be in your hands.
Your option: You can always get dynamic content whenever the user opens your app for a particular date and set a notification for it. But this is not suggestible as the user may not get updated data.
Push Notification: This may not be required with your case, however, if you want to send the dynamic data over your server to your app. This is always the best option.
i have created a function. In which this will call your function at a specific time, when you want. Am creating a clock app so i need to trigger a local notification when ever user created the alarm. And in the notification Center Delegate method, you can handle your response and call the whatever method you want.
class LocalNotificationMethod : NSObject {
static let notificationInstance = LocalNotificationMethod()
let requestIdentifier = "SampleRequest" //identifier is to cancel the notification request
internal func scheduleLocalNotification(titleOfNotification:String, subtitleOfNotification:String, messageOfNotification:String, soundOfNotification:String, dateOfNotification:String) {
if #available(iOS 10.0, *) {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd hh:mm a"
let date3 = formatter.date(from: dateOfNotification)
let content = UNMutableNotificationContent()
content.body = NSString.localizedUserNotificationString(forKey: titleOfNotification, arguments: nil)
content.sound = soundOfNotification.characters.count > 0 ? UNNotificationSound.init(named: soundOfNotification + ".mp3") : UNNotificationSound.default()
let trigger = UNCalendarNotificationTrigger.init(dateMatching: NSCalendar.current.dateComponents([.day, .month, .year, .hour, .minute], from: date3!), repeats: false)
let request = UNNotificationRequest(identifier:requestIdentifier, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request){(error) in
if (error != nil){
print(error?.localizedDescription)
} else {
print("Successfully Done")
}
}
} else {
// Fallback on earlier versions
}
}
}
And in AppDelegate Methods : - You can handle whenever user click on your notification or whenever your notification will be present.Is up to you what you want to done.
//MARK:- Notification Delegates
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
print("Tapped in notification")
}
//This is key callback to present notification while the app is in foreground
#available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("Notification being triggered")
//You can either present alert ,sound or increase badge while the app is in foreground too with ios 10
//to distinguish between notifications
if notification.request.identifier == "SampleRequest" {
completionHandler( [.alert,.sound,.badge])
}
}

How to get a push notification at a set time? (Swift 3)

I'm making an app that's supposed the user everyday at a set time about the news. It get's the text of the news through a function which calls it from an array. My question is: how do I get my app to call the function and then send me a push notification with the info text every day at, let's say, 4am?
Thanks to everyone for answering! Have a great day!
Here is some code I used before. Not a hundred-percent what you are looking for, but I hope useful for you.
You need to modify it to be sending daily
import UIKit
import UserNotifications
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
var isGrantedNotificationAccess:Bool = false
#IBAction func send10SecNotification(_ sender: UIButton) {
if isGrantedNotificationAccess {
//add notification code here
//Set the content of the notification
let content = UNMutableNotificationContent()
content.title = "10 Second Notification Demo"
content.subtitle = "From MakeAppPie.com"
content.body = "Notification after 10 seconds - Your pizza is Ready!!"
//Set the trigger of the notification -- here a timer.
let trigger = UNTimeIntervalNotificationTrigger(
timeInterval: 10.0,
repeats: true)
//Set the request for the notification from the above
let request = UNNotificationRequest(
identifier: "10.second.message",
content: content,
trigger: trigger
)
//Add the notification to the currnet notification center
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
}
override func viewDidLoad() {
super.viewDidLoad()
UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge]) { (granted, error) in
self.isGrantedNotificationAccess = granted
}
}
}

swift NSTimer in Background

I have come across a lot of issues with how to handle NSTimer in background here on stack or somewhere else. I've tried one of all the options that actually made sense .. to stop the timer when the application goes to background with
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidEnterBackground", name: UIApplicationDidEnterBackgroundNotification, object: nil)
and
NSNotificationCenter.defaultCenter().addObserver(self, selector: "appDidBecomeActive", name: UIApplicationWillEnterForegroundNotification, object: nil)
At first I thought that my problem is solved, I just saved the time when the app did enter background and calculated the difference when the app entered foreground .. but later I noticed that the time is actually postponed by 3, 4 , 5 seconds .. that it actually is not the same .. I've compared it to the stopwatch on another device.
Is there REALLY any SOLID solution to running an NSTimer in background?
You shouldn't be messing with any adjustments based upon when it enters background or resumes, but rather just save the time that you are counting from or to (depending upon whether you are counting up or down). Then when the app starts up again, you just use that from/to time when reconstructing the timer.
Likewise, make sure your timer handler is not dependent upon the exact timing that the handling selector is called (e.g. do not do anything like seconds++ or anything like that because it may not be called precisely when you hope it will), but always go back to that from/to time.
Here is an example of a count-down timer, which illustrates that we don't "count" anything. Nor do we care about the time elapsed between appDidEnterBackground and appDidBecomeActive. Just save the stop time and then the timer handler just compares the target stopTime and the current time, and shows the elapsed time however you'd like.
For example:
import UIKit
import UserNotifications
private let stopTimeKey = "stopTimeKey"
class ViewController: UIViewController {
#IBOutlet weak var datePicker: UIDatePicker!
#IBOutlet weak var timerLabel: UILabel!
private weak var timer: Timer?
private var stopTime: Date?
let dateComponentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute, .second]
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter
}()
override func viewDidLoad() {
super.viewDidLoad()
registerForLocalNotifications()
stopTime = UserDefaults.standard.object(forKey: stopTimeKey) as? Date
if let time = stopTime {
if time > Date() {
startTimer(time, includeNotification: false)
} else {
notifyTimerCompleted()
}
}
}
#IBAction func didTapStartButton(_ sender: Any) {
let time = datePicker.date
if time > Date() {
startTimer(time)
} else {
timerLabel.text = "timer date must be in future"
}
}
}
// MARK: Timer stuff
private extension ViewController {
func registerForLocalNotifications() {
if #available(iOS 10, *) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
guard granted, error == nil else {
// display error
print(error ?? "Unknown error")
return
}
}
} else {
let types: UIUserNotificationType = [.alert, .sound, .badge]
let settings = UIUserNotificationSettings(types: types, categories: nil)
UIApplication.shared.registerUserNotificationSettings(settings)
}
}
func startTimer(_ stopTime: Date, includeNotification: Bool = true) {
// save `stopTime` in case app is terminated
UserDefaults.standard.set(stopTime, forKey: stopTimeKey)
self.stopTime = stopTime
// start Timer
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
guard includeNotification else { return }
// start local notification (so we're notified if timer expires while app is not running)
if #available(iOS 10, *) {
let content = UNMutableNotificationContent()
content.title = "Timer expired"
content.body = "Whoo, hoo!"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: stopTime.timeIntervalSinceNow, repeats: false)
let notification = UNNotificationRequest(identifier: "timer", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(notification)
} else {
let notification = UILocalNotification()
notification.fireDate = stopTime
notification.alertBody = "Timer finished!"
UIApplication.shared.scheduleLocalNotification(notification)
}
}
func stopTimer() {
timer?.invalidate()
}
// I'm going to use `DateComponentsFormatter` to update the
// label. Update it any way you want, but the key is that
// we're just using the scheduled stop time and the current
// time, but we're not counting anything. If you don't want to
// use `DateComponentsFormatter`, I'd suggest considering
// `Calendar` method `dateComponents(_:from:to:)` to
// get the number of hours, minutes, seconds, etc. between two
// dates.
#objc func handleTimer(_ timer: Timer) {
let now = Date()
if stopTime! > now {
timerLabel.text = dateComponentsFormatter.string(from: now, to: stopTime!)
} else {
stopTimer()
notifyTimerCompleted()
}
}
func notifyTimerCompleted() {
timerLabel.text = "Timer done!"
}
}
By the way, the above also illustrates the use of a local notification (in case the timer expires while the app isn't currently running).
For Swift 2 rendition, see previous revision of this answer.
Unfortunately, there is no reliable way to periodically run some actions while in background. You can make use of background fetches, however the OS doesn't guarantee you that those will be periodically executed.
While in background your application is suspended, and thus no code is executed, excepting the above mentioned background fetches.

Resources