It seems no way to have any transition in UITabBarController’s viewControllers
.
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.
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 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?
// 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 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
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.