I want to know that when user navigates to another screen. If there is any change in navigation stack, It should notify me. To do that, I do not want to write any code in viewwillappear of any viewcontroller. I wish to write it once and thus I can observe that which screen is navigated.
Consider writing a super UIViewController which all your UIViewControllers inherit from. Then you just write the code in your super ViewController
Sample code for #Nikolaj Nielsen answer:
class BaseViewController: UIViewController {
var identifier: String {
return ""
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Appeared VC: \(identifier)")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("Disappeared VC: \(identifier)")
}
}
class CustomViewController: BaseViewController {
override var identifier: String {
return "CustomViewController"
}
}
Related
I have several views in my app, and I only want a navigationbar on one of them.... I used a navigationcontroller and at first I was using this code (while my app was in its infancy and only had 2 views)
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(true, animated: animated)
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: animated)
super.viewWillDisappear(animated)
}
It worked fine - however, the app has become more complex - I have these views
lazy var orderedViewControllers: [UIViewController] = {
return [self.newVc(viewController: "pageOne"),
self.newVc(viewController: "pageTwo"),
self.newVc(viewController: "pageThree"),
self.newVc(viewController: "pageFour"),
self.newVc(viewController: "activate")
]
}()
Where this code isn't applied to, even if I create a custom view controller for each view.
I thought the way to do this would be to put the top chunk of code in every view, but it's not working for the bottom chunk. In essence my question is how do I use NavigationController to create a bar ONLY on one view.
One option: use a "base view controller" class which handles hiding / showing the Navigation Bar, and make your "pages" sub-classes of the "base" class.
import UIKit
class BaseViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
}
class ViewController: UIViewController {
// has buttons with
// Show (e.g. push)
// segues to Settings, First, Second, Third view controllers
}
class SettingsViewController: UIViewController {
// Settings VC is a normal UIViewController, because
// we *want* the NavBar to remain visible
}
class FirstViewController: BaseViewController {
#IBAction func backTapped(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
}
class SecondViewController: BaseViewController {
#IBAction func backTapped(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
}
class ThirdViewController: BaseViewController {
#IBAction func backTapped(_ sender: Any) {
self.navigationController?.popViewController(animated: true)
}
}
You can use this method of UINavigationControllerDelegate
optional func navigationController(_ navigationController: UINavigationController,
willShow viewController: UIViewController,
animated: Bool){
if viewController == self."desired view controller" {
self.isNavigationBarHidden = true
}else{
self.isNavigationBarHidden = false
}
}
Thank you all for the support. I have resolved my issue by doing the following:
I put the only view controller I wanted to have a navigation bar in a navigation controller view the Embed menu.
I added a custom back button.
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 have 2 controllers inside NavigationController. First pushes the second one to the stack and user can interact with the text field there. Then (in one scenario) user will tap on back button to be taken to the previous screen. Assuming that loading of second one is 'heavy', so I will be keeping only one instance of it once it is needed.
Expected:
I would like to have keyboard hidden once back button is pressed.
Actual:
First responder keeps being restored when I go back to the second for the second time. How to prevent that? Resigning first responder also doesn't do the trick there...
Problem demo:
https://gitlab.com/matrejek/TestApp
Major code parts:
class FirstViewController: UIViewController {
var child: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "child")
return vc
}()
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func onButtonTap(_ sender: Any) {
self.navigationController?.pushViewController(child, animated: true)
}
}
class SecondViewController: UIViewController {
#IBOutlet weak var textField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
view.endEditing(true)
}
}
This does seem odd --- and it seems like your approach should work.
Apparently (based on quick testing), since you are not allowing the Navigation Controller to release the SecondVC, the text field is remaining "active."
If you add this to SecondViewController, it will prevent the keyboard from "auto re-showing" the next time you navigate to the controller - not sure it will be suitable for you, but it will do the job:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
DispatchQueue.main.async {
self.view.endEditing(true)
}
}
Edit: Jan 25 2020
Based on new comments, yes, this seems to be a bug.
My previous work-around answer worked -- sort of. The result was the keyboard popping up and then disappearing on subsequent pushes of child.
Following is a better work-around. We have SecondViewController conform to UITextFieldDelegate and add a BOOL class var / property that will prevent the text field from becoming first responder. Comments should be clear:
class FirstViewController: UIViewController {
var child: UIViewController = {
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
let vc = storyboard.instantiateViewController(withIdentifier: "child")
return vc
}()
#IBAction func onButtonTap(_ sender: Any) {
self.navigationController?.pushViewController(child, animated: true)
}
}
// conform to UITextFieldDelegate
class SecondViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var textField: UITextField!
// bool var to prevent text field re-becoming first responder
// when VC is pushed a second time
var bResist: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// assign text field delegate
textField.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// view has appeared, so allow text field to become first responder
bResist = false
}
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
return !bResist
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// end editing on this view
view.endEditing(true)
// we want to resist becoming first responder on next push
bResist = true
}
}
I have two UIViewController, when I click a button, it goes from the first view controller to the second one. And before that, I animated a UIView to move to another place. After dismissing the second View Controller, I want to move the UIView in the first view controller back to where it originally was. However, when I call a function from the second View Controller to animate the UIview in the first view controller after dismissing the second one, It could not get the UIView's properties, and cannot do anything with it. I think because the first UIViewController is not loaded yet. Is that the problem? And How should I solve this?
There are two solutions you can either use swift closures
class FirstViewController: UIViewController {
#IBAction func start(_ sender: Any) {
guard let secondController = self.storyboard?.instantiateViewController(withIdentifier: "SecondController") as? SecondController else { return }
secondController.callbackClosure = { [weak self] in
print("Do your stuff")
}
self.navigationController?.pushViewController(secondController, animated: true)
}
}
//----------------------------
class SecondController: UIViewController {
var callbackClosure: ((Void) -> Void)?
override func viewWillDisappear(_ animated: Bool) {
callbackClosure?()
}
}
or you can use protocols
class FirstViewController: UIViewController {
#IBAction func start(_ sender: Any) {
guard let secondController = self.storyboard?.instantiateViewController(withIdentifier: "SecondController") as? SecondController else { return }
secondController.delegate = self
self.navigationController?.pushViewController(secondController, animated: true)
}
}
extension ViewController : ViewControllerSecDelegate {
func didBackButtonPressed(){
print("Do your stuff")
}
}
//--------------------------
protocol SecondControllerDelegate : NSObjectProtocol {
func didBackButtonPressed()
}
class SecondController: UIViewController {
weak var delegate: SecondControllerDelegate?
override func viewWillDisappear(_ animated: Bool) {
delegate?.didBackButtonPressed()
}
}
You can try to use a closure. Something like this:
class FirstViewController: UIViewController {
#IBOutlet weak var nextControllerButton: UIButton!
private let animatableView: UIView = UIView()
private func methodsForSomeAnimation() {
/*
perform some animation with 'animatableView'
*/
}
#IBAction func nextControllerButtonAction() {
// you can choose any other way to initialize controller :)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let secondController = storyboard.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController else { return }
secondController.callbackClosure = { [weak self] in
self?.methodsForSomeAnimation()
}
present(secondController, animated: true, completion: nil)
}
}
class SecondViewController: UIViewController {
#IBOutlet weak var dismissButton: UIButton!
var callbackClosure: ((Void) -> Void)?
#IBAction func dismissButtonAction() {
callbackClosure?()
dismiss(animated: true, completion: nil)
/*
or you call 'callbackClosure' in dismiss completion
dismiss(animated: true) { [weak self] in
self?.callbackClosure?()
}
*/
}
}
When you present your second view controller you can pass an instance of the first view controller.
The second VC could hold an instance of the first VC like such:
weak var firstViewController: NameOfController?
then when your presenting the second VC make sure you set the value so it's not nil like so:
firstViewController = self
After you've done this you'll be able to access that viewControllers functions.
iOS 11.x Swift 4.0
In calling VC you put this code ...
private struct Constants {
static let ScannerViewController = "Scan VC"
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == Constants.ScannerViewController {
let svc = destination as? ScannerViewController
svc?.firstViewController = self
}
}
Where you have named the segue in my case "Scan VC", this is what it looks like in Xcode panel.
Now in scan VC we got this just under the class declaration
weak var firstViewController: HiddingViewController?
Now later in your code, when your ready to return I simply set my concerned variables in my firstViewController like this ...
self.firstViewController?.globalUUID = code
Which I have setup in the HiddingViewController like this ...
var globalUUID: String? {
didSet {
startScanning()
}
}
So basically when I close the scanning VC I set the variable globalUUID which in term starts the scanning method here.
When you are saying it could not get the UIView's properties it's because you put it as private ? Why you don't replace your UIView in the first controller when it disappears before to go to your secondViewController. I think it's a case where you have to clean up your view controller state before to go further to your second view controller.
Check IOS lifecycle methods : viewWillDisappear or viewDidDisappear through Apple documentation and just do your animation in one of these methods.
Very simple solution actually... Just put your animation in the viewDidAppear method. This method is called every time the view loads.
class firstViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// insert animation here to run when FirstViewController appears...
}
}
I'd like to have a close button on each view controller that appears in the navigation stack. I've read here that I need to create an object that is a uinavigationdelegate, I think this object will have a method like didTapCloseButton?
Questions:
Should I create a protocol and make everything confirm to it, i.e.:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
public class ViewController: CustomDelegate {
func didTapCloseButton() {
//not sure what goes in here?
}
}
How do I get the close button to show on the navigation bars of every view?
When the user clicks the close button, how do I get that to dismiss every view on that stack?
Thanks for your help!
Here a simple solution. Create UINavigationController subclass and override pushViewController method.
class NavigationController: UINavigationController {
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
let closeBarButtonItem = UIBarButtonItem(
title: "Close",
style: .done,
target: self,
action: #selector(self.popViewController(animated:)))
viewController.navigationItem.rightBarButtonItem = closeBarButtonItem
}
}
Not sure if this is what you intended but you can do:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
extension CustomDelegate where Self : UIViewController{
func didTapCloseButton(){
// write your default implementation for all classes
}
}
now for every UIViewController class you have you can just do :
class someViewController: CustomDelegate{
#IBAction buttonClicked (sender: UIButton){
didTapCloseButton()
}
}