open specific view controller upon tapping local notification - ios

title says it all. i've went through a number of posts trying to put together a solution but to no luck..
i have a notification whose name i'm not sure of...
let request = UNNotificationRequest(identifier: "timerDone", content: content, trigger: trigger)
q1: is the name timerDone?
in viewDidLoad():
NotificationCenter.default.addObserver(self,
selector: "SomeNotificationAct:",
name: NSNotification.Name(rawValue: "timerDone"),
object: nil)
and then i have this method:
#objc func SomeNotificationAct(notification: NSNotification){
DispatchQueue.main.async() {
self.performSegue(withIdentifier: "NotificationView", sender: self)
}
}
with this in AppDelegate.swift:
private func application(application: UIApplication, didReceiveRemoteNotification userInfo: Any?){
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "SomeNotification"), object:nil)
}
any ideas how to do this? thanks in advance!
UPDATE: #Sh_Khan
first, i am coding in swift, i tried translating your code from obj-c to swift as:
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
[self application:application didReceiveLocalNotification:notification]; //how to translate?
}
what should the last line be translated into?
when you wrote:
should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended
let's say the boolean is notiView and we set it to true when we received the local notification and thus the segue will be to a different view controller. is this what you mean?

I found the documentation for user notification a bit confusing and incomplete. The tutorials are better than most other Apple frameworks. However, the tutorials are mostly incomplete and assume that every app implement the notification center delegate inside the AppDelegate. Not!
For many apps handling the notification delegate in a view controller (instead of in the AppDelegate), the view controller would need to be set as the user notification center delegate inside the AppDelegate didFinishLaunchingWithOptions method. Otherwise, your notification firing would not be visible to the notification delegate when your app is launched from the background mode. Your view controller is loaded after the notification fires. You need a way to launch the delegate methods after your view controller has completed its loading.
For example: suppose you are using a split view controller as your initial view controller for your app and you have implemented the split VC's master view controller as your notification delegate, you would need to let UNUserNotificationCenter know that the master VC is its delegate when your application launches (not inside the master VC's viewDidLoad() as most tutorials suggest). Eg,
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
This would allow iOS to call your notification delegate methods after the master VC is loaded when your app is launched cold or from the background mode.
In addition, if you need your master VC know that it was loaded because a user notification firing (and not from normal loading), you will use the NSUserDefaults to communicated this information. Hence, the AppDelegate would look as follows:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
...
let splitViewController = window!.rootViewController as! UISplitViewController
...
if #available(iOS 10.0, *) {
if let _ = launchOptions?[UIApplicationLaunchOptionsKey.localNotification] {
UserDefaults.standard.set("yes", forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
}
if let masterViewController = splitViewController.viewControllers.first as? MasterViewController {
UNUserNotificationCenter.current().delegate = masterViewController
}
}
}
where kMyAppLaunchedByUserNotification is a key you use to communicate with the master VC. In the viewDidAppear() for Master View Controller, you would check User Defaults to see whether it is being loaded because of notification.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if #available(iOS 10.0, *) {
if let _ = UserDefaults.standard.object(forKey: kMyAppLaunchedByUserNotification) {
UserDefaults.standard.removeObject(forKey: kMyAppLaunchedByUserNotification)
UserDefaults.standard.synchronize()
//Handle any special thing that you need to do when your app is launched from User notification as opposed to regular app launch here
// NOTE: the notification delegate methods will be called after viewDidAppear() regardless of you doing any special thing here because you told iOS already in AppDelegate didFinishLaunchingWithOptions
}
}
}
I hope this helps you.

If the app is closed and you tapped a local notification then check it in didFinishLaunchingWithOptions method
/// Objective-C
if (launchOptions[UIApplicationLaunchOptionsLocalNotificationKey] != nil)
{
UILocalNotification *notification =
launchOptions[UIApplicationLaunchOptionsLocalNotificationKey];
[self application:application didReceiveLocalNotification:notification];
}
/// Swift
if (launchOptions![UIApplicationLaunchOptionsKey.localNotification] != nil)
{
var notification =
launchOptions![UIApplicationLaunchOptionsKey.localNotification]
self.application(application, didReceive: notification)
}
note: The viewDidLoad of the rootViewcontroller isn't yet called so observer won't be triggered , so you should store a boolean variable in user defaults in didReceiveLocalNotification method and check it in viewDidAppear method of the rootViewcontroller to make the segue and then make it false as the notificationCenter will work only when app is in foreground or in background if it's not yet suspended . . .
UPDATE: #Shi_Zhang
Yes,this is what I mean

Related

Updating a Tab Bar badge count from Push Notification when the app is in the foreground

I have a Notifications Tab in my TabBarController that I would like to show the same badge count as the App badge count. I have it working in all scenarios except for when the app is in the foreground.
To do this I've created a custom class for my TabBarController which registers for the UIApplicationWillEnterForeground notification and sets the Notification tab's badge to equal the App badge. I also do this in the OnLoad() function.
Now how do I make the Notification Tab badge update when the app is already in the foreground? I capture the notification in the AppDelegate function:
application(_:didReceiveRemoteNotification:fetchCompletionHandler:)
I know it may be possible to dig around the rootViewController to find the tab bar controller but my root controller is a my entry point to the app which checks if the user has a login token in the keychain and segues to either the login view controller or tab bar controller. So given that it segues I'm not sure this is an option?
Is it possible for my UI to register for a 'push notification' notification like the way I register to be notified of UIApplicationWillEnterForeground? This would be ideal but I haven't managed to find such a notification.
Any advice or tips would be appreciated!
Post a notification from didReceiveRemoteNotification in AppDelegate and add an observer in your custom TabBarController to listen and update the UI as below,
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
NotificationCenter.default.post(Notification(name: Notification.Name("didReceiveNotification")))
}
}
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(
self,
selector: #selector(updateBadgeCount),
name: Notification.Name(rawValue: "didReceiveNotification"),
object: nil
)
}
#objc private func updateBadgeCount() {
// Update badge count
}
deinit {
NotificationCenter.default.removeObserver(self)
}
}

AVCaptureDevice.requestAccess presents unexpected behavior with a UINavigationController

Working with Xcode 10.1 and Swift 4.2
I have a complex app that uses a UINavigationController implemented in the AppDelegate.
The rootViewController of the navigationController is a DashboardController() class (subclass of UIViewController)
The DashboardController implements a left menu drawer using several ViewControllers (with self.addChild(viewController))
Everything works fine, except when I need to push a viewController to present a BarCodeScannerView().
The barebone barCodeScannerView can be pushed and popped as expected.
The problems arises when I request access to the camera (only the first time).
As soon as I present the Device.requestAccess(for:) as follow: the viewController is popped and the previous view (rootViewController) is presented. (Still with the "App would like to access the camera" AlertView)
func requestCameraAccess() {
AVCaptureDevice.requestAccess(for: AVMediaType.video) { granted in
if granted {
self.launchScanner()
} else {
self.goBack()
}
}
}
If I click "OK" The system will register that the access was granted, but the
applicationDidBecomeActive (in the AppDelegate) is called after aprox 1 second. I have some initializers in applicationDidBecomeActive, and they all are executed again. And after a quick delay, everything works fine.
BTW: applicationWillResignActive, applicationDidEnterBackground and applicationWillEnterForeground are NOT called. So it is clear that this is not part of an App LifeCycle.
Any idea what might me going on here? What can make the system call applicationDidBecomeActive within the app? and still keep everything running?
Thx in advance...
UPDATE After reading the comments, I was able to isolate the issue #2 as follows:
A simple/barebones project with a UINavigationController with a dashboardViewController as rootViewController. The dashboardViewController pushes a CameraViewController() in viewDidLoad(). The cameraViewController requests access to the camera. When clicking OK, the call to applicationDidBecomeActive is triggered.
The full project is attached. (except the "Privacy - Camera Usage Description" key in the .plist.
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? = UIWindow()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let dashboardViewController = DashboardViewController()
window?.rootViewController = UINavigationController(rootViewController: dashboardViewController)
window?.makeKeyAndVisible()
return true
}
func applicationDidBecomeActive(_ application: UIApplication) {
print("applicationDidBecomeActive")
}
func applicationWillResignActive(_ application: UIApplication) {}
func applicationDidEnterBackground(_ application: UIApplication) {}
func applicationWillEnterForeground(_ application: UIApplication) {}
func applicationWillTerminate(_ application: UIApplication) {}
}
class DashboardViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
let cameraVC = CameraViewController()
self.navigationController?.pushViewController(cameraVC, animated: true)
}
}
import AVFoundation
class CameraViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
AVCaptureDevice.requestAccess(for: AVMediaType.video) { granted in
if granted {
print("Access granted")
}
}
}
}
I'd say the problem is just with your testing procedure. When I run your code with a print statement in applicationWillResignActive, this is what I see:
applicationDidBecomeActive
applicationWillResignActive
Access granted
applicationDidBecomeActive
That seems completely in order and normal. It would have been weird to get a spurious didBecomeActive, but that is not what's happening; we resign active and then become active again, which is fine. You should expect that at any time your app can resign active and become active again. Many things in the normal lifecycle can cause that, and the presentation of an out-of-process dialog like the authorization dialog can reasonably be one of them. You should write your code in such a way as to cope with that possibility.

How to show a specific view controller even if user does not tap on the notification

I have 2 ViewControllers which will pop up depending on the local notification. How can I show these ViewControllers when user taps on the app icon directly instead of the notification ?
Is there any way to call
- (void)application:(UIApplication *)application
didReceiveLocalNotification:(UILocalNotification *)notification
from
- (void)applicationDidBecomeActive:(UIApplication *)application
?
You can just call that method directly by referencing your app delegate and giving it those parameters (you will have to create a dummy UILocalNotification).
However, this is weird.
What you should do is properly SEPARATE the code that shows the view controllers into its own function. Then you can call this function in either of the methods you specified above.
Wherever you schedule the local notification, add a key to NSUserDefaults specifying which VC to load.
UserDefaults.standard.setValue("name_of_vc_to_load", forKey: "vcToLoad")
UserDefaults.standard.synchronize()
Finally in
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
and in
func applicationDidBecomeActive(_ application: UIApplication) {
Check if the VC to load available and load the VC accordingly.
if let vcName : String = UserDefaults.standard.value(forKey: "vcToLoad") as? String {
switch vcName {
case "VCA" :
//load VCA
break
default:
//load VCB
break
}
}
You can either present a VC of your choice over the rootVC or you can replace the rootVC itself.

How to go directly to a specific View Controller if Notification has been opened

I have an UITabBarController which presents in one tab a table of chats. If the user clicks on one row, a view controller will be displayed that presents the latest messages.
Now I am introducing Push notifications. The desired behavior would be to automatically go to the chat room if the user opens up the Push notification. Although there's plenty information available on how to handle these use cases, I do not succeed in implementing it.
Here's my code so far:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
if (!isUserLoggedIn()) {
// show login vc
} else {
let protectedController = mainStoryboard.instantiateViewController(withIdentifier: "TabBarController") as! UITabBarController
if let notificationPayload = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary {
if let chatId = notificationPayload["chatId"] as? String {
print(chatId)
// show chat VC
}
}
window!.rootViewController = protectedController
window!.makeKeyAndVisible()
}
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
// Enable or disable features based on authorization.
}
application.registerForRemoteNotifications()
return true
}
The print statement will never be called but that may be due to the fact that the application is completely closed before opening the notification.
didFinishLaunchingWithOptions is not necessarily called, when you click on a notification, only if your app is not in the memory anymore
In your willFinishLaunchingWithOptions function of your appDelegate, set up a delegate for the currentNotificationCenter
func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
UNUserNotificationCenter.current().delegate = self
}
Make sure your AppDelegate implements UNUserNotificationCenterDelegate relevant for you could be this
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
if let chatID = userInfo["chatID"] as? String {
// here you can instantiate / select the viewController and present it
}
completionHandler()
}
Update to answer your follow-up question: How to add a UIViewController to a UINavigationController within your UITabbarController
In order to instantiate your ViewController and add it to your UITabbarController:
let myViewController = MyViewController()
guard let tabbarController = self.window.rootViewController as? UITabbarController else {
// your rootViewController is no UITabbarController
return
}
guard let selectedNavigationController = tabbarController.selectedViewController as? UINavigationController else {
// the selected viewController in your tabbarController is no navigationController!
return
}
selectedNavigationController.pushViewController(myViewController, animated: true)
When the user taps on a notification, callback method of the app delegate is:
application:didReceiveRemoteNotification:fetchCompletionHandler:
More information about notifications from Apple.
You should put your code inside this method.
Also you can create Router class (subclass of NSObject), which will show chat view controller and will be responsible for navigation between view controllers in application.
It's a good manner incapsulate this logic into separate class, not keeping it in the AppDelegate class.

Checking if iOS app was launched from local notification?

How do you detect if an app that is NOT in an active, inactive, or background state (terminated) is launched from a local notification? So far, I've tried two methods in the App Delegate's didFinishLaunchingWithOptions:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// METHOD 1:
if let options = launchOptions {
if let key = options[UIApplicationLaunchOptionsLocalNotificationKey] {
notificationCenter.postNotification(NSNotification(name: "applicationLaunchedFromNotification", object: nil))
}
}
// METHOD 2:
let notification = launchOptions?[UIApplicationLaunchOptionsLocalNotificationKey] as! UILocalNotification!
if (notification != nil) {
notificationCenter.postNotification(NSNotification(name: "applicationLaunchedFromNotification", object: nil))
}
return true
}
In my View Controller, I observe for the notification in ViewDidLoad and in response, set a UILabel's text:
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.addObserver(self, selector: "handleAppLaunchFromNotification", name: "applicationLaunchedFromNotification", object: nil)
}
func handleAppLaunchFromNotification() {
debugLabel.text = "app launched from notification"
}
But the UILabel's text is never set after launching the terminated app from a local notification.
My questions are:
What am I doing wrong?
Is there an easier way to debug a situation like this other than setting a UILabel? Once the app is terminated, Xcode's debugger detaches the app and I can't use print().
You are checking local notification in didFinishLaunchingWithOptions this methods contains launchOptions for only remote notifications. If app is in terminated state and you perform action on local notification then didReceiveLocalNotification gets call after didFinishLaunchingWithOptions method.

Resources