I'm currently creating an app that uses UINavigationController for screen transitions.
The problem I encountered was the print string in the willShow delegate method on screen B will not be displayed. The transition from screen A to B to A willShow delegate method will be called correctly and even though it is displayed in the console, the transition from screen A to B to C to B to A will cause the method to it will not be called and will not be displayed.
ViewController.swift
// First view
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Home"
}
}
SecondViewController.swift
class SecondViewController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
navigationItem.title = "Second"
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let _ = viewController as? SecondViewController {
print("=====(open)willShow::SecondViewController=====")
} else {
print("=====(close)willShow::SecondViewController=====")
}
}
}
ThirdViewController.swift
class ThirdViewController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
navigationItem.title = "Third"
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if let _ = viewController as? ThirdViewController {
print("=====(open)willShow::ThirdViewController=====")
} else {
print("=====(close)willShow::ThirdViewController=====")
}
}
}
Logs of the transition from screen A to B to A
=====(open)willShow::SecondViewController=====
=====(close)willShow::SecondViewController=====
Logs of the transition from screen A to B to C to B to A
=====(open)willShow::SecondViewController=====
=====(open)willShow::ThirdViewController=====
=====(close)willShow::ThirdViewController=====
Logically, you should see this message at the end.
=====(close)willShow::SecondViewController=====
FirstViewController
SecondViewController
ThirdViewController
Related
I have been trying to refactor my source code so that it would conform to the Coordinator Pattern. I have used UITabBarController as the parent viewController of my app which contains 4 viewControllers.
I have been following the tutorials on how to implement the Coordinator pattern for iOS apps, and I have created and set up the protocols and classes of the Coordinator. I have a button inside my viewController (child viewController of the TabbarViewController), however, on button click, coordinator is not pushing / navigating to the desired VC, and I see the coordinator is returning nil on the debug console while debugging through the breakpoint, and I could not figure it out how to resolve this issue.
MainCoordinator.swift:
class MainCoordinator: SubCoordinator {
var subCoordinators = [SubCoordinator]()
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
func start() {
print("Initialized.. .")
UIApplication.app().window?.rootViewController = self.navigationController
let vc = SplashViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
// testing using a simple Viewcontroller class, its background color is set to red, so if the
// navigation works, a blank red VC should appear. but not working so far
func testView() {
let vc = ViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: false)
}
}
SubCoordinator.swift:
protocol SubCoordinator {
var subCoordinators: [SubCoordinator] { get set }
var navigationController: UINavigationController { get set }
func start()
}
StoryBoarded.swift:
protocol StoryBoarded {
static func instantiate() -> Self
}
// I am using storyBoard, and `instantiate()` should instantiate and return the specified VC
// from the Storyboard with the specified VC id (?)
extension StoryBoarded where Self: UIViewController {
static func instantiate() -> Self {
let id = String(describing: self)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
return storyboard.instantiateViewController(withIdentifier: id) as! Self
}
}
FirstViewController.Swift:
class FirstViewController: UIViewController, StoryBoarded {
#IBOutlet weak var button: UIButton!
var coordinator: MainCoordinator?
//MARK: - viewDidLoad()
override func viewDidLoad() {
super.viewDidLoad()
// If uncommented the below line, coordinator is not returning `nil`, but not navigating
anyways!
//coordinator = MainCoordinator(navigationController: UINavigationController())
}
#IBAction func onButtonTap(_ sender: Any) {
// So, basically I would expect the coordinator to navigate to the testView, but not
navigating
coordinator?.testView()
}
}
ViewController.swift:
// testView
class ViewController: UIViewController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.view.backgroundColor = .red
}
}
and
// TabbarController, set as the root VC after the splashVC is completed
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = UIStoryboard.firstViewController()
let secondVC = UIStoryboard.secondViewController()
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}
start() is being called, and splashVC appears and updates rootViewController with MainViewontroller on completion, But the navigation is not working at all on button click event.
Any feedback or help would highly be appreciated!
Since you're using the StoryBoarded protocol, you should follow the pattern and call instantiate() for initialization. Then, just set the coordinator.
class MainViewController: UITabBarController, StoryBoarded {
var coordinator: MainCoordinator?
override func viewDidLoad() {
super.viewDidLoad()
let firstVC = FirstViewController.instantiate()
let secondVC = SecondViewController.instantiate()
firstVC.coordinator = self.coordinator
secondVC.coordinator = self.coordinator
let views: [UIViewController] = [firstVC, secondVC]
self.setViewControllers(views, animated: false)
self.navigationController?.navigationBar.isHidden = false
}
}
I'm trying to make custom transitions when pushing/popping viewControllers from a custom UINavigationController class. I'm implementing the UINavigationControllerDelegate method
navigationController(_:animationControllerFor:from:to:), but it does not get called.
I'm creating a UINavigationController in storyboard and putting it's class as CustomNavigationController. I'm also assigning it a root ViewController in the storyboard (let's call the root VC CustomViewControllerRoot).
Here is the code I'm using (simplified and not tested):
protocol NavigationDelegate {
func pushCustomViewController()
func popViewController()
}
class CustomNavigationController: UINavigationController, NavigationDelegate {
init() {
super.init(nibName: nil, bundle: nil)
delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func viewDidLoad() {
self.navigationBar.isHidden = true
guard viewControllers.first is CustomViewControllerRoot else {fatalError("Error")}
rootVC = viewControllers.first as? CustomViewControllerRoot
rootVC?.navigationDelegate = self
//Setup the rest of the viewControllers that are to be used
customVC = CustomUIViewController()
customVC?.navigationDelegate = self
}
var rootVC: CustomViewControllerRoot?
var customVC: CustomViewController?
func pushCustomViewController() {
if customVC != nil {
self.pushViewController(customVC!, animated: true)
}
}
func popViewController() {
self.popViewController(animated: true)
}
}
extension CustomNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
// This is never called, even though the delegate is set in the initializer to CustomNavigationController
print("TEST")
return nil
}
}
I then let each custom UIViewController subclass in my navigation hierarchy delegate push or pops to this CustomNavigationController above. For example this is the root vc assigned to the navigation controller. Since it lies as root it never needs to push itself or be popped, as it is presented when the CustomNavigationController is presented. It delegates to CustomNavigationController when it finds that another VC should be presented on top of it:
class CustomViewControllerRoot {
var navigationDelegate: NavigationDelegate?
override func viewDidLoad(){
super.viewDidLoad()
}
#objc func someButtonPressedToPresentCustomVC(){
navigationDelegate?.pushCustomViewController()
}
}
The dismissal is handled inside each CustomViewController which also delegates the pop down to the CustomNavigationController (I don't want to use the navbar for dismissal so there is no "back button" from the start):
class CustomViewController: UIViewController {
var navigationDelegate: NavigationDelegate?
override func viewDidLoad(){
super.viewDidLoad()
}
#objc func dismissViewController(){
navigationDelegate?.popViewController()
}
}
To my understanding the UINavigationControllerDelegate method inside the extension of CustomNavigationController should be called whenever a push or pop is performed since I'm setting the delegate variable to self in the initializer?
Your navigation controller should have a root view controller.
And then you should push custom view controller from your root view controller. And delegate method calls
Navigation Controller Code:import UIKit
class CustomNV: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension CustomNV: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("TEST")
return nil
}
}
RootViewController code:
class RootViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let viewController = UIViewController(nibName: nil, bundle: nil)
viewController.view.backgroundColor = .green
navigationController?.pushViewController(viewController, animated: true)
}
}
Set root view controller as root for navigation controller in storyboard
According to this link , I wanted to pass some data from a B viewController to its parent, A viewController, on back press! here is my code :
in my B viewController, I've added this code -
extension Bcontroller: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
(viewController as? Acontroller)?.number = numberInB
(viewController as? Acontroller)?.myBoolean = boolInB
}
}
and here is my code in A controller :
override func viewWillAppear(_ animated: Bool) {
if number != -1 {
print(myBoolean)
}
}
when I open B controller, navigationController (willShow) is called, and when I press back button, viewWillAppear is called first, and then navigationController(willShow) in B controller is called! so my data is not set, and number will be always -1 . how can I set these variable?
Please find the below steps to implement delegate.
Step 1:- Initialise the protocol in view controller B.
ViewcontrollerB.m
protocol ViewControllerDelegate
{
func didUpdateViewController(_ number: NSNumber, myBoolean: Bool);
}
Step 2:-
initalise object inside viewcontrollerB
var delegate:ViewControllerDelegate?
Step 3:-
Now call this delegate from back function.Here I am considering back is the function to pop viewcontroller.
func Back()
{
delegate?.didUpdateViewController(numberInB!, myBoolean: boolInB!)
}
Step 4:-
Inherit the protocol in viewcontrollerA.
class ViewControllerA: UIViewController,ViewControllerDelegate
Step 5:-
Now Set the delegate in viewcontrollerA.
ViewcontrollerA.m
override func viewDidLoad() {
super.viewDidLoad()
let obj = ViewControllerB()//Initialize it as per your code
obj.delegate = self;
// Do any additional setup after loading the view, typically from a nib.
}
Final Step:-
override the delegate method.
func didUpdateViewController(_ number: NSNumber, myBoolean: Bool) {
print(number,myBoolean)
}
Let me know if it worked
Do call it in viewWillDisappear of controller B:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
let navigationController: UINavigationController = self.navigationController!
let controllers: [Acontroller] = navigationController.viewControllers.filter({ $0 is Acontroller }) as! [Acontroller]
if let viewController: Acontroller = controllers.first {
viewController.number = numberInB
viewController.myBoolean = boolInB
}
}
If the controller is in stack the values will be assigned to it.
If it is a small data then use UserDefaults:
This is how you store data:
UserDefaults.standard.set(1, forKey: "Key")
And this is how you get it back:
UserDefaults.standard.integer(forKey: "Key")
Or you can use a struct with a static variable:
struct ShareValue {
static var UserNum: Int = 0
}
Create a delegate which other view controllers can implement and call the delegate method in viewWillDisappear when isMovingFromParentViewController is true
public protocol YourViewControllerDelegate: class {
func didGoBack(viewController: YourViewController)
}
public class YourViewController: UIViewController {
public weak var delegate: YourViewControllerDelegate?
public override func viewWillDisappear(_ animated: Bool) {
if isMovingFromParentViewController {
delegate?.didGoBack(viewController: self)
}
}
}
In your other view controller:
extension YourOtherViewController: YourViewControllerDelegate {
public func didGoBack(viewController: YourViewController) {
// Do something e.g. set your variables
}
}
I am trying to call a custom method on my UITabBarController subclass from within one of the child view controllers. I have instantiated my CustomTabBarController class as the root view controller in AppDelegate.swift, however, the .tabBarController property on my child view controllers is of the class UITabBarController instead of CustomTabBarController.
Why does this happen? Is it possible to have the .tabBarController property on my view controllers reflect my subclass instead of the default UITabBarController class?
Here is my subclass:
import UIKit
class CustomTabBarController: UITabBarController, UITabBarControllerDelegate, LoginControllerDelegate {
let defaults = UserDefaults.standard
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
setupViews()
}
override func viewDidAppear(_ animated: Bool) {
checkLoginStatus()
}
func checkLoginStatus() {
if defaults.bool(forKey: "isLoggedIn") == false {
let loginController = LoginController()
loginController.delegate = self
present(loginController, animated: true, completion: nil)
}
}
func loginControllerDidDismiss() {
print("Delegation is working...")
}
func setupViews() {
let homeController = HomeController()
homeController.tabBarItem = CustomTabBarItem(title: "Home", imageNames: ["courthouse-icon-unselected", "courthouse-icon"])
let homeNavController = UINavigationController(rootViewController: homeController)
homeNavController.navigationBar.applyCustomStyle()
tabBar.tintColor = UIColor(red:0.18, green:0.34, blue:0.65, alpha:1.00)
self.setViewControllers([homeNavController], animated: true)
}
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
print(viewController.title)
return true
}
}
In my view controller, I would like to access this class like so:
import UIKit
class HomeController: ListController {
override func viewDidLoad() {
super.viewDidLoad()
self.title = "Home"
self.tabBarController??? // Right now this is a UITabBarController, but I would like to it be a CustomTabBarController
}
}
The best approach is to test whether it's what you believe it to be and cast it so that the compiler knows the correct class.
e.g.:
if let custom = self.tabBarController as? CustomTabBarController {
custom.checkLoginStatus()
} else {
print("Unexpected controller \(self.tabBarController)")
}
class ViewController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
navigationController!.delegate = self
}
func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
print("showViewController")
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
print("sss")
}
func update() {
let vc = SecondViewController()
navigationController!.pushViewController(vc, animated: true)
}
}
this is the first controller of my demo , and in console:
sss
showViewController
showViewController
the "didShowViewController" was called twice.
I'm not sure what's going on
-----------------some test----------------------
I add some log in these method of controller : loadView,viewDidLoad ,viewWillAppear,viewDidAppear , and the order of these log is:
loadView
viewDidLoad
viewWillAppear
will:<NaviDemo.ViewController: 0x7fe8c9533050>
<NaviDemo.ViewController: 0x7fe8c9533050>
viewDidAppear
<NaviDemo.ViewController: 0x7fe8c9533050>
I hit the same issue in my code. I was able to work around it by waiting until viewDidAppear to set the navigation delegate instead of setting it in viewDidLoad. To translate it to your example:
override func viewDidLoad() {
super.viewDidLoad()
}
// ...
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController!.delegate = self
}
didShowViewController is called twice because the first time it is called when the navigation controller transitions to showing the view controller. And then it is called again by the navigation controller's own viewDidAppear when it appears on screen, using the topViewController as the controller param which in this case is the same as the controller the first time it was called.
The UINavigationController has displayed two instances of a UIViewController
From the UINavigationControllerDelegate documentation
Called just after the navigation controller displays a view
controller’s view and navigation item properties.
Instead of logging "showViewController", log the UIViewController instance to see what's going on
func navigationController(navigationController: UINavigationController, didShowViewController viewController: UIViewController, animated: Bool) {
print(viewController)
}