Launching app from Local Notification; VC instantiates, but does not appear - ios

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

Related

Present login screen with notification center when app enters background and foreground, Swift

I have Login screen and I would like to present it or just add it to navigation, it doesn't matter right now, still not sure which way I would like it to be, everytime my app comes back to foreground. Code below partially works, it does start face id, but actually whole login screen isn't shown, I can still see my last screen that was opened before entering background. I would like to hide everything with login screen. When I print childrens in appMovedToBackground function, correct view controller is printed, which means its added, but as I said, its not showing, only icon from face id can be seen. Code below is on one view controller only, currently, I would like this to be applied to all my view controllers, should I move functionality to appdelegate? Thanks in advance.
let notificationCenter = NotificationCenter.default
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.addObserver(self, selector: #selector(appMovedToBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(appCameToForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
#objc func appMovedToBackground() {
print("its in background")
let authVC = AuthViewController()
navigationController?.topViewController?.addChild(authVC)
}
#objc func appCameToForeground() {
print("app enters foreground")
if let authVC = navigationController?.topViewController?.children.first as? AuthViewController {
print("willEnterForegroundNotification")
authVC.checkBiometricsAndStart()
}
}
Here you need to correctly add the vc
#objc func appMovedToBackground() {
print("its in background")
guard let added = navigationController?.topViewController else { return }
let authVC = AuthViewController()
authVC.view.frame = UIScreen.main.bounds
added.view.addSubview(authVC.view)
added.addChild(authVC)
}
Regarding that you need it for all vcs , yes implementing it inside AppDelegate is the easiest way

How to open VC on local notification when app is closed

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

Selecting tableview cell from local notification when app closed

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)

Trouble with local notification in AppDelegate

I have an alarm clock app. It has 2 VC. VC1 is a menu VC that linked with VC2. In VC2 there's setting of alarm clock. So I have troubles with getting local notifications.
For example, if I set Alarm Clock on VC2 then I move to VC1 and then go to Home Screen I will receive a notification on the top of the screen. After clicking on notification I will move to VC1 and I will get a message. But I will get an error 'Could not cast value of type 'MyApp.VC1' (0x10ee97730) to 'MyApp.VC2' (0x10ee96bd0)'. If I set Alarm Clock on VC2 then I move to Home Screen I will receive a notification on the top of the screen. After clicking on notification I will move to VC2 and I will get a message and everything will be fine.
Other problem is setting Alarm clock on VC2 and moving to VC1 without moving to Home Screen. When time will I come my app just crashing with same error 'Could not cast value of type 'MyApp.VC1' (0x10ee97730) to 'MyApp.VC2' (0x10ee96bd0)'
func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
let storageController = UIAlertController(title: "Alarm", message: nil, preferredStyle: .alert)
var soundName: String = ""
var index: Int = -1
if let userInfo = notification.userInfo {
soundName = userInfo["soundName"] as! String
index = userInfo["index"] as! Int
}
playSound(soundName)
let stopOption = UIAlertAction(title: "OK", style: .default) {
(action:UIAlertAction)->Void in self.audioPlayer?.stop()
let mainVC = self.window?.visibleViewController as! MainAlarmViewController
storageController.addAction(stopOption)
(self.window?.visibleViewController as! MainAlarmViewController).present(storageController, animated: true, completion: nil)
}
Does anybody know how to resolve it?
When I getting an error I see highlight of this line:
(self.window?.visibleViewController as! MainAlarmViewController).present(storageController, animated: true, completion: nil)
Thank you so much!
P.S. Maybe is it possible to make a notification on the top of a screen with a link to VC2 when app in foreground or app in VC1?
Also sometimes I'm getting a message 'Warning: Attempt to present on whose view is not in the window hierarchy!'
Replace this line
(self.window?.visibleViewController as! MainAlarmViewController).present(storageController, animated: true, completion: nil)
with following code
if let viewController = self.window?.visibleViewController {
if viewController is MainAlarmViewController {
// view controller is MainAlarmViewController
} else {
// view controller is not MainAlarmViewController
}
viewController.present(storageController, animated: true, completion: nil)
} else {
print("Something wrong. Window can't provide visible view controller")
}

Local notification does not show custom action button

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.

Resources