How to get the current top of the stack from the UINavigationController? - ios

Here is a UIViewController where a UINavigationController has been created:
class Main_ProfileViewController: UIViewController {
var ProfileNavigationController = UINavigationController()
buildProfileNavigationController()
func buildProfileNavigationController() {
let RootViewController = BlankViewController200()
ProfileNavigationController = UINavigationController(rootViewController: RootViewController)
self.view.addSubview(ProfileNavigationController.view)
}
}
And here is the main container class with the navigation controller delegate to get the current top of the stack:
class MainContainerViewController: UIViewController {
var currentTop: UIViewController?
}
extension MainContainerViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
currentTop = viewController
print(currentTop)
}
}
However, nothing is ever printed to the console. How is this achieved?

For getting top viewController you can use this code:
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}

Related

UINavigationControllerDelegate doesn't work when back twice

I use coordinator pattern with child coordinators. I have a problem with removing child coordinator of my child coordinator.
This is a sequence of my coordinators:
HomeCoordinator -> RoutineCoordinator (child of HomeCoordinator) -> ExerciseCoordinator (child of RoutineCordinator) -> CustomExerciseCoordinator (child of ExerciseCoordinator)
To get know when user pops view controllers I use method didShow from navigation controller delegate.
When I push view controllers, everything is ok, but I when I move back, method didShow is called only once. When I use back button twice, the second time didShow is not called.
Example:
I move back from CustomExerciseCoordinator to ExerciseCoordinator and didShow works properly. Then I immediately move back to previous coordinator (RoutineCoordinator) and didShow is not called.
I am not sure if it is needed to show all coordinators, because the code for each coordinator looks similar, but below it is shown.
class HomeCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.tintColor = .white
super.init()
navigationController.delegate = self
}
func start() {
let vc = HomeFactory.makeHomeScene(delegate: self)
navigationController.pushViewController(vc, animated: false)
}
}
extension HomeCoordinator: HomeCoordinatorDelegate {
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
class NewRoutineCoordinator: NSObject, Coordinator {
var exerciseNumber: Int?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
weak var passWorkoutToHomeDelegate: PassWorkoutToHome?
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewRoutineFactory.makeNewRoutineScene(delegate: self)
navigationController.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(vc, animated: true)
}
}
extension NewRoutineCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewRoutineViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewExerciseFactory.newExerciseScene(delegate: self)
navigationController.pushViewController(vc, animated: true)
}
extension ExerciseCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewExerciseViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
I guess something is nil the second time you navigate back. This often happens to me when I instantiate a view controller inside a function which it gets de-initialized so my delegation doesn't work anymore. What I usually do is declare a variable outside the function and assign the instantiated view controller to it.
For Instance try:
var newRoutineCoordinator: NewRoutineCoordinator?
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
newRoutineCoordinator = child
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
Not sure the problem is there but this is just to give you an example.
You have UINavigationControllerDelegate inside an extension of NewRoutineCoordinator but It may be already nil by the time you navigate back.

Tab bar controllers with coordinators

I use coordinator pattern in my app, but I have problem with instantiate view controllers. The problem is that I use different module for each tab bar controller.
So far I've used this approach
protocol Storyboarded {
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
And during creation tab bar coordinator:
class MainTabBarController: UITabBarController, Storyboarded {
let main = MainCoordinator(navigationController: UINavigationController())
let calendar = CalendarCoordinator(navigationController: UINavigationController())
let chart = ChartCoordinator(navigationController: UINavigationController())
let profile = ProfileCoordinator(navigationController: UINavigationController())
override func viewDidLoad() {
super.viewDidLoad()
main.start()
calendar.start()
chart.start()
profile.start()
viewControllers = [main.navigationController, calendar.navigationController, chart.navigationController, profile.navigationController]
}
My all view controllers conform to Storyboarded Protocol:
class HomeTableViewController: UIViewController, Storyboarded {}
And coordinator for each tab bar controller looks like this
class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeTableViewController.instantiate()
vc.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "home"), tag: 0)
navigationController.pushViewController(vc, animated: false)
}
}
The problem is that other tab bar controllers belong to others storyboard, not only "Main". Using instantiate() from protocol causes error. I wonder how to create protocol extension where I can initialise ViewControllers with different storyboard names, not only "Main".
Try add this:
protocol Storyboarded {
static var storyboardName: String { get }
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static var storyboardName: String {
"Main" // Default implementation
}
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: Self.storyboardName, bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
Now you can do:
extension HomeTableViewController: Storyboarded {
class var storyboardName: String {
"Home" // Other storyboard name here, overrides default implementation
}
}

How can I navigate from custom class to UIViewController in Swift 4

I have a ViewController which have a button, it calls some method in MyClass.swift. I'm trying to navigate from MyClass.swift to UIViewController MainViewController when I press that button. I implemented this method and I get "navigating..." in the log but nothing happen.
class Myclass {
// some code ...
func someFunc() {
// some code ...
navigateToMainViewController()
}
func navigateToMainViewController() {
let storyboard: UIStoryboard? = UIApplication.shared.keyWindow?.rootViewController?.storyboard
if let myMVC = storyboard?.instantiateViewController(withIdentifier: "MainViewController") {
print("navigating....")
let navController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController
navController?.pushViewController(myMVC, animated: true)
}else {
print("Something wrong..")
}
}
// some code ...
}
Thank your for your help.
Change your navigation Function and add a parameter of viewController to it, and take a variable in myClass of type UIViewController:
var viewController: UIViewController!
func someFunc(_ viewController: UIViewController) {
self.viewController = viewController
self.navigateToMainViewController()
}
func navigateToMainViewController() {
let storyboard: UIStoryboard? = UIApplication.shared.keyWindow?.rootViewController?.storyboard
if let myMVC = storyboard?.instantiateViewController(withIdentifier: "MainViewController") {
print("navigating....")
self.viewController.navigationController?.pushViewController(myMVC, animated: true)
}else {
print("Something wrong..")
}
}
Then From viewController, Call someFunc() with instance of ViewController:
class ViewController: UIViewController {
#IBAction func buttonPressed(_ sender: UIButton) {
myClassObject.someFunc(self)
}
}

How to get the ViewController from UITabBarItem

When a user taps on an item, I want to get the ViewController associated with that tab.
The TabBar delegate no longer provides the ViewController delegate. Instead, it provides a didSelectItem delegate.
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
}
How do I get the ViewController from UITabBarItem?
If you're using a UITabBarController and not just a UITabBar look into using UITabBarControllerDelegate rather than UITabBarDelegate. UITabBarControllerDelegate provides the method:
func tabBarController(_ tabBarController: UITabBarController,
didSelectViewController viewController: UIViewController)
This is a swift 4 Extension version that works for me:
import UIKit
extension UIApplication {
var visibleViewController: UIViewController? {
guard let rootViewController = keyWindow?.rootViewController else {
return nil
}
return getVisibleViewController(rootViewController)
}
private func getVisibleViewController(_ rootViewController: UIViewController) -> UIViewController? {
if let presentedViewController = rootViewController.presentedViewController {
return getVisibleViewController(presentedViewController)
}
if let navigationController = rootViewController as? UINavigationController {
return navigationController.visibleViewController
}
if let tabBarController = rootViewController as? UITabBarController {
// Uncomment the line bellow the TabBarController
//return tabBarController.selectedViewController
// uncomment the line bellow to get the visible ViewController of the TabBarController
return getVisibleViewController(tabBarController.selectedViewController!)
}
return rootViewController
}
}
This can be called as easy as this:
let visibleVC = UIApplication.shared.visibleViewController
I hope this work for you as well ;)

Remove a custom UIViewController class inside Navigation stack generically

I want to remove some controllers of specific type inside UINavigationController stack programmatically.
Working non-generic fuction:
if let navigationController = navigationController {
var controllers = [AnyObject]()
for item in navigationController.viewControllers {
if !(item is CustomViewController) {
controllers.append(item)
}
}
navigationController.viewControllers = controllers
}
However, I'm trying to make this one becomes generic.
func removeController<T>(controller: T.Type, navigationController: UINavigationController?) {
if let navigationController = navigationController {
var controllerArray = [AnyObject]()
for item in navigationController.viewControllers {
if !(item is T.Type) {
controllerArray.append(item)
}
}
navigationController.viewControllers = controllerArray
}
}
removeController(CustomViewController.self, navigationController)
After many attempts, it still doesn't work. Can anybody help me please, thank you.
Try out the following code:
func removeController<T>(type: T.Type, navigationController: UINavigationController?) {
if let navigationController = navigationController {
var controllerArray = [UIViewController]()
for item in navigationController.viewControllers as [UIViewController] {
if !(item is T) {
controllerArray.append(item)
}
}
navigationController.viewControllers = controllerArray
}
}
removeController(CustomViewController.self, navController)

Resources