Navigating to Specific View Controllers from AppDelegate Methods - ios

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.

Related

3D Touch quick actions not working when called from didFinishLaunchingWithOptions

I'm implementing 3D touch quick actions in my app and I have the following problem:
When the app is already running and so the quick action is initiated through perform Action For Shortcut Item, it works perfectly fine. However, when the app is killed and then launched through a quick action (so didFinishLaunchingWithOptions) it does not take me to the desired view controller, but rather to the home screen of the app.
Here is my code:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
//... other stuff I'm doing
if let shortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem {
shortcutItemToProcess = shortcutItem
}
return true
NOTE: I've read previous SO answers where they said that I need to return false in didFinishLaunchingWithOptions when the app was launched through a quick action, so that performAction won't get called. I need to always return true however in my didFinishLaunching method because of the other things I'm handling there. I tried however to return false just to see if that causes the problem and the app still behaved the same way.
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
shortcutItemToProcess = shortcutItem
}
Here is how I present the view controller:
func applicationDidBecomeActive(_ application: UIApplication) {
if let shortcutItem = shortcutItemToProcess {
if shortcutItem.type == "com.myName.MyApp.myQuickAction" {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myViewController = storyboard.instantiateViewController(withIdentifier: "myViewController") as! MyViewController
if let navVC = window?.rootViewController as! UINavigationController? {
navVC.pushViewController(myViewController, animated: true)
}
}
So this works fine when app is already running, but it lands me on the home page of my app when the app is killed. What am I doing wrong and how can I solve this?

How can I pass data loaded when my application finished launching thru my UITabBar to a UITableView?

I am a new iOS programmer and I am trying to make an application that involves the following flow:
-> UITabBar -> UINavController -> UITableViewController.
Initially the program was working when I have the following flow:
-> UINavController -> UITableViewController
But when I added the UITabBar (with the Embed In method)the I had two issues:
1) Casting the initial view from UITableView to UITabBarView
2) The data which have been restored from Archives of the phone are not loading in the TableView.
I managed to fix the casting issue with the UIStoryboard IDs, but I am not sure if this way I created the second problem of not passing data to the UITableView correctly.
The Casting problem has taking place at the appDelegate code. Here is the original code I had before incorporating the UITabBarView:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let navController = window!.rootViewController as! UINavigationController
let SLBprojectController = navController.topViewController as! GR8TableView
SLBprojectController.SLBprojectDB = thisSLBprojectDB
return true
}
The problem with the above code was that it told me that it could not cast a TableViewController (GR8TableView) into a UITabBarView. I have managed to fix this by searching in the StackOverflow forums by making the following:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let mainStoryboardIpad : UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
let tabBarIntial : UITabBarController = mainStoryboardIpad.instantiateViewController(withIdentifier: "TabBar") as! UITabBarController
let navigationController:UINavigationController = mainStoryboardIpad.instantiateViewController(withIdentifier: "navController") as! UINavigationController
let navigationController1:UIViewController = mainStoryboardIpad.instantiateViewController(withIdentifier: "ViewController1")
let SLBprojectController = navigationController.topViewController as! GR8TableView
SLBprojectController.SLBprojectDB = thisSLBprojectDB
tabBarIntial.viewControllers = [navigationController, navigationController1]
tabBarIntial.selectedIndex = 0
return true
}
But after I fixed the "casting" issue then I am getting problems loading the data in the TableView. Not sure if this problem is related to the way I fixed the casting issue.
Any assistance would be much much appreciated!
The simplest answer would be to go back to logic similar to what worked for you, but with a slightly more complex relationship. For example, I created an app with a tab bar controller, a navigation controller in the first tab, and a FirstViewController embedded in the navigation controller.
Since I don't have a database, I gave the FirstViewController a single instance variable called running.
The following code finds the controller and sets the variable:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if let tabController = window!.rootViewController as? UITabBarController {
if let navController = tabController.viewControllers![0] as? UINavigationController {
if let firstController = navController.topViewController as? FirstViewController {
firstController.running = true
} else {
print("Unexpected controller in navigation hierarchy: \(String(describing: navController.topViewController))")
}
} else {
print("Unexpected contained controller: \(String(describing: tabController.viewControllers![0]))")
}
} else {
print("Unexpected root controller: \(String(describing: window!.rootViewController))")
}
return true
}
Note that I'm not creating any new controllers. That's all done by default storyboard loading.
I believe you should be able to adapt this to your own variable and class names.

How to I instantiate multiple View Controllers from AppDelegate?

There is a container view which holds 3 view controllers (V1, V2 and V3). I'm able to switch from A, B or C by swiping left or right. Both A or B contain their own collection view. If I tap on any cell in the collection view inside A or B, the PlayerVC (named Player in the pic above ^^) launches and a video begins to play using AVPlayer.
The problem is: Since I am using universal links the user goes straight to the playerView to play a video but when they press the done button the app crashes. The issue I beleive is because the rest on the view controllers are NOT initialized? How do I initialize the container view and the other view controllers? Or if this is not the issue please let me know what the problem is.
Here is the app delegate with some sample code. Please provide code if possible to help out!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let pagingViewController = window?.rootViewController as? PagingViewController {
pagingViewController.videoPlaybackManager = videoPlaybackManager
}
return true
}
class AppDelegate: UIResponder, UIApplicationDelegate {
// Other App Delegate methods.....
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool {
// 1) Make sure the passed `user activity` has expected characteristics.
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else {
return false
}
// HELP: I need to get to `PlayerVC` from here?
return true
// If we can't do the above we default to opening the page in safari
}
}
EDIT - Additional code of how I am instantiating the 3 VCs inside PagingViewController ( container View)
private func setupViewControllers() {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
page1 = storyboard.instantiateViewController(withIdentifier: StoryboardIdentifiers.feedViewController.rawValue) as! FeedViewController
page1.view.translatesAutoresizingMaskIntoConstraints = false
page1.delegate = self
scrollView.addSubview(page1.view)
addChildViewController(page1)
page1.didMove(toParentViewController: self)
// Inject dependency.
page1.videoPlaybackManager = videoPlaybackManager
page2 = storyboard.instantiateViewController(withIdentifier: StoryboardIdentifiers.favoritesViewController.rawValue) as! FavoritesViewController
page2.view.translatesAutoresizingMaskIntoConstraints = false
page2.delegate = self
scrollView.addSubview(page2.view)
addChildViewController(page2)
page2.didMove(toParentViewController: self)
page3 = storyboard.instantiateViewController(withIdentifier: StoryboardIdentifiers.settingsViewController.rawValue) as! SettingsViewController
page3.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.addSubview(page3.view)
addChildViewController(page3)
page3.didMove(toParentViewController: self)
......
If you are dealing with UIPageViewController, to set the proper child UIViewController you can:
// In PagingViewController
func loadChildViewControllerFromUniversalLink(myParameter: Bool) {
// The condition you need to satisfied in order to present
// the correct child view controller
if myParameter { // is satisfied
let universalLinkVc = MyChildViewController() // load your VC here
let direction : UIPageViewControllerNavigationDirection = .forward // or .reverse, as you wish
self.setViewControllers([universalLinkVc], direction: direction, animated: animated, completion: nil)
}
}
And then in your AppDelegate's application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: #escaping ([Any]?) -> Void) -> Bool:
myPagingViewControllerInstance.loadChildViewControllerFromUniversalLink(true)

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

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.

Resources