Open tonyskansf opened 3 years ago
Hey @tonyskansf, a coordinator is an object controlling a certain view controller. In the case of NavigationCoordinator, this is a viewcontroller of type "UINavigationController". If you use a TabBarCoordinator, the rootViewController (i.e. the one being coordinated) is a UITabBarCoordinator.
When you specify these routers in the initializer of a TabBarCoordinator, it will call the UITabBarController's setViewControllers method. So, when a tab is selected, the UITabBarController will switch the tab to that NavigationCoordinator's rootViewController, which is an empty UINavigationController (hence, the screen is black).
What should actually happen is, that whenever you create the TabBarCoordinator, it should try to present the sheet and fail, since the SpecialCoordinator's rootViewController is not yet in the view hierarchy. You can probably see something similar in the console.
A possible solution to this would be a custom UITabBarControllerDelegate implementation overriding the tabBarController(_:shouldSelect:)
method and set this to your AppCoordinator's delegate
property. In your custom override, you should decipher, whether the viewController to be selected is your SpecialCoordinator's rootViewController. If it is, then return false, but also figure out how to then trigger the sheet route.
I would probably do something like this:
class TabSheetViewController: UIViewController {} // This is the type of viewController to present a sheet for, instead of selecting it.
class SheetTabBarDelegate: NSObject, UITabBarControllerDelegate {
var didSelectSheet: (UIViewController) -> Void
init(didSelectSheet: @escaping (UIViewController) -> Void) {
self.didSelectSheet = didSelectSheet
}
open func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool {
if viewController is TabSheetViewController {
didSelectSheet()
return false
} else {
return true
}
}
}
class AppCoordinator: TabBarCoordinator<AppRoute> {
var sheetDelegateObject: SheetTabBarDelegate?
init() {
let normalCoordinator = NormalCoordinator()
normalCoordinator.rootViewController.tabBarItem = UITabBarItem(tabBarSystemItem: .recents, tag: 0)
let sheetVC = TabSheetViewController()
sheetVC.tabBarItem = UITabBarItem(tabBarSystemItem: .more, tag: 1)
super.init(tabs: [normalCoordinator, sheetVC], select: normalCoordinator)
sheetDelegateObject = SheetTabBarDelegate { [weak self] in self?.trigger(.presentSheet) }
delegate = sheetDelegateObject
}
}
π Hi @pauljohanneskraft, thanks for your response. Your suggestion put me on a right track.
So intially I've tried to alter the code a bit. (For readability I've cut out some part of the code)
class SheetTabBarDelegate: NSObject, UITabBarControllerDelegate {
open func tabBarController(...) -> Bool {
if viewController is TabSheetViewController {
didSelectSheet(viewController) // pass viewController to be presented to the delegate
return false
} else {
return true
}
}
}
class AppCoordinator: TabBarCoordinator<AppRoute> {
init() {
// ...
sheetDelegateObject = SheetTabBarDelegate { [weak self] in self?.trigger(.presentSheet($0) }
}
override func prepareTransition(for route: AppRoute) -> TabBarTransition {
switch route {
case let .presentSheet(viewController):
return .present(viewController)
}
}
}
However, the application crashed whenever I tapped the bar that was supposed to present a sheet due to:
Application tried to present modally an active controller <UITabBarController: 0x106076000>
The workaround I've tried and seemed to work, but I'm not sure if it is a good practice or if the library should be used like this.
I've basically created a coordinator for the view controller to be presented with two routes -- initial
& sheet
-- and put this coordinator in the place of the previous sheetVC
. The initial
route is just a dummy holding a view controller that will satisfy the if-statement in UITabBarControllerDelegate
.
So instead of calling self.trigger(.presentSheet)
I call coordinator.trigger(.sheet)
in the delegate callback. While this solution "works" I believe it might be error prone as I got this warning instead.
Presenting view controllers on detached view controllers is discouraged <UINavigationController: 0x1088ada00>
I will keep this solution for now as this feature is not fundamental for my application. Although, if you know why this is, I'd highly appreciate your help as I already do. π
Hi. Is there any progress on this issue?
I found a simple solution.
The reason for the error is to use the same UIViewController
inside the UITabBarController
.
I create a new coordinator and UIViewController
like this:
import UIKit
import XCoordinator
enum EmptyRoute: Route {
case empty
}
class EmptyViewController: UIViewController {
// MARK: - Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
}
class EmptyCoordinator: NavigationCoordinator<EmptyRoute> {
// MARK: - Initialization
init() {
super.init(initialRoute: .empty)
}
// MARK: - Overrides
override func prepareTransition(for route: EmptyRoute) -> NavigationTransition {
let viewController = EmptyViewController()
return .push(viewController)
}
}
Then I brought this coordinator instead of the coordinator you want to present:
convenience init() {
let firstCoordinator = FirstCoordinator()
firstCoordinator.rootViewController.tabBarItem = .init(title: "First", image: nil, tag: 0)
let emptyCoordinator = EmptyCoordinator()
emptyCoordinator.rootViewController.tabBarItem = .init(title: "Second", image: nil, tag: 0)
let thirdCoordinator = ThirdCoordinator()
thirdCoordinator.rootViewController.tabBarItem = .init(title: "Third", image: nil, tag: 0)
self.init(
firstRouter: firstCoordinator.strongRouter,
emptyRouter: emptyCoordinator.strongRouter,
secondRouter: thirdCoordinator.strongRouter
)
}
I use custom tab bar delegate like this:
super.init(
tabs: [
firstRouter,
emptyRouter,
thirdRouter
],
select: firstRouter
)
sheetDelegateObject = SheetTabBarDelegate { [unowned self] _ in
self.trigger(.second)
}
delegate = sheetDelegateObject
SheetTabBarDelegate itself:
open class SheetTabBarDelegate: NSObject, UITabBarControllerDelegate {
// MARK: - Properties
public var didSelectSheet: (UIViewController) -> Void
// MARK: - Initialization
public init(didSelectSheet: @escaping (UIViewController) -> Void) {
self.didSelectSheet = didSelectSheet
}
// MARK: - Methods
open func tabBarController(
_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController
) -> Bool {
if viewController.children.first is EmptyViewController {
didSelectSheet(viewController)
return false
} else {
return true
}
}
}
Finally, I use SecondCoordinator (which is the coordinator we want to present) at prepareTransition
:
override func prepareTransition(for route: MainRoute) -> TabBarTransition {
switch route {
case .first:
return .select(firstRouter)
case .second:
return .presentFullScreen(secondRouter)
case .third:
return .select(thirdRouter)
}
}
Hi, I am trying out the XCoordinator and got this problem, which I do not know the solution to. Not sure if this is the right place to ask such question so I am sorry if this does not belong here.
I want to implement a tab bar that has one specific tab, which only presents a view controller on tap.
Code
For the sake of example the code is simplified to two tabs --
SpecialCoordinator
is the one I want to present a view controller.I have created an extension to present a
Presentable
modally.Both Normal and Special Coordinators are
NavigationCoordinator<...>
and the implementation of SpecialCoordinator looks like this:If I use
.push(vc)
the view controller gets pushed; however, if.presentAsSheet(vc)
is used tabs are switched but the screen is black (nothing gets presented).Can you please help me? Thanks.