How to use SceneCoordinator with UITabBarController?

It seems no way to have any transition in UITabBarController’s viewControllers.

3 Likes

Yes, I’m trying to find a way to integrate SceneCoordinators and TabBar Controller. What did you end up with?

I added a scene for each tab bar and a transition type for tab bar. After that, I used didSelect delegate to change the current view controller in the scene coordinator.

1 Like

Hi noahblues,

Thanks for the reply. I’m not sure how you wire up the transitions for each view controller.
Do you have the project up on Github?
or could you post the code?

Thank you!

Same here, any idea will be great :smiley: If I find a solution I’ll post here here…

I’m looking for something like this :

case .selectIndex(let index):
            if window.rootViewController as? UITabBarController != nil {
                let tababarController = window.rootViewController as! UITabBarController
                tababarController.selectedIndex = index
            }
        } 

@noahblues did you do that?

@eddiek @b9bloch

// TabBar
func bindViewModel() {
    rx.didSelect
      .map { [weak self] in
        self?.viewControllers?.index(of: $0) ?? 0
      }
      .bind(to: viewModel.tabSwitchingAction.inputs)
      .disposed(by: rx_disposeBag)
  }

// View model for tab bar
lazy var tabSwitchingAction: Action<Int, Void> = { (this: RootTabViewModel) in
    return Action { index in
      this.sceneCoordinator.transition(to: Scene.tabSwitching, type: .rootTabsSwitching(index))
    }
 }(self)

// Scene Coordinator
case .rootTabsSwitching(let index):
      guard var tabVC = currentViewController.tabBarController else {
        fatalError("Can't switch a view controller without a current tab bar controller")
      }

      while tabVC.tabBarController != nil {
        tabVC = tabVC.tabBarController!
      }

      guard let viewController = tabVC.viewControllers?[index] else {
        fatalError("Index not in range of the tab bar controller's view controllers.")
      }

      tabVC.selectedIndex = index
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      subject.onCompleted()
    }


static func actualViewController(for viewController: UIViewController) -> UIViewController {
    if let navigationController = viewController as? UINavigationController {
      return SceneCoordinator.actualViewController(for: navigationController.viewControllers.first!)
    } else if let tabBarController = viewController as? UITabBarController,
        let selectedViewController = tabBarController.selectedViewController {
      return SceneCoordinator.actualViewController(for: selectedViewController)
    } else {
      return viewController
    }
  }

Sorry for the missing piece: Scene+ViewControllers

// 
func viewControllers() -> [UIViewController] {
    switch self {
    case .root(let viewModel):
      var rootTabBarVC = R.storyboard.main.rootTabBar()!
      rootTabBarVC.bindViewModel(to: viewModel)
      return [rootTabBarVC]
    case .rootTabs(let boxViewModel, let activitiesViewModel, let requirements):
      let boxNC = R.storyboard.main.boxNavigation()!
      var boxViewController = boxNC.viewControllers.first as! BoxViewController
      boxViewController.bindViewModel(to: boxViewModel)
      
      var activitiesVC = R.storyboard.main.activities()!
      let activitiesNavigation = UINavigationController(rootViewController: activitiesVC)
      activitiesVC.bindViewModel(to: activitiesViewModel)

      let requirementPages = RequirementPagesViewController(viewModel: requirements)
          let requirementsContainer = MaterialsContainerViewController(pages: requirementPages)
          let navi = UINavigationController(rootViewController: requirementsContainer)

      return [boxNC, activitiesNavigation, navi]
  case .tabSwitching:
      return []
  }
}

Hello @noahblues ! Thank you for your help :slight_smile: I’m almost like your solution but I think my solution isn’t scalable

(I’ve edited my post because my first question was ugly ^^)

So actually I’ve something like this :

///Enum Scene
enum Scene {
    case users(UsersViewModel)
    case players(PlayerViewModel)
    case tabSwitching
    case mainTabBAr(MainTabBarViewModel, UsersViewModel, PlayerViewModel)
}

///Enum Scene+Vc
extension Scene {
    func viewController() -> UIViewController {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        switch self {
        case .users(let viewModel):
            let nc = storyboard.instantiateViewController(withIdentifier: "Users") as! UINavigationController
            var vc = nc.viewControllers.first as! ViewController
            vc.bindViewModel(to: viewModel)
            return nc
        case .players(let viewModel):
            let nc = storyboard.instantiateViewController(withIdentifier: "Players") as! UINavigationController
            var vc = nc.viewControllers.first as! PlayerViewController
            vc.bindViewModel(to: viewModel)
            return nc
        case .tabSwitching:
            let mainTabBarController = storyboard.instantiateViewController(withIdentifier: "HomeTabBar") as! UITabBarController
            return mainTabBarController
        case .mainTabBAr(let mainTabBarViewModel, let usersViewModel, let playersViewModel):
            var mainTabBarController = storyboard.instantiateViewController(withIdentifier: "HomeTabBar") as! MainTabBarController
            mainTabBarController.bindViewModel(to: mainTabBarViewModel)
            
            let nc1 = storyboard.instantiateViewController(withIdentifier: "Users") as! UINavigationController
            var vc1 = nc1.viewControllers.first as! ViewController
            vc1.bindViewModel(to: usersViewModel)
            nc1.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.contacts, tag: 0)
            
            
            let nc2 = storyboard.instantiateViewController(withIdentifier: "Players") as! UINavigationController
            var vc2 = nc2.viewControllers.first as! PlayerViewController
            vc2.bindViewModel(to: playersViewModel)
            
            mainTabBarController.viewControllers = [nc1, nc2]
            nc2.tabBarItem = UITabBarItem(tabBarSystemItem: UITabBarSystemItem.downloads, tag: 0)
            
            return mainTabBarController
        }
    }
}


///Enum SceneTransitionType
enum SceneTransitionType {
    // you can extend this to add animated transition types,
    // interactive transitions and even child view controllers!
    
    case root       // make view controller the root view controller
    case push       // push view controller to navigation stack
    case modal      // present view controller modally
    case rootTabsSwitching(Int) // Select index for tabbar
}

In the AppDelegate I instantiate my app with :

let sceneCoordinator = SceneCoordinator(window: window!)

    let playerService = PlayerService()
    let playerViewModel = PlayerViewModel(playerService: playerService, coordinator: sceneCoordinator)
    
    let userService = UserService()
    let userViewModel = UsersViewModel(userService: userService, coordinator: sceneCoordinator)

    let mainViewModel = MainTabBarViewModel(coordinator: sceneCoordinator)
    
    let mainScene = Scene.mainTabBAr(mainViewModel, userViewModel, playerViewModel)
    sceneCoordinator.transition(to: mainScene, type: .root)

But I think my solution in the Scene+ViewController isn’t very good… There are many lines which can be simplified… I wanted to use

mainTabBarCOntroller.viewCOntrollers = [Scene.users(usersViewModel), Scene.players(playersViewModel)]

So I’m loonking on it, and the principal problem is that I’ve to keep the .tabSwitching value in the enum and I don’t like to have two times almost the same code…

Do you have a better solution?

Best regards

@b9bloch I modified the viewController method in Scene+ViewController file to return multiple view controllers in an array.
And in the transition method is the place where I set the viewControllers of UITabBarController

And this is my SceneTransitionType and the transition method:

enum SceneTransitionType {
  case root
  case push
  case modal
  case tabs                                   // Set tabs
  case tabSwitching(Int)               // Switching on tab bars other than the root tab bar.
  case rootTabsSwitching(Int)      // Switching tabs on root tab bar.
}

func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void> {
  // ... ignored code
  case .tabs:
      let tabVC = currentViewController as! UITabBarController
      tabVC.setViewControllers(viewControllers, animated: false)
      currentViewController = SceneCoordinator.actualViewController(for: viewControllers[0])
      subject.onCompleted()
 // ... ignored code
}
 

@noahblues Thank you for your help, it’s more clear for me now :slight_smile:

Hey @noahblues can you share your
Scene+ViewController
Scene
SceneCoordinator
SceneTransitionType

that would give me a clear idea about TabBar navigation.

Thanks

@hbasin3 Let me help you as I have faced the need today:

SceneTransitionType

import Foundation

enum SceneTransitionType {
  // you can extend this to add animated transition types,
  // interactive transitions and even child view controllers!
  case root
  case push
  case modal
  case initTabs
  case rootTabsSwitching(Int)
}
extension Scene {
  func viewController() -> UIViewController  {

    switch self {
    case .home(let viewModel): //here is the entry point to my TabBarController after authentication
      var tc = HomeTabBarController()
      tc.bindViewModel(to: viewModel)
      return tc
    case .tabSwitching: // in order not to go down to optionals I just initiate empty UIViewController, I believe it will be automatically deleted after non-use
      return UIViewController()
    }
}
enum Scene {
  case home(HomeViewModel)
  case tabSwitching
}
protocol SceneCoordinatorType {
  @discardableResult
  func transition(to scene: Scene, type: SceneTransitionType) -> Completable
  
  @discardableResult
  func pop(animated: Bool) -> Completable
}

extension SceneCoordinatorType {
  @discardableResult
  func pop() -> Completable {
    return pop(animated: true)
  }
}

class SceneCoordinator: SceneCoordinatorType {
  
  fileprivate var window: UIWindow
  fileprivate var currentViewController: UIViewController
  fileprivate let tabs: [UIViewController]
  
  required init(window: UIWindow) {
    self.window = window
    currentViewController = window.rootViewController!
    
    let tabOne = TabOneVC()
    tabOne.tabBarItem = UITabBarItem(title: "Tab Ibe", image: Icons.tabOne, selectedImage: Icons.tabOne)
    
   let tabTwo = TabTwoVC()
    tabTwo.tabBarItem = UITabBarItem(title: "Tab Two", image: Icons.tabTwo, selectedImage: Icons.tabTwo)
    self.tabs = [tabOne, tabTwo]
  }
  
  static func actualViewController(for viewController: UIViewController) -> UIViewController {
    if let navigationController = viewController as? UINavigationController {
      return navigationController.viewControllers.first!
    } else if let tabBarController = viewController as? UITabBarController,
              let selectedViewController = tabBarController.selectedViewController {
      return SceneCoordinator.actualViewController(for: selectedViewController)
    } else {
      return viewController
    }
  }
  
  @discardableResult
  func transition(to scene: Scene, type: SceneTransitionType) -> Completable {
    let subject = PublishSubject<Void>()
    let viewController = scene.viewController()

    switch type {
    case .root:
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      window.rootViewController = viewController
      subject.onCompleted()
      
    case .push:
      guard let navigationController = currentViewController.navigationController else {
        fatalError("Can't push a view controller without a current navigation controller")
      }
      // one-off subscription to be notified when push complete
      _ = navigationController.rx.delegate
        .sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
        .map { _ in }
        .bind(to: subject)
      navigationController.pushViewController(viewController, animated: true)
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      
    case .modal:
      currentViewController.present(viewController, animated: true) {
        subject.onCompleted()
      }
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      
    case .initTabs:
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      window.rootViewController = viewController

      let tabVC = viewController as! UITabBarController
      tabVC.setViewControllers(tabs, animated: false)
      currentViewController = SceneCoordinator.actualViewController(for: tabs[0])
      subject.onCompleted()
      
    case .rootTabsSwitching(let index):
      guard var tabVC = currentViewController.tabBarController else {
        fatalError("Can't switch a view controller without a current tab bar controller")
      }
      
      while tabVC.tabBarController != nil {
        tabVC = tabVC.tabBarController!
      }
      
      guard let viewController = tabVC.viewControllers?[index] else {
        fatalError("Index not in range of the tab bar controller's view controllers.")
      }
      
      tabVC.selectedIndex = index
      currentViewController = SceneCoordinator.actualViewController(for: viewController)
      subject.onCompleted()

//    case .tabSwitching(let index):
//      subject.onCompleted()
//      return []
    }
    
    return subject.asObservable()
      .take(1)
      .ignoreElements()
  }
  
  @discardableResult
  func pop(animated: Bool) -> Completable {
    let subject = PublishSubject<Void>()
    if let presenter = currentViewController.presentingViewController {
      // dismiss a modal controller
      currentViewController.dismiss(animated: animated) {
        self.currentViewController = SceneCoordinator.actualViewController(for: presenter)
        subject.onCompleted()
      }
    } else if let navigationController = currentViewController.navigationController {
      // navigate up the stack
      // one-off subscription to be notified when pop complete
      _ = navigationController.rx.delegate
        .sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
        .map { _ in }
        .bind(to: subject)
      guard navigationController.popViewController(animated: animated) != nil else {
        fatalError("can't navigate back from \(currentViewController)")
      }
      currentViewController = SceneCoordinator.actualViewController(for: navigationController.viewControllers.last!)
    } else {
      fatalError("Not a modal, no navigation controller: can't navigate back from \(currentViewController)")
    }
    return subject.asObservable()
      .take(1)
      .ignoreElements()
  }
}

And I use it like this:

     let homeViewModel = HomeViewModel(
        viewerService: self.service,
        coordinator: self.sceneCoordinator
      )
      
      self.sceneCoordinator.transition(to: Scene.home(homeViewModel), type: .initTabs)

Inside TabBar controller as mentioned above:

 func bindViewModel() {
    rx.didSelect
      .map { [weak self] in
        self?.viewControllers?.index(of: $0) ?? 0
      }
      .bind(to: viewModel.tabSwitchingAction.inputs)
      .disposed(by: self.rx.disposeBag)
  }

and in ViewModel

lazy var tabSwitchingAction: Action<Int, Void> = { (this: HomeViewModel) in
    return Action { index in
      this.sceneCoordinator
        .transition(to: Scene.tabSwitching, type: .rootTabsSwitching(index))
        .asObservable()
        .map { _ in }
    }
  }(self)

Enjoy!

I don’t want to make my tabBar a rootViewController instead i want to push it.
i am able to push it.

But when i try navigate from 1st tab bar controller or any tabbar controller to an inside controller via push it pushes the whole tabbar.

Can you please help me out in this?

Provide as much details as possible with code samples as I don’t follow. I also can suggest to look at RxFlow. I’m not sure why would you need to have more than one TabBarControllers.