Controlling which view controller loads after receiving a push notification in SWIFT

31,145

Solution 1

Updated for Swift 4.2

Like it was said, you want to register to remote notifications in applicationDidLaunchWithOptions :

 func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
     let pushSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    UIApplication.shared.registerUserNotificationSettings(pushSettings)
     UIApplication.shared.registerForRemoteNotifications()
}

There is no way to know in which viewController you will be when you come back from the lockScreen/Background. What I do is I send a notification from the appDelegate. When you receive a remoteNotification, didReceiveRemoteNotification in the appDelegate is called.

 func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
    let notif = JSON(userInfo) // SwiftyJSON required 

Depending on your notification payload, you should first make sure it is not nil and then call a NSNotification that will be catched by the viewControllers that should catch this notification. This is also where you could post different kinds of notification based on payload you received. Could look like this, just take it as an example :

if notif["callback"]["type"] != nil {
  NotificationCenter.default.post(name: Notification.Name(rawValue: "myNotif"), object: nil)
  // This is where you read your JSON to know what kind of notification you received  

}

If you receive a message notification and you have not logged in anymore because the token has expired, then the notification will never be catched in the view controller because it will never be watched.

Now for the part where you catch the notification in the view controller. In the viewWillAppear:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.addObserver(self, selector: #selector(self.catchIt), name: NSNotification.Name(rawValue: "myNotif"), object: nil)
}

Now that you added this observer, each time a notification is called in this controller, the function catchIt will also be called. You will have to implement it in every view controller you want to implement a specific action.

func catchIt(_ userInfo: Notification){

    let prefs: UserDefaults = UserDefaults.standard
    prefs.removeObject(forKey: "startUpNotif")

    if userInfo.userInfo?["userInfo"] != nil{
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc: RedirectAppInactiveVC = storyboard.instantiateViewController(withIdentifier: "RedirectAppInactiveVC") as! RedirectAppInactiveVC
        self.navigationController?.pushViewController(vc, animated: true)
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc: RedirectAppActiveVC = storyboard.instantiateViewController(withIdentifier: "RedirectAppActiveVC") as! RedirectAppActiveVC
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

Don't forget to unsubscribe from the notifications when leaving the view controller, else the viewController, if still in the stack, will catch the notification and execute it (well you might want to that, but it's safer to know what you are going into). So I suggest unsubscribing in the viewWillDisappear:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillAppear(animated)
    NotificationCenter.default.removeObserver(self)
}

Doing it this way, you will load the viewController you want. Now we haven't treated all the cases yet. What if you haven't opened your application yet. Obviously, no UIViewController has been loaded, and none of them will be able to catch the notification. You want to know if you received a notification in didFinishLaunchingWithOptions: in the appDelegate. What I do is:

let prefs: UserDefaults = UserDefaults.standard
if let remoteNotification = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? NSDictionary {
    prefs.set(remoteNotification as! [AnyHashable: Any], forKey: "startUpNotif")
    prefs.synchronize()
}

Now, you have set a preference saying the application was started using a remote notification. In the controllers that should be loaded first in your application, I suggest doing the following in the viewDidAppear:

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    let prefs: UserDefaults = UserDefaults.standard
    if prefs.value(forKey: "startUpNotif") != nil {
        let userInfo: [AnyHashable: Any] = ["inactive": "inactive"]
        NotificationCenter.default.post(name: Notification.Name(rawValue: "myNotif"), object: nil, userInfo: userInfo as [AnyHashable: Any])
    }
}

Hope it helps. I also made a GitHub repository to illustrate with local notifications : Local Notifications Observer Pattern (similar to Remote notifications). A similar logic can be implemented using the root view Controller Local Notifications Root Pattern, I personally think it will depend on what you want to implement.

These examples are here to illustrate how it can be simply implemented. With bigger projects, you will end up with more complex architectures such as coordinators that that internally utilize similar mechanisms.

Solution 2

In addition to @NickCatib's answer, to find out if you received a notification while your app is running and if so, in the foreground or background you need to use this method in your AppDelegate:

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {


// You can determine your application state by
if UIApplication.sharedApplication().applicationState == UIApplicationState.Active {

// Do something you want when the app is active

} else {

// Do something else when your app is in the background


}
}

Solution 3

When you run you application, you are calling applicationDidLaunchWithOptions:

UIApplication.sharedApplication().registerUserNotificationSettings ( UIUserNotificationSettings(forTypes: (UIUserNotificationType.Alert | UIUserNotificationType.Badge | UIUserNotificationType.Sound), categories: nil))



if( launchOptions != nil){
    var notificationDict: AnyObject? = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey]
    if(notificationDict != nil){
        handleNotification(notificationDict! as! [NSObject : AnyObject])
    }

}

Here, you have handleNotification which is basicly my custom function where I extract data from the notification and use that information to show corresponding controller.

Here is an example of that:

let notificationType = userInfo["aps"]!["alert"]!!["some-key-I-Need"]! as! String
var storyboard = UIStoryboard(name: "Main", bundle: nil)
let mainViewController = storyboard.instantiateInitialViewController() as! MyViewController
self.window?.rootViewController  = mainViewController

Solution 4

I found all the answers above to be very helpful. Yet, the most voted did not worked for me when the app is deactivated. Later a tried to implement the answers from @NickCatib and @thefredelement combined and they generated an error when executing storyboard.instantiateInitialViewController() - "Could not cast value of type 'UINavigationController'". I found out that this happened because I have a storyboard file with NavController as rootViewController. To solve this issue I created a new Navigation Controller, and that worked with a problem: i lost the correct navigation for the app, and the view didn't even showed the back button. The answer to my problems was to use @NickCatib and @thefredelement answers, but to instantiate the view using an identifier and to push it, using the rootViewController as an UINavigationController, as shown below.

let rootViewController = self.window?.rootViewController as! UINavigationController
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let mvc = storyboard.instantiateViewControllerWithIdentifier("MyViewController") as! 
             MyViewController
rootViewController.pushViewController(mvc, animated: true)

This worked fine for me, and I didn't lose the correct navigation properties for the app.

Share:
31,145
Henry Brown
Author by

Henry Brown

Updated on April 22, 2020

Comments

  • Henry Brown
    Henry Brown about 4 years

    Once I receive a push notification and swipe to open it, it just opens my app and not the VC I want.

    So my question is how do I load the VC I want? I know if the app is open I would move the VC over to another inside the didReceiveRemoteNotification but how do I do it if the app isn't open? or if it is in background mode?

    Also I have TWO different push notifications, so therefore I need it to move ONE of TWO different VCs. How can I tell the difference between different push notifactions?

    Thanks.

  • Henry Brown
    Henry Brown almost 9 years
    thanks! although I still feel a bit lost how am I extracting data??
  • Miknash
    Miknash almost 9 years
    Well, you should consider the notification as it is json. as you can see in the second chunk of the code, in the first line I extract "some-key-I-need". If you are not sure what is in your notification, and correspondingly access your data. You have everythgin about format here: developer.apple.com/library/ios/documentation/…
  • Miknash
    Miknash almost 9 years
    Good point, with this if-else and my answer all possible actions are covered :) here is a +1 :)
  • Henry Brown
    Henry Brown almost 9 years
    sorry I'm still lost by looking at that? the first chunk of code is meant to do what? and the second? Is there a way to consider which type of push notification I am receiving and then from there move to the correct view controller....
  • Miknash
    Miknash almost 9 years
    First chunk of code is the part that is checking if the app is launched with push notification - that's the options parameter. Inside we check if that options are actually push notifications or something else. If we are sure that we have PN then I call handleNotification method. In the second part, I am just showing to you how to extract one key from PN and how to show particular view controller ( you can put whichever VC you want)
  • Henry Brown
    Henry Brown almost 9 years
    So what goes inside the handleNotification function?
  • Miknash
    Miknash almost 9 years
    Well, I have put this second chunck of code. You can use notificationType to determine what to show, or what to pass to MyViewController.
  • Miknash
    Miknash almost 9 years
    Great job with this really good post, but one thing I would avoid for this kind is using NSUserDefaults(). you could just set rootViewController in app delegate and set values there. Oh, and please fix prefs.removeObjectForKey("startUpNotification") to prefs.removeObjectForKey("startUpNotif") since that's the key you use
  • Swift Rabbit
    Swift Rabbit almost 9 years
    I think you are right depending on the context. If you are setting the root view controller, you will never be able to get to a specific view that should be previous to that view. For example if a conversation notification makes the conversation as root and then you want to pop the navigation controller to go back, you will be stuck. I'm not sure of what I just said, if you will be able to pop if you changed the root directly that way?
  • Henry Brown
    Henry Brown almost 9 years
    Thanks although just a few questions, still I am not sure how you exactly differentiate between different push notifications? As one notification I will want to move to one VC and one notification to another? Also as @NickCatib has said surely just rootViewController would be much simpler then NSUserDefaults?
  • Miknash
    Miknash almost 9 years
    Not really, if you need to show specific controller, you can push onto the stack as many controllers as you wish. You can even set the navigation stack with the view controllers you actually need. @Henry Brown: You extract data from that JSON, and depending on the content do different things...
  • Henry Brown
    Henry Brown almost 9 years
    Right got ya, this along with a bit more research and I think I am almost there, thanks for your help!
  • ken
    ken over 6 years
    The view controller mention is it any view controller also can or need the main entrance view controller only?
  • Harshal Pathak
    Harshal Pathak about 6 years
    this is working only when application is running in background, if application is not in background it will take only up to homeviewcontroller
  • Dilip Tiwari
    Dilip Tiwari over 5 years
    if i m on same viewcontroller where i wanted and i received notification then how to not receive notification at that time and receive when their is different page
  • Silambarasan
    Silambarasan about 5 years
    Without rootviewController how to push viewController from AppDelegate?