Open a different UIViewController when app is launched via SiriKit - ios

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

Related

"No windows have a root view controller, cannot save application state" when trying to persist iOS app state

I want to adopt the state restoration in my app. I've opted out of using SceneDelegate because my app is not supposed to be running in multiple windows.
So I've implemented the necessary methods in my AppDelegate
func application(_ application: UIApplication, shouldSaveSecureApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreSecureApplicationState coder: NSCoder) -> Bool {
return true
}
But every time I quit the app I get this message in the debugger
No windows have a root view controller, cannot save application state
I setup my window programmatically at the launch of app like this
func configureWindow() {
let window = UIWindow(frame: UIScreen.main.bounds)
let launchViewController = Storyboard.launch.instantiate() as LaunchViewController
launchViewController.viewModel = LaunchViewModel(context: context)
window.rootViewController = RootNavigationController(rootViewController: launchViewController)
window.makeKeyAndVisible()
self.window = window
}
So I don't see how is it possible that at the time the app gets killed it has no rootViewController.
This definitely seems like a bug to me, but has anyone encountered such issue?
I've opted out of using SceneDelegate because my app is not supposed to be running in multiple windows.
Check this answer, it links to the article, that says
The best way to implement UI state restoration is to make your app scene-based

iOS Universal Link opens app, does not trigger app delegate methods

I am trying to enable universal links on iOS, (as part of Firebase's password-less sign-up). Testing locally on iOS 13.2.
The apple-app-site-associated (AASA) JSON looks as such (https://lokitools.page.link/apple-app-site-association):
{"applinks":{"apps":[],"details":[{"appID":"43S54AHEMG.com.jkalash.Loki","paths":["NOT /_/*","/*"]}]}}
Universal links do open the app, however I am unable to handle the app opening from them. Delegate methods:
application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool
do not get called, when opening from universal links. Tried both apps running in the background and force closed. AASA validator (https://branch.io/resources/aasa-validator/) says file looks good, and I have tried troubleshooting by re-installing app and observing console logs for swcd (https://ios13.dev/universal-links-debugging-on-ios-13-cjwsux93w001p6ws1swtstmzc) but nothing out of the ordinary shows up and it does look like the AASA file was downloaded.
I have also tried following Apple's troubleshooting guide (https://developer.apple.com/library/archive/qa/qa1916/_index.html) but the final step which fails (step 8) does not cover my case which is the app does open (iOS detects universal links), but the delegate methods just don't get called.
Turns out this is not a universal links specific problem, but a change in iOS 13's way of triggering app lifecycle events. Instead of coming through UIApplicationDelegate, they come through UISceneDelegate.
One confusing thing is that the app delegate methods aren't deprecated so you won't get a warning if you have both app delegate and scene delegate methods in place but only one will be called.
Refer to App delegate methods aren't being called in iOS 13 for a comprehensive answer
I am using iOS 13 with Swift 5, replace the application (: continue: restorationHandler :) method of the AppDelegate.swift file and add the scene (: continue :) method to the SceneDelgate.swift file
In my case in the SceneDelegate.swift file add the following:
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let urlToOpen = userActivity.webpageURL else {
return
}
handleURL(urlToOpen)
}
Since you are able to open the app, I think all is good with your AASA file. The following delegate method gets called fine in my case:
func application(_: UIApplication, continue userActivity: NSUserActivity, restorationHandler _: #escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL else {
return false
}
let urlString = url.absoluteString
var queryParams: [String: String?] = [:]
if let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let params = components.queryItems {
for param in params {
queryParams[param.name] = param.value
}
}
return true
}
Hope it helps!
If you use Google Analytics, please refer to my here. The issue may be caused by method swizzling.

Best Approach to Present Modals After App Reaches Foreground

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

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

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