Setting common values for viewControllers in UITabBarController with moreNavigationController - ios

I have 'n' ViewControllers, all taking a common variable 'cv1', I would like to set this cv1 before the controllers are loaded.
Everything is fine till I set only 4 controllers, (i.e) without More tab item. and once I introduce more controllers, delegate "tabBarController:didSelectViewController" doesn't trigger for the modules in UIMoreNavigationController.
So I tried setting the UINavigationController delegate for my TabBarController Class and handled the other module tabItem selection like
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if navigationController != self.moreNavigationController
{
//navigation happening in some other tab Item
return
}
if navigationController.viewControllers.count != 2
{
//The first controller of the more tabItem will be MoreViewController, followed by our desired controller in the navigation stack
return
}
guard let selectedController:MyTabItemViewController = self.getTopViewController(from: viewController) else {
print("***WARNING*** Selected controller doesn't seem to expect info :\(String(describing: selectedViewController))")
return
}
selectedController.sp = self.sp
selectedController.sb = self.sb
}
I am able to get the ViewController instance, but the viewDidLoad method gets called before UINavigationController's willShow method, I would like to set the variables before the viewDidLoad method as the UI structuring and data fetch depends on this variable.

Related

iOS Black screen when close modally presented view controller

I have a tabbar controller and suppose if the user goes to 3rd tab and in 3rd view controller, User presses a button and there I present the view controller as modal .
Now if user switched the tab let say tab 1 or 2 without closing the modally presented view controller, and mean while he comes back to tab 3, the user will still able to see the modally presented view controller, now if user closes the modally presented view controller here, then there is a black screen instead of view controller that opened the modally presented view controller.
The possible work around is that I hide the tabbar controller so that user must close modally presented the view controller first. but it is not the requirement right now. Due to some reasons I can not hide bottom tab bar controller.
Please suggest me how to do following
First of all when user switches tab after opening modally presented view controller, so when he closes the modally presented vc he must see the screen at the back
If point no 1 is not possible so when user goes back again to tab 3 (in which he started modally presented VC) there should not be modally presented view controller. In other words when user switched the tab and the modally presented view controller is opened, it must close, before moving to other tab.
You can do it with UITabBarControllerDelegate.
Option 1: Creating subclass of UITabBarController
class MyTabBar: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let currentlySelectedViewController = self.viewControllers?[self.selectedIndex] as? YourThirdTabViewController,
let presentedViewController = currentlySelectedViewController.presentedViewController {
presentedViewController.dismiss(animated: false, completion: nil)
}
return true
}
}
Here I am assuming that I have my own sub class of UITabBarController which is set as UITabBarControllerDelegate using self.delegate = self in view didLoad.
Option 2: Make TabBarController's ViewController UITabBarControllerDelegate in their ViewDidLoad
If you dont wanna have your own subclass of UITabBarController all you have to do is simply set your self as UITabBarControllerDelegate in each ViewController's ViewDidLoad
You can have a common protocol or extension to UIViewController and confirm UITabBarControllerDelegate to avoid code duplication if you decide to go down the path of not subclassing UITabBarController
protocol TabBarControllerProtocol: UITabBarControllerDelegate where Self: UIViewController {}
extension TabBarControllerProtocol {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
if let currentlySelectedViewController = self.tabBarController?.viewControllers?[self.tabBarController?.selectedIndex ?? 0] as? SomeViewController,
let presentedViewController = currentlySelectedViewController.presentedViewController {
presentedViewController.dismiss(animated: false, completion: nil)
}
return true
}
}
In whichever ViewController, you want this logic to kick in you can say
class SomeViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
}
}
extension SomeViewController: TabBarControllerProtocol {}
Thats it.
shouldSelect viewController gets called every time user taps on tab to switch.
The if condition checks if ViewController at selectedIndex (already selected not the new one) is viewController of specific interest or not and also checks if this view controller has presented anything or not, if it has presented it will call dismiss on it
This way when user switches tab from 3rd tab to any other tab, if third tab VC has presented another view controller modally it will dismiss it before it switches tab.

Is it possible to set a coordinator property on a UITabBarController?

I'm trying to implement a slide out menu in my tab-based app. I have a container controller which holds the MenuViewController and the UITabBarController, and it conforms to a MasterDelegate protocol which tells the container to slide out the MenuViewController when the hamburger button located on an individual tab is pressed. My problem is that the coordinator property for the UITabBarController is not set until after the UITabBarController is configured, and thus the property in the UITabBarController as well as the ViewController for the tab containing the button is always nil.
Here is the code belonging to the container that sets up the UITabBarController:
func configureHomeController() {
centerController = MainTabBarController()
// Make sure to set delegate of homeController
centerController.masterDelegate = self
print("MainContainterController: Setting the MainTabBarController delegate")
if centerController.masterDelegate != nil {
print("MainContainerController: MainTabBarController Delegate set")
}
view.addSubview(centerController.view)
// Add the navigationController containing the HomeController as a child to the ContainerController
addChild(centerController)
// Home controller didMove to the parent container of self, which is the ContainerController
centerController.didMove(toParent: self)
}
In the TabBarController, there is a print statement that executes after the TabBarController is configured which tells me if the masterDelegate property is nil or not. This is printed before the container controller prints the two above statements. Since the 2nd print statement is printed, I know that the coordinator is being set, but it's set too late.
My question is how do I get the masterDelegate to be set before the UITabBarController is done being configured? Or is there a way to set that property after the fact?
Create a convenience init for your tabbarcontroller and pass the delegate to it. Something like this:
convenience init(masterDelegate: MasterDelegate) {
self.init(nibName: nil, bundle: nil)
self.masterDelegate = masterDelegate
}
the when you are setting up the TabBarController, instantiate it like this:
centerController = MainTabBarController(masterDelegate: self)
centerController.configure()
Also, make a configure function in MainTabBarController. In there, set up your viewControllers.
(in MainTabBarController)
func configure() {
viewControllers = [viewController, viewController2, ....]
}

NavigationController.topViewController is equal to viewController in navigationController delegate

I am implementing the navigationControllerDelegate func:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
print("callin the Navigation Controller Delgate")
if viewController === self {
print("Calling the Navigation Controller delegate because is self and going to call tapButton")
//want to know who was previously on top of navigation.
}
}
I want to know here which was the viewController that is being removed form the stack since apple docs says that the
viewController
The view controller whose view and navigation item properties are being shown.
This means that this assumption is true:
viewController == navigationController.topViewController
or this one:
viewController == navigationController.visibleViewController
If not then one of this are the the viewControllers that is going to be removed.
It's not cleat for me since the func parameter name is willShow viewController, or is just a fancy name and the will show is the already shown.
So if not how from the delegate I may know which VC is being removed from the Navigation Stack.
It would have been nice if the delegate provided the viewController being removed, but I found it's just straight forward just to keep a reference.
weak var lastRemovedViewController: UIViewController? = nil
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
defer { lastRemovedViewController = viewController }
print("Navigation Removed \(lastRemovedViewController.debugDescription)")
}
yes is the answer, the viewController passed in the delegate func is the same as the topViewController, also is equal to navigationController.visibleViewController. so there is no way to know which was the removed viewController from the stack.

Coordinator pattern with UINavigationControllers and a UITabBarController

I'm trying to learn how to integrate coordinator pattern into iOS development.
I have an app which like this. In the storyboard, it looks like this. The navigation controllers and tabbars are not added in the storyboard because according to coordinator pattern, they will be added programatically.
The first view controller is PhoneViewController which takes user's phone number. This view controller is embedded in a navigation controller. After entering the phone number, it moves to the VerifyPhoneViewController. After verification, it moves to MainViewController a tabbarcontroller which contains three tabs. Each of these view controller will have a separate navigation controller of their own.
I have a protocol which contains all the necessary properties and functions each coordinator needs to implement.
protocol Coordinator {
var childCoordinators: [Coordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
I created a separate coordinator called AuthCoordinator for the authentication flow part of the app.
class AuthCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
// The initial view
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.pushViewController(phoneViewController, animated: false)
}
func submit(phoneNo: String) {
let verifyPhoneViewController = VerifyPhoneViewController.instantiate()
verifyPhoneViewController.coordinator = self
verifyPhoneViewController.phoneNo = phoneNo
navigationController.pushViewController(verifyPhoneViewController, animated: true)
}
// Move to the tabbarcontroller
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
}
The navigation works fine. However there's a small issue.
Notice after moving to the tabbarcontroller, the titles don't show in the navigationbar when I switch between view controllers (I do set them in viewDidLoad method of each view controller). Plus the back button to VerifyPhoneViewController is still there too.
The issue is obvious. The navigationcontroller I initialized for the AuthCoordinator is still there at the top. I'm literally pushing the MainViewController on to that stack.
func main() {
let mainViewController = MainViewController.instantiate()
navigationController.pushViewController(mainViewController, animated: true)
}
What I can't figure out is a way to not do it like this. I can hide the navigationbar in the start method but then it's not ideal because well, it hides the navigationbar and I don't want that.
func start() {
let phoneViewController = PhoneViewController.instantiate()
phoneViewController.coordinator = self
navigationController.navigationBar.isHidden = true
navigationController.pushViewController(phoneViewController, animated: false)
}
Is there a different way to keep the navigationcontroller for the duration of the auth flow and then discard it when/soon after showing the MainViewController?
The demo project is uploaded here.

UITabBarController's viewControllers present modal controller issue

I have a UITabBarController with 4 viewControllers setup.
One of the controller has a button that present another controller (wrapped on UINavigationController) with the following setup:
self.definesPresentationContext = true
navController.modalPresentationStyle = .overCurrentContext
navController.modalTransitionStyle = .crossDissolve
self.present(navController, animated: true)
Until this point is working fine.
Now if I switch to another tab (while the previous modal is open), and return again to the tab that presented the modal (The screen is still there, that's ok). Then if i close the modal (from a Button), the modal is dismissed but the controller view's has gone (white), then if I switch to another tab and return to the tab again, the view load correctly.
Note: For this case, I need overCurrentContext, don't want to block UITabBarController (with fullScreen).. Also try with .currentContext, custom
If this is the same bug that I demonstrate here, the workaround I give is to prevent the user from switching to another tab while this tab is showing the presented view controller:
override func viewDidLoad() {
super.viewDidLoad()
self.tabBarController?.delegate = self
}
extension FirstViewController : UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
return self.presentedViewController == nil
}
}

Resources