Coordinator pattern with UINavigationControllers and a UITabBarController - ios

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.

Related

How to use the UInavigationcontroller animation with library Motion?

I came across a library call Motion on GitHub which provide sets of transition on view and controllers.
Based on the website, to use the animation on navigation controller we need to create a new navigation controller class like below, and I apply the new navigation controller. But when it failed and not pushing to the new view controller.
the GitHub link is https://github.com/CosmicMind/Motion
class AppNavigationController: UINavigationController {
open override func viewDidLoad() {
super.viewDidLoad()
isMotionEnabled = true
motionNavigationTransitionType = .zoom
}
}
then on the main view:
let navigation = AppNavigationController()
navigation.pushViewController(newViewController, animated: false)
If you're using Motion's animated NavigationController throughout your app, you can set it this way in your AppDelegate's didFinishLaunchingWithOptions method.
let newViewController = NewViewController(nibName: "NewViewController",bundle: nil)
let navigation = AppNavigationController(rootViewController: newViewController!)
self.window?.rootViewController = navigation
self.window?.makeKeyAndVisible()
UPDATE:
If you want to create a separate NavigationController and push specific UIViewController to it, then you need to present the navigation controller instead of pushing it. See below.
let motionNavController = AppNavigationController(rootViewController: galleryViewController)
motionNavController.motionNavigationTransitionType = .none
motionNavController.isNavigationBarHidden = true
self.present(motionNavController, animated: true, completion: nil)

Setting common values for viewControllers in UITabBarController with moreNavigationController

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.

How to present a view controller from a detached view controller?

I'm trying to present a view controller modally and get the famous Presenting view controllers on detached view controllers is discouraged error. I researched it and the consensus solution appears to be making the presentation from the parent, which I tried but did not have success with. I suspect the problem is because the navigation controller was instantiated from a struct as a static property (to make it easier for other view controller's to pop to root as this is what the UX called for).
struct SectionNavigationControllers {
static var one = SectionNavigationController()
static var two = SectionNavigationController()
static var three = SectionNavigationController()
static var four = SectionNavigationController()
}
And here is where one of the navigation controllers is created (using this struct):
let SectionOneRoot = MasterSearchViewController()
func addNavigationController() {
self.addChildViewController(SectionOneRoot)
SectionOneRoot.didMove(toParentViewController: self)
SectionNavigationControllers.one = SectionNavigationController(rootViewController: SectionOneRoot)
view.addSubview(SectionNavigationControllers.one.view)
}
And so when I try to present a view controller modally from MasterSearchViewController (the root view controller), I get the said error.
navigationController?.present(Random200ViewController(), animated: true, completion: nil)
Ideas?
Here's a convenience function you can add to any piece of code to present a view controller from anywhere in your app:
func showModally(_ viewController: UIViewController) {
let window = UIApplication.shared.keyWindow
let rootViewController = window?.rootViewController
rootViewController?.present(viewController, animated: true, completion: nil)
}
I hope it helps!
If you want to present it on your app's root viewController, you can do it like this:
let rootVC = UIApplication.shared.keyWindow?.rootViewController
rootVC?.present(Random200ViewController(), animated: true, completion: nil)

Making a conditional tab bar in ios

I'm working on an IOS app that uses tabs for navigation. The app gives access to users to a video library. However there are two types of users, those who purchase individual episodes and those who are subscribed. The former only have access to the videos they purchased while the latter have access to every single video in the library.
In my tab bar (in storyboard) I have a Purchases button, but if the user is a subscriber I don't want this tab to appear.
The app checks if a user is logged in upon launching and checks to see what the user status is (buyer or subscriber). I would like to know if there is a way to load different sets of tabs depending on the user type.
If any one could steer me in the right direction I'd really appreciate it. Thanks!
From the top of my head I can think of several ways but this could do it. I am assuming that you somehow know which kind of user is logged in based on the server's response or something similar.
Create your own class that mutates depending on the user eg:
MyTabBarController: UITabBarController {
override func viewDidLoad() {
if (currentUser == admin) {
setupAdminTabBar()
} else {
setupRegularTabBar()
}
}
}
then on each function do something like
func setupRegularTabBar() {
//do this many as many times as root view controllers you want
let searchNavController = createMyNavController(unselectedImage: UIImage(named: "yourimage"), selectedImage: UIImage(named: "yourimage"), rootViewController: UserSearchController(collectionViewLayout: UICollectionViewFlowLayout()))
//add the other controllers that you create like the one above...
viewControllers = [searchNavController]
}
fileprivate func createMyNavController (unselectedImage: UIImage, selectedImage: UIImage, rootViewController : UIViewController = UIViewController()) -> UINavigationController {
let viewController = rootViewController
let navController = UINavigationController(rootViewController: viewController)
navController.tabBarItem.image = unselectedImage
navController.tabBarItem.selectedImage = selectedImage
return navController
}
Subclass UITabBarController and use setViewControllers(_:animated:):
class MyTabBarController: UITabBarController
{
override func viewDidLoad()
{
super.viewDidLoad()
switch user
{
case .buyer:
guard let vc1 = storyboard?.instantiateViewController(withIdentifier: "first"),
let vc2 = storyboard?.instantiateViewController(withIdentifier: "second") else
{
return
}
setViewControllers([vc1, vc2], animated: true)
case .subscriber:
guard let vc3 = storyboard?.instantiateViewController(withIdentifier: "third"),
let vc4 = storyboard?.instantiateViewController(withIdentifier: "fourth") else
{
return
}
setViewControllers([vc3, vc4], animated: true)
}
}
}
You can use the setViewControllers function of UITabBarController:
func setViewControllers(_ viewControllers: [UIViewController]?, animated: Bool)
Set up all the possible controllers in the storyboard with a separate outlet for each one. Then pass an array of the outlets you wish to appear to setViewControllers

Accessing the frontmost controller of an iOS App

I'm trying to access the frontmost controller of the Application during the user navigation using this code:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
println(UIApplication.sharedApplication().keyWindow?.rootViewController)
}
But it seems that the rootViewController always refers to the first controller defined by the storyboard independently by when I'm accessing that property.
Is there something I'm doing wrong or I've misunderstood about the rootViewController property?
rootViewController is indeed the topmost, ultimate view controller owned by UIWindow.
To get the currently displaying view controller, you need to walk down the controller hierarchy. Here is an Objective-C category that you can add to your application, and using a bridging header you'll easily be able to call this UIWindow category from your swift code.
OK, based on the code that Michael pointed out, I wrote some Swift (1.2) code to do the same thing. You can add this as an extension to UIViewController (as I did), UIApplication, or for that matter simply make it a global function.
extension UIViewController {
static func getVisibleViewController () -> UIViewController {
let rootViewController = UIApplication.sharedApplication().keyWindow?.rootViewController
return getVisibleViewControllerFrom(rootViewController!)
}
static func getVisibleViewControllerFrom(viewController: UIViewController) -> UIViewController {
let vcToReturn: UIViewController
if let navController = viewController as? UINavigationController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(navController.visibleViewController)
}
else if let tabBarController = viewController as? UITabBarController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(tabBarController.selectedViewController!)
}
else {
if let presentedViewController = viewController.presentedViewController {
vcToReturn = UIViewController.getVisibleViewControllerFrom(presentedViewController)
}
else {
vcToReturn = viewController
}
}
return vcToReturn
}
}
You'd call this in the following way:
let visibleViewController = UIViewController.getVisibleViewController()
Hope this helps.
Andrew
PS I haven't tried this in Swift 2.0 yet, so I can't guarantee it will work without issues there. I know it won't work (as written) in Swift 1.1 or 1.0.

Resources