Remove a custom UIViewController class inside Navigation stack generically - ios

I want to remove some controllers of specific type inside UINavigationController stack programmatically.
Working non-generic fuction:
if let navigationController = navigationController {
var controllers = [AnyObject]()
for item in navigationController.viewControllers {
if !(item is CustomViewController) {
controllers.append(item)
}
}
navigationController.viewControllers = controllers
}
However, I'm trying to make this one becomes generic.
func removeController<T>(controller: T.Type, navigationController: UINavigationController?) {
if let navigationController = navigationController {
var controllerArray = [AnyObject]()
for item in navigationController.viewControllers {
if !(item is T.Type) {
controllerArray.append(item)
}
}
navigationController.viewControllers = controllerArray
}
}
removeController(CustomViewController.self, navigationController)
After many attempts, it still doesn't work. Can anybody help me please, thank you.

Try out the following code:
func removeController<T>(type: T.Type, navigationController: UINavigationController?) {
if let navigationController = navigationController {
var controllerArray = [UIViewController]()
for item in navigationController.viewControllers as [UIViewController] {
if !(item is T) {
controllerArray.append(item)
}
}
navigationController.viewControllers = controllerArray
}
}
removeController(CustomViewController.self, navController)

Related

UINavigationControllerDelegate doesn't work when back twice

I use coordinator pattern with child coordinators. I have a problem with removing child coordinator of my child coordinator.
This is a sequence of my coordinators:
HomeCoordinator -> RoutineCoordinator (child of HomeCoordinator) -> ExerciseCoordinator (child of RoutineCordinator) -> CustomExerciseCoordinator (child of ExerciseCoordinator)
To get know when user pops view controllers I use method didShow from navigation controller delegate.
When I push view controllers, everything is ok, but I when I move back, method didShow is called only once. When I use back button twice, the second time didShow is not called.
Example:
I move back from CustomExerciseCoordinator to ExerciseCoordinator and didShow works properly. Then I immediately move back to previous coordinator (RoutineCoordinator) and didShow is not called.
I am not sure if it is needed to show all coordinators, because the code for each coordinator looks similar, but below it is shown.
class HomeCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
navigationController.navigationBar.prefersLargeTitles = true
navigationController.navigationBar.tintColor = .white
super.init()
navigationController.delegate = self
}
func start() {
let vc = HomeFactory.makeHomeScene(delegate: self)
navigationController.pushViewController(vc, animated: false)
}
}
extension HomeCoordinator: HomeCoordinatorDelegate {
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
class NewRoutineCoordinator: NSObject, Coordinator {
var exerciseNumber: Int?
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
weak var passWorkoutToHomeDelegate: PassWorkoutToHome?
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewRoutineFactory.makeNewRoutineScene(delegate: self)
navigationController.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(vc, animated: true)
}
}
extension NewRoutineCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewRoutineViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
class ExerciseCoordinator: NSObject, Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
private var removeCoordinatorWhenViewDismissed: ((Coordinator) -> ())
init(navigationController: UINavigationController, removeCoordinatorWith removeCoordinatorWhenViewDismissed: #escaping ((Coordinator) -> ())) {
self.navigationController = navigationController
self.removeCoordinatorWhenViewDismissed = removeCoordinatorWhenViewDismissed
}
func start() {
navigationController.delegate = self
let vc = NewExerciseFactory.newExerciseScene(delegate: self)
navigationController.pushViewController(vc, animated: true)
}
extension ExerciseCoordinator: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
guard let fromViewController = navigationController.transitionCoordinator?.viewController(forKey: .from) else {
return
}
if navigationController.viewControllers.contains(fromViewController) {
return
}
if let vc = fromViewController as? NewExerciseViewController {
removeCoordinatorWhenViewDismissed(self)
}
}
}
I guess something is nil the second time you navigate back. This often happens to me when I instantiate a view controller inside a function which it gets de-initialized so my delegation doesn't work anymore. What I usually do is declare a variable outside the function and assign the instantiated view controller to it.
For Instance try:
var newRoutineCoordinator: NewRoutineCoordinator?
func goToWorkoutCreating() {
let child = NewRoutineCoordinator(navigationController: navigationController, removeCoordinatorWith: removeChild)
newRoutineCoordinator = child
child.passWorkoutToHomeDelegate = self
addChild(child: child)
child.start()
}
Not sure the problem is there but this is just to give you an example.
You have UINavigationControllerDelegate inside an extension of NewRoutineCoordinator but It may be already nil by the time you navigate back.

Tab bar controllers with coordinators

I use coordinator pattern in my app, but I have problem with instantiate view controllers. The problem is that I use different module for each tab bar controller.
So far I've used this approach
protocol Storyboarded {
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
And during creation tab bar coordinator:
class MainTabBarController: UITabBarController, Storyboarded {
let main = MainCoordinator(navigationController: UINavigationController())
let calendar = CalendarCoordinator(navigationController: UINavigationController())
let chart = ChartCoordinator(navigationController: UINavigationController())
let profile = ProfileCoordinator(navigationController: UINavigationController())
override func viewDidLoad() {
super.viewDidLoad()
main.start()
calendar.start()
chart.start()
profile.start()
viewControllers = [main.navigationController, calendar.navigationController, chart.navigationController, profile.navigationController]
}
My all view controllers conform to Storyboarded Protocol:
class HomeTableViewController: UIViewController, Storyboarded {}
And coordinator for each tab bar controller looks like this
class MainCoordinator: Coordinator {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
let vc = HomeTableViewController.instantiate()
vc.tabBarItem = UITabBarItem(title: "Home", image: UIImage(systemName: "home"), tag: 0)
navigationController.pushViewController(vc, animated: false)
}
}
The problem is that other tab bar controllers belong to others storyboard, not only "Main". Using instantiate() from protocol causes error. I wonder how to create protocol extension where I can initialise ViewControllers with different storyboard names, not only "Main".
Try add this:
protocol Storyboarded {
static var storyboardName: String { get }
static func instantiate() -> Self
}
extension Storyboarded where Self: UIViewController {
static var storyboardName: String {
"Main" // Default implementation
}
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: Self.storyboardName, bundle: Bundle.main)
return storyboard.instantiateViewController(identifier: id) as! Self
}
}
Now you can do:
extension HomeTableViewController: Storyboarded {
class var storyboardName: String {
"Home" // Other storyboard name here, overrides default implementation
}
}

Swift - Change var with selectedIndex

I'm trying to go from a ViewController1 to a ViewController2 by changing the selectedIndex of the tabBarController.
In ViewController1:
func action() {
_ = self.tabBarController?.selectedIndex = 4
}
In ViewController2:
class ViewController2: UIViewController {
var isOnInfo = true
}
My problem is pretty simple. I would like to change the value of isOnInfo in the action() function.
Is it possible?
Another way to do that would be to create a global var, but if I can avoid that it would be better.
Thank you for you answers.
I can recommend you to move your changing tabs logic in Tab bar controller.
Make your own class TabBarController(subclass of UITabBarController), set it as custom class of your tab bar controller at the storyBoard and move the function action() there.
import UIKit
class TabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
}
func action(isOnInfo: Bool) {
selectedIndex = 4
if let viewControllers = self.viewControllers,
let viewController2 = viewControllers[selectedIndex] as? ViewController2 {
viewController2.isOnInfo = isOnInfo
}
}
}
Then in viewController1:
if let tabBarController = self.tabBarController as? TabBarController {
tabBarController.action(isOnInfo: false)
}
You can get object of viewcontroller at selected index in tabbarcontroller. once you have it, you can modify any public property.
func action() {
_ = self.tabBarController?.selectedIndex = 4
if let vc2 = self.tabBarController?.viewControllers![4] as?
ViewController2
{
vc2.isOnInfo = true
}
}

I am trying to using a custom delegate in swift but when I want to call its methods, it did not get called

I have been searching for how the delegate works and I tried to do it in my project. Unfortunately, the delegate method I implement does not get called ever. I am trying to do a slide-out navigation panel. so what I did is that I put two uicontainerviews, one is for slide-out navigation panel and the other for main view controller
enter image description here
The code is that
For main view controller
protocol MainViewControllerDelegate {
func toggleSideMenu()
}
class MainViewController: UIViewController {
var delegate: MainViewControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Slide Action
#IBAction func slideMenuTapped(_ sender: UIBarButtonItem){
delegate?.toggleSideMenu()
print("Slide Menu has been tapped")
}
}
For container view controller
class ContainerVC: UIViewController {
#IBOutlet weak var SideMenuConstraint: NSLayoutConstraint!
#IBOutlet weak var slideMenuContainer: UIView!
#IBOutlet weak var mainViewContainer: UIView!
var mainViewController: MainViewController?
var isSideMenuOpened = false
override func viewDidLoad() {
super.viewDidLoad()
mainViewController = UIStoryboard.mainViewController()
mainViewController?.delegate = self
}
}
extension ContainerVC: MainViewControllerDelegate{
func toggleSideMenu() {
print("It works")
if isSideMenuOpened{
isSideMenuOpened = false
SideMenuConstraint.constant = -260
mainViewContainer.layer.shadowOpacity = 0
} else {
isSideMenuOpened = true
SideMenuConstraint.constant = 0
mainViewContainer.layer.shadowOpacity = 0.59
}
UIView.animate(withDuration: 0.3) {
self.view.layoutIfNeeded()
}
}
}
extension UIStoryboard{
static func mainStoryboard() -> UIStoryboard { return UIStoryboard(name: "Main", bundle: Bundle.main) }
static func mainViewController() -> MainViewController? {
return mainStoryboard().instantiateViewController(withIdentifier: "MainViewController") as? MainViewController
}
}
Please let know what's wrong
I think the reason is that you embed your main view controller in navigation controller :
let navigationController = self.childViewControllers.last as! UINavigationController
let mainViewController = navigationController.topViewController as! MainViewController
mainViewController?.delegate = self
Here is where you got wrong:
mainViewController = UIStoryboard.mainViewController()
mainViewController?.delegate = self
this mainViewController is not the same as the child of the container view controller, so setting its delegate doesn't really do anything.
You need to first get the VC that is the child of the container view controller:
mainViewController = self.childViewControllers.last as! MainViewController
mainViewController.delegate = self

How to get the current top of the stack from the UINavigationController?

Here is a UIViewController where a UINavigationController has been created:
class Main_ProfileViewController: UIViewController {
var ProfileNavigationController = UINavigationController()
buildProfileNavigationController()
func buildProfileNavigationController() {
let RootViewController = BlankViewController200()
ProfileNavigationController = UINavigationController(rootViewController: RootViewController)
self.view.addSubview(ProfileNavigationController.view)
}
}
And here is the main container class with the navigation controller delegate to get the current top of the stack:
class MainContainerViewController: UIViewController {
var currentTop: UIViewController?
}
extension MainContainerViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) {
currentTop = viewController
print(currentTop)
}
}
However, nothing is ever printed to the console. How is this achieved?
For getting top viewController you can use this code:
extension UIApplication {
class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let navigationController = controller as? UINavigationController {
return topViewController(controller: navigationController.visibleViewController)
}
if let tabController = controller as? UITabBarController {
if let selected = tabController.selectedViewController {
return topViewController(controller: selected)
}
}
if let presented = controller?.presentedViewController {
return topViewController(controller: presented)
}
return controller
}
}

Resources