Swift Deeplink to already open App's already open viewcontroller? - ios

I have an app that accepts a Deeplink URL and opens a viewcontroller with variables from the link and it works well if the App is opened/run for the first time by the user using the Deeplink.
However, if the App is already open/or in the background and has that view controller open... it then opens the same viewcontroller back up again so then I have two. I do not want to open the viewcontroller an additional time.
Is there some way I can identify that viewcontroller that is already open and pass the variables from the Deeplink to it?
or do I need to close it in some way and re-open it?
I am open to suggestions.... thanks in advance.

Try using UIApplication.shared.keyWindow?.rootViewController and testing what class it is. For example:
if let vc = UIApplication.shared.keyWindow?.rootViewController {
if vc is SomeViewController {
// Do something.
}
}

You can find the visible view controller with the following method
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIApplication.shared.keyWindow?.rootViewController
}
if rootVC?.presentedViewController == nil {
return rootVC
}
if let presented = rootVC?.presentedViewController {
if presented.isKind(of: UINavigationController.self) {
let navigationController = presented as! UINavigationController
return navigationController.viewControllers.last!
}
if presented.isKind(of: UITabBarController.self) {
let tabBarController = presented as! UITabBarController
return tabBarController.selectedViewController!
}
return getVisibleViewController(presented)
}
return nil
}
you can then switch on the presented view
if let presentedView = getVisibleViewController(window?.rootViewController) {
switch presentedView {
//code
default:
//code
}
}
and of course in the switch present a view controller if it is not the one that you want to be open.
No need to close a viewcontroller before opening it!

Related

What is the Use of Returning the Same function itself in Swift

Well I see some syntax in the following function which returns the topMostViewController. This function is defined in AppDelegate
func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
//***A topViewController which is Returning itself
//***This is where I got Confusion
return topViewController(controller: navigationController.visibleViewController)
} else if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
} else if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
And it's used as
if (self.topViewController() as? SomeViewController) != nil {
if orientation.isPortrait {
return .portrait
} else {
return .landscape
}
}
I understood that the code is trying to set orientation based on the currently visible View Controller but I don't understand what is the necessity of returning the same function itself in topViewController. Also I see some syntax like
extension UIApplication {
/// The top most view controller
static var topMostViewController: UIViewController? {
return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
}
}
extension UIViewController {
/// The visible view controller from a given view controller
var visibleViewController: UIViewController? {
if let navigationController = self as? UINavigationController {
// *** Here it's returning Same variable i.e visibleViewController
// *** a function could call itself recursively. But how can a Variable calls itself recursively?
return navigationController.topViewController?.visibleViewController
} else if let tabBarController = self as? UITabBarController {
return tabBarController.selectedViewController?.visibleViewController
} else if let presentedViewController = presentedViewController {
return presentedViewController.visibleViewController
} else {
return self
}
}
}
Edited
That is called recursion. There is a condition in the recursion that cause t end the cycle :
Not in the navigationController, because it has another visible controller
Not in the tabBarController, because it has another visible controller
Not presenting another controller, because the presented one is visible
if one of these appears -> we go down one level and call this function again until none of these true.
It is a recursive function. The topViewController function calls itself to find the top most controller which is visible. The function will exit when controller?.presentedViewController returns nil (Which means that the value held by controller is the top most visible controller). You can also achieve the same without a recursive function as mentioned here: How to find topmost view controller on iOS, but it looks much more cleaner than the looping implementation.

Opening ViewController In AppDelegate While Keeping Tabbar

In my Xcode project when a user taps on a notification I want to first send them to a certain item in my tabBar then I want to instantiate a view controller and send an object over to that view controller. I have code the that sends them to the tabBar I want, but I do not know how to instantiate them to the view controller while keeping the tabBar and navigation bar connected to the view controller. All the answers on this require you to change the root view controller and that makes me lose connection to my tabBar and navigation bar when the view controller is called.
A Real Life Example of this: User receives Instagram notification saying "John started following you" -> user taps on notification -> Instagram opens and shows notifications tab -> quickly send user to "John" profile and when the user presses the back button, it sends them back to the notification tab
Should know: The reason why I'm going to a certain tab first is to get that tab's navigation controller because the view controller I'm going to does not have one.
Here's my working code on sending the user to "notifications" tab (I added comments to act like the Instagram example for better understanding):
if let tabbarController = self.window!.rootViewController as? UITabBarController {
tabbarController.selectedViewController = tabbarController.viewControllers?[3] //goes to notifications tab
if type == "follow" { //someone started following current user
//send to user's profile and send the user's id so the app can find all the information of the user
}
}
First of all, you'll to insatiate a TabBarController:
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
And then insatiate all of the viewControllers of TabBarController. If your viewControllers is embedded in to the UINavigationController? If so, you'll to insatiate a Navigation Controller instead:
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
Also you should instantiate your desired ViewController too:
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
Make all of the NavigationControllers as viewControllers of TabBarController:
tabBarController.viewControllers = [first, second, third]
And check: It's about your choice.
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
Make tabBarController as a rootViewController:
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
Finally: It's your completed code:
func openViewController() {
let storyboard = UIStoryboard.init(name: "YourStoryboardName", bundle: nil)
let tabBarController = storyboard.instantiateViewController(withIdentifier: "YourTabBarController") as! UITabBarController
let first = storyboard.instantiateViewiController(withIdentifier: "YourFirstNavigationController") as! UINavigationController
let second = storyboard.instantiateViewiController(withIdentifier: "YourSecondNavigationController") as! UINavigationController
let third = storyboard.instantiateViewiController(withIdentifier: "YourThirdNavigationController") as! UINavigationController
let desiredVC = storyboard.instantiateViewController(withIdentifier: "desiredVC") as! ExampleDesiredViewController
tabBarController.viewControllers = [first, second, third]
if tabBarController.selectedViewController == first {
// Option 1: If you want to present
first.present(desiredVC, animated: true, completion: nil)
// Option 2: If you want to push
first.pushViewController(desiredVC, animated. true)
}
self.window = UIWindow.init(frame: UIScreen.main.bounds)
self.window?.rootViewController = tabBarController
self.window?.makeKeyAndVisible()
}
If you want to present or push ViewController when the notification is tapped? Try something like that:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: #escaping () -> Void) {
switch response.actionIdentifier {
case UNNotificationDefaultActionIdentifier:
openViewController()
completionHandler()
default:
break;
}
}
}
I can think of two ways to do that:
1) If that view controller is a UINavigationController you can simply push the profile from wherever you are:
if let tabNavigationController = tabbarController.viewControllers?[3] as? UINavigationController {
tabbarController.selectedViewController = tabNavigationController
let profileViewController = ProfileViewController(...)
// ... set up the profile by setting the user id or whatever you need to do ...
tabNavigationController.push(profileViewController, animated: true) // animated or not, your choice ;)
}
2) Alternatively, what I like to do is control such things directly from my view controller subclass (in this case, PostListViewController). I have this helper method in a swift file that I include in all of my projects:
extension UIViewController {
var containedViewController: UIViewController {
if let navController = self as? UINavigationController, let first = navController.viewControllers.first {
return first
}
return self
}
}
Then I would do this to push the new view controller:
if let tabViewController = tabbarController.selectedViewController {
tabbarController.selectedViewController = tabViewController
if let postListViewController = tabViewController.containedViewController as? PostListViewController {
postListViewController.goToProfile(for: user) // you need to get the user reference from somewhere first
}
}
In my last live project, I'm using the same approach like yours. So even though I doubt this method is the correct or ideal for handling a push notification from the AppDelegate (I still got a lot of stuff to learn in iOS 🙂), I'm still sharing it because it worked for me and well I believe the code is still readable and quite clean.
The key is to know the levels or stacks of your screens. The what are childViewControllers, the topMost screen, the one the is in the bottom, etc...
Then if you're now ready to push to a certain screen, you would need of course the navigationController of the current screen you're in.
For instance, this code block is from my project's AppDelegate:
func handleDeeplinkedJobId(_ jobIdInt: Int) {
// Check if user is in Auth or in Jobs
if let currentRootViewController = UIApplication.shared.keyWindow!.rootViewController,
let presentedViewController = currentRootViewController.presentedViewController {
if presentedViewController is BaseTabBarController {
if let baseTabBarController = presentedViewController as? BaseTabBarController,
let tabIndex = TabIndex(rawValue: baseTabBarController.selectedIndex) {
switch tabIndex {
case .jobsTab:
....
....
if let jobsTabNavCon = baseTabBarController.viewControllers?.first,
let firstScreen = jobsTabNavCon.childViewControllers.first,
let topMostScreen = jobsTabNavCon.childViewControllers.last {
...
...
So as you can see, I know the hierarchy of the screens, and by using this knowledge as well as some patience in checking if I'm in the right screen by using breakpoints and printobject (po), I get the correct reference. Lastly, in the code above, I have the topMostScreen reference, and I can use that screen's navigationController to push to a new screen if I want to.
Hope this helps!

In TabBarViewController, how can I access my children directly?

I want to access a child called SubscriptionsViewController (3rd tab)
This is what I'm doing, but it doesn't work.
var subscriptionsViewController: SubscriptionsViewController? {
get {
let viewControllers = self.childViewControllers
for viewController in viewControllers {
if let vc = viewController as? SubscriptionsViewController {
return vc
}
}
return nil
}
}
Assuming you have an instance of tab bar controller, you can do it as follows:
var subscriptionsViewController: SubscriptionsViewController? {
get {
let viewControllers = tabController.viewControllers //assuming you have a property tabBarController
for viewController in viewControllers {
if viewController is SubscriptionsViewController {
return vc
}
}
return nil
}
}
You can access a child of your tab bar controller with the following :
self.tabBarController.viewControllers[2]

performSegueWithIdentifier not working when being called from new instance of viewController in Swift

My mission
When app receive notification and user taps on the notification i want to redirect the user to the correct View. In my case, SingleApplicationViewController.
Current code
PushNotification.swift - A class with static functions to handle behaviors when receiving Push Notifications
The __getNavigationController returns a specific NavigationController based on a tab -and viewIndex from TabBarController.
internal static func __getNavigationController(tabIndex: Int, viewIndex: Int) -> UINavigationController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let window:UIWindow? = (UIApplication.sharedApplication().delegate?.window)!
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateViewControllerWithIdentifier("MainEntry")
window?.rootViewController = viewController
let rootViewController = appDelegate.window!.rootViewController as! UITabBarController
rootViewController.selectedIndex = tabIndex
let nav = rootViewController.viewControllers![viewIndex] as! UINavigationController
return nav
}
The applicationClicked is being called when user click on notification and that method calls on __getApplication to fetch the application from the db with the objectId received in the push notification and then instantiate a GroupTableViewController to perform segue to the SingleApplicationViewController.
(TabbarController -> Navigation Controller -> GroupTableViewController -> SingleApplicationViewController)
What is a bit strange is when I set tabIndex to 0 and viewIndex to 1. The GroupView however is on second tab (tab 1) and the view controller should be the first (0). But when I set them to the corresponding numbers, I receive nil and the application crashes.
I read that you will force the view controller to load when doing _ = groupTableViewController.view and which it actually does. When this is being called, the viewDidLoad -function is being called.
/************** APPLICATION ***************/
static func applicationClicked(objectId: String) {
__getApplication(objectId) { (application, error) in
if application != nil && error == nil {
let nav = __getNavigationController(0, viewIndex: 1)
let groupTableViewController = nav.viewControllers.first as! GroupsTableViewController
_ = groupTableViewController.view
groupTableViewController.performSegueWithIdentifier("GroupTableToApplicationToDetailApplication", sender: application!)
} else {
// Hanlde error
}
}
}
GroupTableViewController.prepareForSegue()
Here I create a new instance of the ApplicationTableViewController, which is a middle step before getting to SingleApplicationViewController
} else if segue.identifier == "GroupTableToApplicationToDetailApplication" {
let navC = segue.destinationViewController as! UINavigationController
let controller = navC.topViewController as! ApplicationViewController
controller.performSegueWithIdentifier("ApplicationsToSingleApplicationSegue", sender: sender as! Application)
}
So, what's not working?
Well, the prepareForSegue in GroupTableViewController is not being called. I use the same code structure on my TimeLineViewController, and almost the exact same code, when getting another Push Notification and it works perfectly. In that case I use tabIndex 0 and viewIndex 0 to get the proper NavigationController.
Please, any thoughts and/or suggestions is more than welcome!
There is change in following method..
internal static func __getNavigationController(tabIndex: Int) -> UINavigationController {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let window:UIWindow? = (UIApplication.sharedApplication().delegate?.window)!
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyBoard.instantiateViewControllerWithIdentifier("MainEntry")
window?.rootViewController = viewController
let rootViewController = appDelegate.window!.rootViewController as! UITabBarController
rootViewController.selectedIndex = tabIndex
let nav = rootViewController.selectedViewController as! UINavigationController //This will return navigation controller..
//No need of viewIndex..
return nav
}
you have written
let nav = rootViewController.viewControllers![viewIndex] as! UINavigationController
change to rootViewController.selectedViewController give you UINavigationController.
Here you get navigavtion controller object..In your applicationClicked method nav object might be nil so it can not execute further performsegue code.
Check following method.
/************** APPLICATION ***************/
static func applicationClicked(objectId: String) {
__getApplication(objectId) { (application, error) in
if application != nil && error == nil {
let nav = __getNavigationController(0)//0 is your tab index..if you want 1 then replace it with 1
let groupTableViewController = nav.viewControllers.first as! GroupsTableViewController //Rootview controller of Nav Controller
groupTableViewController.performSegueWithIdentifier("GroupTableToApplicationToDetailApplication", sender: application!) //Perform seque from Root VC...
} else {
// Hanlde error
}
}
}

single function to dismiss all open view controllers

I have an app which is a single view application. I have a navigation controller linked up to all child controllers from the root view controller.
In each child controller, I have a logout button. I'm wondering if I can have a single function I can call which will dismiss all the controllers which have been open along along the way, no matter which controller was currently open when the user presses logout?
My basic start:
func tryLogout(){
self.dismissViewControllerAnimated(true, completion: nil)
let navigationController = UINavigationController(rootViewController: UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("LoginViewController") )
self.presentViewController(navigationController, animated: true, completion: nil)
}
I am looking for the most memory efficient way of carrying out this task. I will put my logout function in a separate utils file, but then I can't use self. And I still have the issue of knowing which controllers to dismiss dynamically.
Update
Pop to root view controller has been suggested. So my attempt is something like:
func tryLogout(ViewController : UIViewController){
print("do something")
dispatch_async(dispatch_get_main_queue(), {
ViewController.navigationController?.popToRootViewControllerAnimated(true)
return
})
}
Would this be the best way to achieve what I'm after?
You can call :
self.view.window!.rootViewController?.dismiss(animated: false, completion: nil)
Should dismiss all view controllers above the root view controller.
Updated answer for Swift 4 and swift 5
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true, completion: nil)
and when you use navigationController
self.navigationController?.popToRootViewController(animated: true)
Works for Swift 4 and Swift 5
To dismiss any unwanted residue Modal ViewControllers, I used this and worked well without retaining any navigation stack references.
UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: false, completion: nil)
self.view.window! did crash for my case possibly because its a Modal screen and had lost the reference to the window.
Swift3
navigationController?.popToRootViewControllerAnimated(true)
Take a look at how unwind segues work. Its super simple, and lets you dismiss/pop to a certain viewcontroller in the heirarchy, even if it consists of a complex navigation (nested pushed and or presented view controllers), without much code.
Here's a very good answer (by smilebot) that shows how to use unwind segues to solve your problem
https://stackoverflow.com/a/27463286/503527
To dismiss all modal Views.
Swift 5
view.window?.rootViewController?.dismiss(animated: true, completion: nil)
If you have a customed UITabbarController, then try dismiss top viewController in UITabbarController by:
class MainTabBarController: UITabBarController {
func aFuncLikeLogout() {
self.presentedViewController?.dismiss(animated: false, completion: nil)
//......
}
}
If you have access to Navigation Controller, you can try something like this. Other solutions didn't work for me.
func popAndDismissAllControllers(animated: Bool) {
var presentedController = navigationController?.presentedViewController
while presentedController != nil {
presentedController?.dismiss(animated: animated)
presentedController = presentedController?.presentedViewController
}
navigationController?.popToRootViewController(animated: animated)
}
works for Swift 3.0 +
self.view.window!.rootViewController?.dismiss(animated: true, completion: nil)
I have figured out a generic function to dismiss all presented controllers using the completion block.
extension UIWindow {
static func keyWindow() -> UIWindow? {
UIApplication.shared.windows.filter({ $0.isKeyWindow }).first
}
}
func getVisibleViewController(_ rootViewController: UIViewController?) -> UIViewController? {
var rootVC = rootViewController
if rootVC == nil {
rootVC = UIWindow.keyWindow()?.rootViewController
}
var presented = rootVC?.presentedViewController
if rootVC?.presentedViewController == nil {
if let isTab = rootVC?.isKind(of: UITabBarController.self), let isNav = rootVC?.isKind(of: UINavigationController.self) {
if !isTab && !isNav {
return rootVC
}
presented = rootVC
} else {
return rootVC
}
}
if let presented = presented {
if presented.isKind(of: UINavigationController.self) {
if let navigationController = presented as? UINavigationController {
return navigationController.viewControllers.last!
}
}
if presented.isKind(of: UITabBarController.self) {
if let tabBarController = presented as? UITabBarController {
if let navigationController = tabBarController.selectedViewController! as? UINavigationController {
return navigationController.viewControllers.last!
} else {
return tabBarController.selectedViewController!
}
}
}
return getVisibleViewController(presented)
}
return nil
}
func dismissedAllAlert(completion: (() -> Void)? = nil) {
if let alert = UIViewController.getVisibleViewController(nil) {
// If you want to dismiss a specific kind of presented controller then
// comment upper line and uncomment below one
// if let alert = UIViewController.getVisibleViewController(nil) as? UIAlertController {
alert.dismiss(animated: true) {
self.dismissedAllAlert(completion: completion)
}
} else {
completion?()
}
}
Note: You call anywhere in code at any class
Use:-
dismissedAllAlert() // For dismiss all presented controller
dismissedAllAlert { // For dismiss all presented controller with completion block
// your code
}
Swift
Used this to jump directly on your ROOT Navigation controller.
self.navigationController?.popToRootViewController(animated: true)

Resources