Swift: how to schedule local notifications with dynamic content - ios

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])
}
}

Related

UNCalendarNotificationTrigger not initiating

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...

Not able to perform action on push notification custom buttons

I am working on an iOS application which involves device to device push notification. In Foreground and Background state, I am able to receive notification and able to perform respective actions in respective custom buttons (ACCEPT & REJECT). Everything works fine in the two mentioned states. But in killed/terminated state, although I am able to receive notification, but I am not able to perform action on clicking custom buttons (ACCEPT & REJECT). Can you guys help me this?
//Notification action button function
func setActionCategories(){
let acceptAction = UNNotificationAction(
identifier: NAString().notificationAcceptIdentifier(),
title: NAString().accept().capitalized,
options: [.init(rawValue: 0)])
let rejectAction = UNNotificationAction(
identifier: NAString().notificationRejectIdentifier(),
title: NAString().reject().capitalized,
options: [.init(rawValue: 0)])
let actionCategory = UNNotificationCategory(
identifier: NAString().notificationActionCategory(),
actions: [acceptAction,rejectAction],
intentIdentifiers: [],
options: [.customDismissAction])
UNUserNotificationCenter.current().setNotificationCategories(
[actionCategory])
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
//Here we are performing Action on Notification Buttons & We created this buttons in "setActionCategories" function.
if response.notification.request.content.categoryIdentifier == NAString().notificationActionCategory() {
//Created Firebase reference to get currently invited visitor by E-Intercom
var gateNotificationRef : DatabaseReference?
gateNotificationRef = GlobalUserData.shared.getUserDataReference().child(Constants.FIREBASE_CHILD_GATE_NOTIFICATION).child(userUID).child(guestType!).child(guestUID!)
//Performing accept & reject on click of recently invited visitor by E-Intercom from Notification view.
switch response.actionIdentifier {
//If Accept button will pressed
case NAString().notificationAcceptIdentifier():
gateNotificationRef?.child(NAString().status()).setValue(NAString().accepted())
}
break
//If Reject button will pressed
case NAString().notificationRejectIdentifier(): gateNotificationRef?.child(NAString().status()).setValue(NAString().rejected())
break
default:
break
}
}
UIApplication.shared.applicationIconBadgeNumber = 0
completionHandler()
}
Hi Ashish can you provide some code for us to better assist you. There should be a completion handler in there where you can add an action function. Then you can perform whatever you need the buttons to do.
Add the below condition in didFinishLaunchingWithOptions delegate
if launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] != nil {
// Do what you want to do when remote notification is tapped.
}
you can call didReceive delegate method in it.

iOS 10 User Notifications does not allow repeating of notifications at intervals

http://www.openradar.me/26855019
With the new iOS 10 User Notifications framework, one can no longer schedule notifications at a specific date or time that repeats every minute or every hour.
How can I do it in User Notifications framework?
I found this:
https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/SchedulingandHandlingLocalNotifications.html#//apple_ref/doc/uid/TP40008194-CH5-SW1
You can try to define categories for UNNotificationResponse, then handle response depends on category type.
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
if response.notification.request.content.categoryIdentifier == "TIMER_EXPIRED" {
// Handle the actions for the expired timer.
if response.actionIdentifier == "SNOOZE_ACTION" {
// Invalidate the old timer and create a new one. . .
}
else if response.actionIdentifier == "STOP_ACTION" {
// Invalidate the timer. . .
}
}
// Else handle actions for other notification types. . .
}
You can trigger a notification based on time, calendar or location. The trigger can be repeating:
let date = Date(timeIntervalSinceNow: 60)
let triggerMinute = Calendar.current.dateComponents([.minute,.second], from: date)
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerMinute, repeats: true)
This code attempts to schedule notification such that the minute & second components of the date matches, you could configure it in the way you want.

Keep a Firebase listener in memory when the app is in background

I have been trying to use a Firebase listener to trigger local notifications. I have found a post that addresses exactly what I am trying to do with much of it explained, however I do not have the reputation to comment on the post and there seems to be no indication of how to accomplish what I want anywhere else.
The original poster says this.
I figured it out! I had to use a different approach but i was able to
get my Firebase Database observer to trigger notifications in the
background.
As long as the object containting the database observer is not
deallocated from memory it will continue to observe and trigger. So I
created a global class which contains a static database object
property like this:
class GlobalDatabaseDelegate {
static let dataBase = DataBase()
}
This is where I am confused as to what to do for my own project. It is my understanding that I have to create a class similar to DataBase() which contains my database reference. The problem is I do not understand how to create class object that will contain the database listener.
say for example my reference is :
let userRef = FIRDatabase.database.reference().child("users")
And I want to observe any users added to the database and then trigger a local notification. I am able to write the code to do so, just not sure how to contain it in an object class of its own and then make it static.
Forgive me for being a little slow. Any help would be very much appreciated.
The rest of the post follows :
I also extended the DataBase class to be the
UNUserNotificationCenterDelegate so it can send the push notitications
like this:
extension DataBase: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
print("Tapped in notification")
}
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
print("Notification being triggered")
completionHandler( [.alert,.sound,.badge])
}
func observeNotificationsChildAddedBackground() {
self.notificationsBackgroundHandler = FIREBASE_REF!.child("notifications/\(Defaults.userUID!)")
self.notificationsBackgroundHandler!.queryOrdered(byChild: "date").queryLimited(toLast: 99).observe(.childAdded, with: { snapshot in
let newNotificationJSON = snapshot.value as? [String : Any]
if let newNotificationJSON = newNotificationJSON {
let status = newNotificationJSON["status"]
if let status = status as? Int {
if status == 1 {
self.sendPushNotification()
}
}
}
})
}
func sendPushNotification() {
let content = UNMutableNotificationContent()
content.title = "Here is a new notification"
content.subtitle = "new notification push"
content.body = "Someone did something which triggered a notification"
content.sound = UNNotificationSound.default()
let request = UNNotificationRequest(identifier: "\(self.notificationBackgroundProcessName)", content: content, trigger: nil)
NotificationCenter.default.post(name: notificationBackgroundProcessName, object: nil)
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().add(request){ error in
if error != nil {
print("error sending a push notification :\(error?.localizedDescription)")
}
}
}
}
In essence I am trying to keep a firebase listener in memory when the app is in background.
So the original post that I have linked in has the answer but it is a matter of understanding it. I have also implemented my code in a slightly different approach.
I found another post detailing the technique needed to run a custom data service class. Custom Firebase Data Service Class : Swift 3
To set keep the firebase listener in memory there are few steps.
1.Create a firebase data service class. In that class I have a static variable that is of the same class
class FirebaseAPI {
var isOpen = false
static let sharedInstance = FirebaseAPI()
// I added functions for firebase reference in this class
func observeNotifications(){
//firebase call here
}
}
2.Set up notification settings in app delegate. This is where my set up differs from the original post.
let notificationSettings = UIUserNotificationSettings(types: [.badge, .alert, .sound], categories: nil)
UIApplication.shared.registerUserNotificationSettings(notificationSettings)
3.Create a reference to the firebase class in a viewcontroller of your choice, it works in app delegate but not advisable.
let sharedInstance = FirebaseAPI.sharedInstance
4.Call functions to setup observer
self.sharedInstance.observeNotifications()
You can then trigger fire a local notification using a completion handler with the function or fire off notifications within the firebase function.
Update: Apple have implemented updates in regards to background modes which have stopped this method from working . Currently the only method is to use APNS

How do I use CKSubscription in an iOS 10 messages extension?

iOS 10 introduced messages extensions, which are the first (to my knowledge) extension which does not require a host application. I am trying to use CloudKit in a messages extension which does not have a host app.
From what I can tell, CKSubscription relies on push notifications. However, I cannot register for push notifications in the usual way (via UIApplication) in app extensions:
let app = UIApplication.shared // Error: not available here blah blah blah
This means it is seemingly impossible to receive CKSubscription notifications in a messages app. I did find hope in the new UserNotifications.framework, but it does not provide any mechanisms for registering for remote notifications. I tried:
override func willBecomeActive(with conversation: MSConversation) {
// ...
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.badge, .alert]) { success, error in
if error != nil { fatalError() }
}
center.delegate = self
// ...
}
// MARK: UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: (UNNotificationPresentationOptions) -> Void) {
fatalError() // This never gets called :(
}
But when I update records that are the subject of my CKSubscription, no notification is presented to the user and the delegate is not notified.
Here's my CKSubscription code:
let sub = CKRecordZoneSubscription(zoneID: legitimateZone)
let notifInfo = CKNotificationInfo()
notifInfo.alertBody = "Wow it works! Amazing!"
sub.notificationInfo = notifInfo
privateDB.save(sub) { sub, error in
if let e = error {
fatalError("Error saving zone sub: \(e)")
}
print("Saved subscription")
}
How do I get CKSubscription notifications in a messages extension?
I don't even need the notification presented to the user, nor do I need to receive them in the background. All I want is to know when records are updated while my extension is running.
If there is another way to do this other than CKSubscription I'd love to hear as well (as long as it's not going to poll CloudKit constantly, wasting my precious 40 requests/sec).
I have tried on both a physical device and in the simulator.

Resources