Best Approach to Present Modals After App Reaches Foreground - ios

Users within my iOS application have two states, logged in and logged out. I am using firebase sdk for auth and realtime database data delivery.
For logged in users, I want to present custom in-app alert modals if a certain condition is true for that alert. These conditions are date-drive, i.e. users will get a specific alert on certain dates, but not others. This means the conditions are not decided from user input.
When the server returns that the conditions are met for a certain alert, there are different states. These are what I can think of:
Users can open the app from a killed state where the auth is
reestablished during the app's initialization, i.e. logged
out->logged in.
Users can open the app from a background state.
Users can be in-app while the date changes.
My question is what is best practice here? The three approaches (maybe neither are best) I am considering are these:
Singleton - Have an AlertManager singleton that listens for the alert conditions met events and then the singleton navigates the view controller tree to find the current foreground view controller to present the alert
Protocol - Have a protocol that is adopted by all view controllers. Not entirely sure how I would go about this one as I cannot override the viewDidAppear method from a Protocol default implementation. Not sure if this approach is viable for that reason.
Inheritance - Have a ViewController base class that is inherited by all custom View Controllers. That base class handles alert checks in its viewDidAppear method.
A tangential question to consider is how I should be handling the case when there is already a presented view controller when I need to present the alert view controller? I do not want to interrupt the user so I would want to queue the alert to be presented after the currently presented view controller is dismissed.

you can use this function in AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool
An example for your case
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
//Check if User logged in
if logged
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController =
storyboard.instantiateViewController(withIdentifier: "yourLoggedinVC")
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let initialViewController =
storyboard.instantiateViewController(withIdentifier: "yourLoggedOutVC")
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
}

Related

open specific view controller upon tapping local notification

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

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 present login screen only when a userdefaults key doesn't exist?

I am trying to develop an application (still learning) where i present a logon screen which takes a username and password - this then goes off to a web service to authenticate and returns an access token.
The access token is then stored in userdefaults and then presents a new view controller which gives access to the secured data.
My problem is that when i close my app - force close, it then asks to login again.
Because my login view controller is the initial view controller then i added a check to see if access token exists in userdefaults and present the new view controller which gives access to the secured data. Now my problem is that the login screen is always open behind my secured view controller so when opening the app from scratch you can briefly see the login view controller before it then presents the secured view controller.
How would i ideally handle this, is it the case the initial view controller is set to the secured view controller when the user defaults key exists - but doing this how would i handle the logout function as i would need to 'pop' to root view controller and clear user defaults, but since the login screen isnt in the view hierarchy then i cannot return to this? If it presented the login view controller on logout then the secured view controller still exists under the login view controller.
Sorry if this is a little long winded but just trying to describe the problem i am having.
Thanks
You just need to in the Appdelegate.swift's application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) method to judge.
But the precondition is you should manual operation the window:
Delete this line in your info.plist:
Then in your AppDelegate.swift you can set your window manually:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
self.window = UIWindow.init(frame: UIScreen.main.bounds)
let sb:UIStoryboard = UIStoryboard.init(name: "Main", bundle: nil)
let isLogin:Bool = UserDefaults.standard.bool(forKey: "isLogin")
if isLogin {
let vc2 = sb.instantiateViewController(withIdentifier: "ViewController2")
self.window?.rootViewController = vc2
}else {
let vc1 = sb.instantiateViewController(withIdentifier: "ViewController")
self.window?.rootViewController = vc1
}
self.window?.makeKeyAndVisible()
return true
}
And in your ViewController.swift(you can regard it as LoginVc):
override func viewDidLoad() {
super.viewDidLoad()
/* add userdefaults */
UserDefaults.standard.set(true, forKey: "isLogin")
UserDefaults.standard.synchronize()
}

Open a different UIViewController when app is launched via SiriKit

I am trying to implement SiriKit in my iOS app. I want to open a different view controller when the app is launched through Siri.
How can I handle this type of operation in my app?
You can do this, however, first you will have to setup SiriKit in your app, which is a bit of work with a long list of instructions: https://developer.apple.com/library/prerelease/content/documentation/Intents/Conceptual/SiriIntegrationGuide/index.html#//apple_ref/doc/uid/TP40016875-CH11-SW1 .
There's also a sample SiriKit App that Apple has put together called UnicornChat: https://developer.apple.com/library/content/samplecode/UnicornChat/Introduction/Intro.html
Once you have added your SiriKit App Extension and have handled your Intent properly, you will be able to send it to your app using a ResponseCode associated with your Intent. This will open your app, and you can catch that and send it to WhateverViewController by adding the following code to your app delegate:
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
// implement to handle user activity created by Siri or by our SiriExtension
let storyboard = UIStoryboard(name: "Main", bundle: nil)
// Access the storyboard and fetch an instance of the view controller
let viewController: WhateverViewController = storyboard.instantiateViewController(withIdentifier: "WhateverViewController") as! WhateverViewController
window?.rootViewController = viewController
window?.makeKeyAndVisible()
}

Navigating to Specific View Controllers from AppDelegate Methods

At the moment, I have implemented these two methods in my AppDelegate
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool
and
func application(app: UIApplication, openURL url: NSURL, options: [String : AnyObject]) -> Bool
The first will get called if the user opens my app with a search result from Spotlight and the second one gets called if my app gets opened from Apple Maps (since it's a routing app).
MY QUESTION IS, WHAT IS THE BEST WAY TO GO TO A SPECIFIC UIViewController FROM APPDELEGATE (independent from no matter what view the user is in)?
The reason I ask is because at the moment I'm trying to navigate to it manually depending where the user may be. For example, they may be in a UIViewController that is displayed modally (which then needs to be dismissed) or they may be deep in a UINavigationController, in which the app will then need to call popToRootViewController.
Doing it this way, the code is getting hairy and doesn't seem to work right. It also just doesn't seem right to do it this way either because it is very fragile.
I just was trying to figure out the how to implement the first method you mentioned and found a helpful start from https://www.hackingwithswift.com/read/32/4/how-to-add-core-spotlight-to-index-your-app-content
His code is:
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
if userActivity.activityType == CSSearchableItemActionType {
if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
let splitViewController = self.window!.rootViewController as! UISplitViewController
let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController
if let masterVC = navigationController.topViewController as? MasterViewController {
masterVC.showTutorial(Int(uniqueIdentifier)!)
}
}
}
return true
}
I found self.window?.rootViewController as? yourRootViewControllerClass a good springboard for your question.
My code, which was very basic, looks like this:
// create a storyBoard item to instantiate a viewController from. If you have multiple storyboards, use the appropriate one. I just have one so it is "Main"
let sb = UIStoryboard(name: "Main", bundle: nil)
// get the navigation controller from the window and instantiate the viewcontroller I need.
if let viewController = sb.instantiateViewControllerWithIdentifier("DetailViewController") as? ViewController,
let nav = window?.rootViewController as? UINavigationController {
viewController.setupController(bookName)// setup the viewController however you need to. I have a method that I use (I grabbed the bookName variable from the userActivity)
nav.pushViewController(viewController, animated: false)//use the navigation controller to present this view.
}
This works for me but I must give a caveat. My app has only one storyboard with three viewControllers (NavController->tableViewController->ViewController). I am not sure how this logic will work on more complex apps.
Another good reference is: http://www.appcoda.com/core-spotlight-framework/
func application(application: UIApplication, continueUserActivity userActivity: NSUserActivity, restorationHandler: ([AnyObject]?) -> Void) -> Bool {
let viewController = (window?.rootViewController as! UINavigationController).viewControllers[0] as! ViewController
viewController.restoreUserActivityState(userActivity)
return true
}
That viewController has this method:
override func restoreUserActivityState(activity: NSUserActivity) {
if activity.activityType == CSSearchableItemActionType {
if let userInfo = activity.userInfo {
let selectedMovie = userInfo[CSSearchableItemActivityIdentifier] as! String
selectedMovieIndex = Int(selectedMovie.componentsSeparatedByString(".").last!)
performSegueWithIdentifier("idSegueShowMovieDetails", sender: self)
}
}
}
I think the good place for renavigation is an event UIApplicationDidBecomeActiveNotification with subscription in root view controller (whatever you use).
When you do your code in AppDelegate - just schedule an Action: assemble your parcel with abstract number of properties, data parameters and so on. Store it somewhere (NSUserDefauils - is good place to be, but it may be even SqlCipher instance). And keep rest with hoping to following event.
When UIApplicationDidBecomeActiveNotification is fired - wake up, catch stored Action parcell, and perform your renavigation according to Action properties.
About your modal view controllers. They (exactly - controllers WHO show) should be ready to dismiss all VC's they show modally when renavigation event arrive.

Resources