I am implementing local notifications in my iOS10 app. It is a simple master-detail app where users enter items (medicinal drugs) with expiry dates. Notifications are triggered when an item expires.
The app structure is a DrugTableViewController and a DetailViewController (which contains an array drugs of Drug objects) embedded in a navigation controller.
I am trying to get the appropriate row in the tableview selected when the user taps on a notification.
The code below is successful when the app is open or in background, but does not select the row when the app has been closed when the notification is received (though it still loads the tableview correctly).
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
// Pull out the buried userInfo dictionary
let userInfo = response.notification.request.content.userInfo
if let drugID = userInfo["drug"] as? String {
let navigationController = self.window?.rootViewController as! UINavigationController
let destinationViewController = navigationController.viewControllers[0] as! DrugTableViewController
navigationController.popToViewController(destinationViewController, animated: true)
var selectRow: Int? = nil
for drug in destinationViewController.drugs {
if drug.uniqueID == drugID {
selectRow = destinationViewController.drugs.index(of: drug)!
let path = IndexPath(row: selectRow!, section: 0)
destinationViewController.tableView.reloadData()
destinationViewController.tableView.selectRow(at: path, animated: true, scrollPosition: .top)
selectRow = nil
break
}
}
}
// Call completion handler
completionHandler()
}
I have also tried instantiating my detail view controller from the storyboard ID but this does not work.
Any help would be greatly appreciated!
This code may be invoked until table view has beed loaded. Firstly try to delay notification handling for a second:
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
//Your code here
})
and look, will it help or not. If will, think how you can check is tableview has been loaded or not (or just left the delay)
Related
Hy I am new to iOS but somehow I manged to complete some tasks. I am working on app that sets reminders for user. So for this I am using Local notifications (UNUserNotificationCenterDelegate).
Everything is working good and fine. I have written some code, I am receiving notification at scheduled time and I have handled following cases.
When app is in background
When app is in forground.
My app handles these both cases perfectly or you can say as I needed. but I am helpless in following case
when the App is removed from recent, or not even running in
background at all,and a that time if the scheduled notification pops up, and user taps the notification, It opens the splash view controller then opens my app main view controller, where as I need to go to same view controller from where user set the scheduled time for reminder.
I think I am quite clear in what I want and what is happening. Is there any changes to do that. I know it can be possible as Whats App and other apps are also doing this. Please help me in doing this. ...
Note:
I am using UserNotifications (Local notification) and Deployment target is 10.3
Update:
I saw this link has same need as mine but I do not know what the selected answer suggest as I am new to iOS so I am not sure what and how to do:
So, your problem is when the app is killed or inactive and then when user tap the notification the reminder screen will show up, right?
Here's the case:
Notification shows (inactive/killed) -> tap notification -> splash -> reminder screen.
You should save your data that you want to show in notification. iOS will save any notification data in remoteNotification.
So, when user opens the app from inactive, the first thing that will be called is launchOption in AppDelegate.
Here's the example:
if launchOptions != nil {
// get data from notificaioton when app is killed or incative
if let userInfo = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary {
// Do what you want, you can set set the trigger to move it the screen as you want, for your case is to reminder screen
}
}
When your app launches via LocalNotification your UNUserNotificationCenterDelegate method userNotificationCenter didReceive response will be called.
So I would recommend you to present your notification on top of current view controller as like below approach.
//Add this extension in any of your class
extension UIApplication {
#objc class func topViewController(_ base: UIViewController?) -> UIViewController? {
var baseController = base
if baseController == nil{
baseController = UIApplication.shared.keyWindow?.rootViewController
}
if let nav = baseController as? UINavigationController {
return topViewController(nav.visibleViewController)
}
if let tab = baseController as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(selected)
}
}
return baseController
}
}
//In your `userNotificationCenter didReceive response` method
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
if response.actionIdentifier == "YourIdentifier"{
let controllerToPresent = MyNotificationViewController(nibName: "MyNotificationViewController", bundle: nil)
controllerToPresent.notificationInfo = response.notification.request.content.userInfo
//If navigation flow needed, add controllerToPresent to nav controller
let navConroller = UINavigationController(rootViewController: controllerToPresent)
UIApplication.topViewController(nil)?.present(navConroller, animated: true, completion: nil)
//If navigation flow not needed, present directly
UIApplication.topViewController(nil)?.present(controllerToPresent, animated: true, completion: nil)
}
completionHandler()
}
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.
I would like to launch my app from a local notification that will appear when the home screen is locked or when the user is in another app based on similar discussions here and here I have the following code in my AppDelegate:
func userNotificationCenter(_: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case "play":
var setAlarmVC = self.window?.rootViewController as? SettingAlarmViewController
if setAlarmVC == nil {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
setAlarmVC = storyboard.instantiateViewController(withIdentifier: "AlarmViewController") as? SettingAlarmViewController
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.rootViewController = setAlarmVC
self.window?.makeKeyAndVisible()
}
case "snooze":
print("I pressed pause")
default:
break
}
completionHandler()
}
Within my SettingAlarmViewController's viewDidLoad, I have set up some simple print-outs
override func viewDidLoad() {
super.viewDidLoad()
print("Setting Alarm View Controller was instantiated")
}
When I press play from the local notification while the app is in the background, I get the console print-out as expected:
Setting Alarm View Controller was instantiated
But the app does not actually launch and Setting Alarm View Controller does not appear. If I then click on the app, a fresh Setting Alarm View Controller is the first visible thing. I feel like I must be missing something obvious, but I cannot figure out what it is.
EDIT: Doing more testing. When the notification appears on the lock screen and the user presses "play" from the lock screen, the password / unlock screen does not appear, but the app still launches and I get the print-out " Setting Alarm View Controller was instantiated"
Well, a day of my life wasted on this, here is the problem so that others do not have the same issue: developer.apple.com/documentation/usernotifications/… you must ad [.foreground] to your Notification Action, as in let playAction = UNNotificationAction(identifier: "play", title: "play", options: [.foreground])
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
To test local notifications, I wrote a test app with a single view controller.
In viewDidLoad, I set up the custom action, the notification category, and the userNotificationCenter delegate.
In viewDidAppear, I set the notification content, setup a trigger that fires after 5 sec, create the notification request, and add it to the notification center.
I expect the following:
Foreground mode:
When the app is launched, it should present after 5 sec the notification in foreground. Before, the delegate function „willPresent notification“ should be called.
Background mode:
If, however, the app is put into background by pressing the home button before the trigger fires, the notification should be presented in the home screen, and the delegate function „willPresent notification“ is not called.
After the notification has been presented, the user can tap the action button.
This should bring the app into foreground, and trigger the „didReceive response“ delegate function.
What happens is:
The action button in never shown, only title and body.
When I tap the body, the delegate function „didReceive response“ is triggered using the default action identifier.
The problem:
Why is the custom action button not shown?
Here is my code:
import UIKit
import UserNotifications
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
let userNotificationCenter = UNUserNotificationCenter.current()
let categotyId = "categoryID"
let actionID = "actionID"
override func viewDidLoad() {
super.viewDidLoad()
userNotificationCenter.requestAuthorization(options: [.alert]) { (granted, error) in
if granted {
let okAction = UNNotificationAction(identifier: self.actionID,
title: "OK",
options: [])
let category = UNNotificationCategory(identifier: self.categotyId,
actions: [okAction],
intentIdentifiers: [],
options: [.customDismissAction])
self.userNotificationCenter.setNotificationCategories([category])
self.userNotificationCenter.delegate = self
} else {
print("local notifications not granted")
}
}
userNotificationCenter.removeAllPendingNotificationRequests()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let content = UNMutableNotificationContent()
content.title = NSString.localizedUserNotificationString(forKey: "Title", arguments: nil)
content.body = NSString.localizedUserNotificationString(forKey: "Body", arguments: nil)
content.categoryIdentifier = categotyId
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: (5), repeats: false)
let request = UNNotificationRequest.init(identifier: "requestID",
content: content,
trigger: trigger)
userNotificationCenter.add(request, withCompletionHandler: { (error) in
if let error = error {
print("Could not add notification request. Error: \(error)")
}
})
}
// MARK: - Notification Delegate
// Will be called while app is in the foreground
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: #escaping (UNNotificationPresentationOptions) -> Void) {
// Show alert to the user
print("App in foreground. Show alert.")
completionHandler([.alert])
}
// Should be called after the user tapped the action button
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: #escaping () -> Void) {
let request = response.notification.request
let requestID = request.identifier
switch response.actionIdentifier {
case actionID:
print("Custom OK action triggered in background")
case UNNotificationDefaultActionIdentifier:
print("Default action triggered in background")
default:
print("Unknown action triggered in background, action identifier: \(response.actionIdentifier)")
}
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [requestID])
completionHandler()
}
}
Sorry for my question, but maybe somebody else has the same problem:
I simply did not know that first, only title/body is displayed:
However, I was not aware of the thin grey bar below the body. If this bar is pulled down, the custom action button appears:
Update: As of iOS 10 beta 2, rich notifications are also available on pre-3D touch devices. Pull down on the regular notification to see it.
Make sure you are testing on a iPhone6s/iPhone6s plus simulator/device, it doesn't seem to work on pre-3D touch devices.
On a iPhone6 simulator, try to click and drag down on the stock notification you get and you should see your custom UI appear.