264

I need to execute an action (emptying an array), when the back button of a UINavigationController is pressed, while the button still causes the previous ViewController on the stack to appear. How could I accomplish this using swift? enter image description here

StevenZ
  • 6,983
  • 5
  • 16
  • 18

32 Answers32

562

Replacing the button to a custom one as suggested on another answer is possibly not a great idea as you will lose the default behavior and style.

One other option you have is to implement the viewWillDisappear method on the View Controller and check for a property named isMovingFromParentViewController. If that property is true, it means the View Controller is disappearing because it's being removed (popped).

Should look something like:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if self.isMovingFromParentViewController {
        // Your code...
    }
}

In swift 4.2

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if self.isMovingFromParent {
        // Your code...
    }
}
Yannick Loriot
  • 7,107
  • 2
  • 33
  • 56
manecosta
  • 8,682
  • 3
  • 26
  • 39
  • 7
    @gmogames yes, you cannot do that. The question didn't ask for that though. To be able to stop the action of going back I guess you really need to override the button. – manecosta May 08 '17 at 14:15
  • 13
    For **Swift 3.1**: `override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if isMovingFromParentViewController { // Your code... } }` – Doug Amos May 08 '17 at 14:58
  • 34
    ```viewWillDisappear(animated:)``` will get triggered if you get a phone call. This is likely not what you want. Probably better to use ```willMove(toParentViewController:)``` – Joe Susnick Nov 29 '17 at 20:46
  • in Swift 4, missing _: override func viewWillDisappear(_ animated : Bool) – Javier Calatrava Llavería Apr 24 '18 at 09:16
  • 1
    This should be the accepted answer. Clean and simple. – temp Apr 21 '19 at 22:16
  • 2
    No this is totally wrong This method is called when back button is press and we pop to ViewController then this method is called question is to how to do action in NavigationItem BackButton Click. This is like completion and we can do this thing in ViewWillDisappear. I hope you got what I am telling you. is there is any way to do button click then please give me solution. Thank You – Yogesh Patel Jul 01 '19 at 11:52
  • 2
    unfortunately this also gets called when going to any moving to any new view controller... but I need to only capture the back button and nothing else – Quinn Oct 22 '19 at 14:17
  • 2
    How has this got so much upvotes? question is rather than default behaviour need to detect when back button is tapped & do action... self.isMovingFromParent will always return true if user has navigated in this page.. – jayant rawat Mar 03 '22 at 13:47
  • Best answer, you take advantage of lifecycle view controller. – Salvatore.isc May 24 '23 at 17:36
184

One option would be implementing your own custom back button. You would need to add the following code to your viewDidLoad method:

    - (void) viewDidLoad {
        [super viewDidLoad];
        self.navigationItem.hidesBackButton = YES;
        UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle:@"Back" style:UIBarButtonItemStyleBordered target:self action:@selector(back:)];
        self.navigationItem.leftBarButtonItem = newBackButton;
    }

    - (void) back:(UIBarButtonItem *)sender {
        // Perform your custom actions
        // ...
        // Go back to the previous ViewController
        [self.navigationController popViewControllerAnimated:YES];
    }

UPDATE:

Here is the version for Swift:

        override func viewDidLoad {
            super.viewDidLoad()
            self.navigationItem.hidesBackButton = true
            let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Bordered, target: self, action: "back:")
            self.navigationItem.leftBarButtonItem = newBackButton
        }

       @objc func back(sender: UIBarButtonItem) {
            // Perform your custom actions
            // ...
            // Go back to the previous ViewController
            self.navigationController?.popViewControllerAnimated(true)
        }

UPDATE 2:

Here is the version for Swift 3:

        override func viewDidLoad {
            super.viewDidLoad()
            self.navigationItem.hidesBackButton = true
            let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(YourViewController.back(sender:)))
            self.navigationItem.leftBarButtonItem = newBackButton
        }

       @objc func back(sender: UIBarButtonItem) {
            // Perform your custom actions
            // ...
            // Go back to the previous ViewController
            _ = navigationController?.popViewController(animated: true)
        }
Rashid Latif
  • 2,809
  • 22
  • 26
fr33g
  • 2,249
  • 1
  • 12
  • 12
  • 2
    This doesn't pop to the previous view controller; it pops to the root view controller. – rocky Dec 30 '14 at 23:03
  • 118
    How can i have an arrow like ordinary back button? – TomSawyer Jan 27 '16 at 08:03
  • @rocky You may try the below line in back function: [self.navigationController dismissViewControllerAnimated:YES completion:nil]; – malajisi Jul 21 '16 at 16:32
  • 2
    @TomSawyer For that, please have a look at the answer below – fr33g Nov 11 '16 at 09:17
  • 7
    Doing a substitution of a system button to override a feature is not a good way. The best way is the answer below! http://stackoverflow.com/a/27715660/2307276 – dpizzuto Feb 01 '17 at 13:00
  • @TomSawyer I ended up using creating a new UIButton, setting the title with setTitle and then setting the image with setImage. You can see this in use with a function at the link below. Line 42. https://github.com/andrewlundy/ios-data-passing-tutorial/blob/e69cbe6ca527cd9af2bf9919a9a00cc36f9534eb/Data-Passing-Tutorial/Controller/ContactDetailsVC.swift#L42 – andrewlundy Apr 20 '19 at 03:08
  • 1
    it's work but we will lost swipe back action – famfamfam May 14 '21 at 09:05
77
override func willMove(toParent parent: UIViewController?)
{
    super.willMove(toParent: parent)
    if parent == nil
    {
        print("This VC is 'will' be popped. i.e. the back button was pressed.")
    }
}
beryllium
  • 29,669
  • 15
  • 106
  • 125
Iya
  • 1,888
  • 19
  • 12
43

I was able to achieve this with the following :

Swift 3

override func didMoveToParentViewController(parent: UIViewController?) {
   super.didMoveToParentViewController(parent)

   if parent == nil {
      println("Back Button pressed.")
      delegate?.goingBack()
   }           
}

Swift 4

override func didMove(toParent parent: UIViewController?) {
    super.didMove(toParent: parent)

    if parent == nil {
        debugPrint("Back Button pressed.")
    }
}

No need of custom back button.

Vinoth Vino
  • 9,166
  • 3
  • 66
  • 70
Siddharth Bhatt
  • 613
  • 6
  • 7
  • 1
    This is fantastics. Old remark but still works with the latest Swift. – user3204765 Aug 09 '18 at 03:19
  • This is triggered (false positive) also when unwinding from next view controller (over this one) so not really back button press detection. – user2878850 Oct 03 '19 at 11:01
  • Same remark as previous person, this code does not detect the activation of the back button, but the pop of the current view. – Vilmir Apr 21 '20 at 21:51
36

If you want to have back button with back arrow you can use an image and code below

backArrow.png arrow1 backArrow@2x.png arrow2 backArrow@3x.png arrow3

override func viewDidLoad() {
    super.viewDidLoad()
    let customBackButton = UIBarButtonItem(image: UIImage(named: "backArrow") , style: .plain, target: self, action: #selector(backAction(sender:)))
    customBackButton.imageInsets = UIEdgeInsets(top: 2, left: -8, bottom: 0, right: 0)
    navigationItem.leftBarButtonItem = customBackButton
}

func backAction(sender: UIBarButtonItem) {
    // custom actions here
    navigationController?.popViewController(animated: true)
}
Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
  • This is still the most practical solution in 2022. – zeeshan Aug 30 '22 at 12:18
  • For iOS16, SF symbol "chevron.backward" matchs the default image even more. `let image = UIImage(systemName: "chevron.backward", withConfiguration: UIImage.SymbolConfiguration(weight: .semibold))` – Sam Oct 29 '22 at 06:10
31

I created this (swift) class to create a back button exactly like the regular one, including back arrow. It can create a button with regular text or with an image.

Usage

weak var weakSelf = self

// Assign back button with back arrow and text (exactly like default back button)
navigationItem.leftBarButtonItems = CustomBackButton.createWithText("YourBackButtonTitle", color: UIColor.yourColor(), target: weakSelf, action: #selector(YourViewController.tappedBackButton))

// Assign back button with back arrow and image
navigationItem.leftBarButtonItems = CustomBackButton.createWithImage(UIImage(named: "yourImageName")!, color: UIColor.yourColor(), target: weakSelf, action: #selector(YourViewController.tappedBackButton))

func tappedBackButton() {

    // Do your thing

    self.navigationController!.popViewControllerAnimated(true)
}

CustomBackButtonClass

(code for drawing the back arrow created with Sketch & Paintcode plugin)

class CustomBackButton: NSObject {

    class func createWithText(text: String, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
        let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
        negativeSpacer.width = -8
        let backArrowImage = imageOfBackArrow(color: color)
        let backArrowButton = UIBarButtonItem(image: backArrowImage, style: UIBarButtonItemStyle.Plain, target: target, action: action)
        let backTextButton = UIBarButtonItem(title: text, style: UIBarButtonItemStyle.Plain , target: target, action: action)
        backTextButton.setTitlePositionAdjustment(UIOffset(horizontal: -12.0, vertical: 0.0), forBarMetrics: UIBarMetrics.Default)
        return [negativeSpacer, backArrowButton, backTextButton]
    }

    class func createWithImage(image: UIImage, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
        // recommended maximum image height 22 points (i.e. 22 @1x, 44 @2x, 66 @3x)
        let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.FixedSpace, target: nil, action: nil)
        negativeSpacer.width = -8
        let backArrowImageView = UIImageView(image: imageOfBackArrow(color: color))
        let backImageView = UIImageView(image: image)
        let customBarButton = UIButton(frame: CGRectMake(0,0,22 + backImageView.frame.width,22))
        backImageView.frame = CGRectMake(22, 0, backImageView.frame.width, backImageView.frame.height)
        customBarButton.addSubview(backArrowImageView)
        customBarButton.addSubview(backImageView)
        customBarButton.addTarget(target, action: action, forControlEvents: .TouchUpInside)
        return [negativeSpacer, UIBarButtonItem(customView: customBarButton)]
    }

    private class func drawBackArrow(frame frame: CGRect = CGRect(x: 0, y: 0, width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) {
        /// General Declarations
        let context = UIGraphicsGetCurrentContext()!

        /// Resize To Frame
        CGContextSaveGState(context)
        let resizedFrame = resizing.apply(rect: CGRect(x: 0, y: 0, width: 14, height: 22), target: frame)
        CGContextTranslateCTM(context, resizedFrame.minX, resizedFrame.minY)
        let resizedScale = CGSize(width: resizedFrame.width / 14, height: resizedFrame.height / 22)
        CGContextScaleCTM(context, resizedScale.width, resizedScale.height)

        /// Line
        let line = UIBezierPath()
        line.moveToPoint(CGPoint(x: 9, y: 9))
        line.addLineToPoint(CGPoint.zero)
        CGContextSaveGState(context)
        CGContextTranslateCTM(context, 3, 11)
        line.lineCapStyle = .Square
        line.lineWidth = 3
        color.setStroke()
        line.stroke()
        CGContextRestoreGState(context)

        /// Line Copy
        let lineCopy = UIBezierPath()
        lineCopy.moveToPoint(CGPoint(x: 9, y: 0))
        lineCopy.addLineToPoint(CGPoint(x: 0, y: 9))
        CGContextSaveGState(context)
        CGContextTranslateCTM(context, 3, 2)
        lineCopy.lineCapStyle = .Square
        lineCopy.lineWidth = 3
        color.setStroke()
        lineCopy.stroke()
        CGContextRestoreGState(context)

        CGContextRestoreGState(context)
    }

    private class func imageOfBackArrow(size size: CGSize = CGSize(width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) -> UIImage {
        var image: UIImage

        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        drawBackArrow(frame: CGRect(origin: CGPoint.zero, size: size), color: color, resizing: resizing)
        image = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()

        return image
    }

    private enum ResizingBehavior {
        case AspectFit /// The content is proportionally resized to fit into the target rectangle.
        case AspectFill /// The content is proportionally resized to completely fill the target rectangle.
        case Stretch /// The content is stretched to match the entire target rectangle.
        case Center /// The content is centered in the target rectangle, but it is NOT resized.

        func apply(rect rect: CGRect, target: CGRect) -> CGRect {
            if rect == target || target == CGRect.zero {
                return rect
            }

            var scales = CGSize.zero
            scales.width = abs(target.width / rect.width)
            scales.height = abs(target.height / rect.height)

            switch self {
                case .AspectFit:
                    scales.width = min(scales.width, scales.height)
                    scales.height = scales.width
                case .AspectFill:
                    scales.width = max(scales.width, scales.height)
                    scales.height = scales.width
                case .Stretch:
                    break
                case .Center:
                    scales.width = 1
                    scales.height = 1
            }

            var result = rect.standardized
            result.size.width *= scales.width
            result.size.height *= scales.height
            result.origin.x = target.minX + (target.width - result.width) / 2
            result.origin.y = target.minY + (target.height - result.height) / 2
            return result
        }
    }
}

SWIFT 3.0

class CustomBackButton: NSObject {

    class func createWithText(text: String, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
        let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.fixedSpace, target: nil, action: nil)
        negativeSpacer.width = -8
        let backArrowImage = imageOfBackArrow(color: color)
        let backArrowButton = UIBarButtonItem(image: backArrowImage, style: UIBarButtonItemStyle.plain, target: target, action: action)
        let backTextButton = UIBarButtonItem(title: text, style: UIBarButtonItemStyle.plain , target: target, action: action)
        backTextButton.setTitlePositionAdjustment(UIOffset(horizontal: -12.0, vertical: 0.0), for: UIBarMetrics.default)
        return [negativeSpacer, backArrowButton, backTextButton]
    }

    class func createWithImage(image: UIImage, color: UIColor, target: AnyObject?, action: Selector) -> [UIBarButtonItem] {
        // recommended maximum image height 22 points (i.e. 22 @1x, 44 @2x, 66 @3x)
        let negativeSpacer = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.fixedSpace, target: nil, action: nil)
        negativeSpacer.width = -8
        let backArrowImageView = UIImageView(image: imageOfBackArrow(color: color))
        let backImageView = UIImageView(image: image)
        let customBarButton = UIButton(frame: CGRect(x: 0, y: 0, width: 22 + backImageView.frame.width, height: 22))
        backImageView.frame = CGRect(x: 22, y: 0, width: backImageView.frame.width, height: backImageView.frame.height)
        customBarButton.addSubview(backArrowImageView)
        customBarButton.addSubview(backImageView)
        customBarButton.addTarget(target, action: action, for: .touchUpInside)
        return [negativeSpacer, UIBarButtonItem(customView: customBarButton)]
    }

    private class func drawBackArrow(_ frame: CGRect = CGRect(x: 0, y: 0, width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) {
        /// General Declarations
        let context = UIGraphicsGetCurrentContext()!

        /// Resize To Frame
        context.saveGState()
        let resizedFrame = resizing.apply(CGRect(x: 0, y: 0, width: 14, height: 22), target: frame)
        context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY)
        let resizedScale = CGSize(width: resizedFrame.width / 14, height: resizedFrame.height / 22)
        context.scaleBy(x: resizedScale.width, y: resizedScale.height)

        /// Line
        let line = UIBezierPath()
        line.move(to: CGPoint(x: 9, y: 9))
        line.addLine(to: CGPoint.zero)
        context.saveGState()
        context.translateBy(x: 3, y: 11)
        line.lineCapStyle = .square
        line.lineWidth = 3
        color.setStroke()
        line.stroke()
        context.restoreGState()

        /// Line Copy
        let lineCopy = UIBezierPath()
        lineCopy.move(to: CGPoint(x: 9, y: 0))
        lineCopy.addLine(to: CGPoint(x: 0, y: 9))
        context.saveGState()
        context.translateBy(x: 3, y: 2)
        lineCopy.lineCapStyle = .square
        lineCopy.lineWidth = 3
        color.setStroke()
        lineCopy.stroke()
        context.restoreGState()

        context.restoreGState()
    }

    private class func imageOfBackArrow(_ size: CGSize = CGSize(width: 14, height: 22), color: UIColor = UIColor(hue: 0.59, saturation: 0.674, brightness: 0.886, alpha: 1), resizing: ResizingBehavior = .AspectFit) -> UIImage {
        var image: UIImage

        UIGraphicsBeginImageContextWithOptions(size, false, 0)
        drawBackArrow(CGRect(origin: CGPoint.zero, size: size), color: color, resizing: resizing)
        image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        return image
    }

    private enum ResizingBehavior {
        case AspectFit /// The content is proportionally resized to fit into the target rectangle.
        case AspectFill /// The content is proportionally resized to completely fill the target rectangle.
        case Stretch /// The content is stretched to match the entire target rectangle.
        case Center /// The content is centered in the target rectangle, but it is NOT resized.

        func apply(_ rect: CGRect, target: CGRect) -> CGRect {
            if rect == target || target == CGRect.zero {
                return rect
            }

            var scales = CGSize.zero
            scales.width = abs(target.width / rect.width)
            scales.height = abs(target.height / rect.height)

            switch self {
            case .AspectFit:
                scales.width = min(scales.width, scales.height)
                scales.height = scales.width
            case .AspectFill:
                scales.width = max(scales.width, scales.height)
                scales.height = scales.width
            case .Stretch:
                break
            case .Center:
                scales.width = 1
                scales.height = 1
            }

            var result = rect.standardized
            result.size.width *= scales.width
            result.size.height *= scales.height
            result.origin.x = target.minX + (target.width - result.width) / 2
            result.origin.y = target.minY + (target.height - result.height) / 2
            return result
        }
    }
}
Tulleb
  • 8,919
  • 8
  • 27
  • 55
guido
  • 2,792
  • 1
  • 21
  • 40
  • Would you be so kind to update your answer for iOS 11? – BR41N-FCK Jan 12 '18 at 21:21
  • 3
    Hi @guido, your solution is perfect, I tried your code and noticed that there is space in front of back button even though you added barbutton with negative width. – Pawriwes Aug 16 '18 at 03:48
22

In Swift 5 and Xcode 10.2

Please don't add custom bar button item, use this default behaviour.

No need of viewWillDisappear, no need of custom BarButtonItem etc...

It's better to detect when the VC is removed from it's parent.

Use any one of these two functions

override func willMove(toParent parent: UIViewController?) {
    super.willMove(toParent: parent)
    if parent == nil {
        callStatusDelegate?.backButtonClicked()//Here write your code
    }
}

override func didMove(toParent parent: UIViewController?) {
    super.didMove(toParent: parent)
    if parent == nil {
        callStatusDelegate?.backButtonClicked()//Here write your code
    }
}

If you want stop default behaviour of back button then add custom BarButtonItem.

Naresh
  • 16,698
  • 6
  • 112
  • 113
  • 6
    Note that this is also called when you pop programmatically, not only press back button. – User May 28 '20 at 18:16
13

If you are using navigationController then add the UINavigationControllerDelegate protocol to class and add the delegate method as follows:

class ViewController:UINavigationControllerDelegate {

    func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController,
animated: Bool) {
        if viewController === self {
            // do here what you want
        }
    }
}

This method is called whenever the navigation controller will slide to a new screen. If the back button was pressed, the new view controller is ViewController itself.

top.dev
  • 447
  • 1
  • 6
  • 18
Ajinkya Patil
  • 5,518
  • 4
  • 20
  • 22
9

You can subclass UINavigationController and override popViewController(animated: Bool). Beside being able to execute some code there you can also prevent the user from going back altogether, for instance to prompt to save or discard his current work.

Sample implementation where you can set a popHandler that gets set/cleared by pushed controllers.

class NavigationController: UINavigationController
{
    var popHandler: (() -> Bool)?

    override func popViewController(animated: Bool) -> UIViewController?
    {
        guard self.popHandler?() != false else
        {
            return nil
        }
        self.popHandler = nil
        return super.popViewController(animated: animated)
    }
}

And sample usage from a pushed controller that tracks unsaved work.

let hasUnsavedWork: Bool = // ...
(self.navigationController as! NavigationController).popHandler = hasUnsavedWork ?
    {
        // Prompt saving work here with an alert

        return false // Prevent pop until as user choses to save or discard

    } : nil // No unsaved work, we clear popHandler to let it pop normally

As a nice touch, this will also get called by interactivePopGestureRecognizer when the user tries to go back using a swipe gesture.

Rivera
  • 10,792
  • 3
  • 58
  • 102
7

NO

override func willMove(toParentViewController parent: UIViewController?) { }

This will get called even if you are segueing to the view controller in which you are overriding this method. In which check if the "parent" is nil of not is not a precise way to be sure of moving back to the correct UIViewController. To determine exactly if the UINavigationController is properly navigating back to the UIViewController that presented this current one, you will need to conform to the UINavigationControllerDelegate protocol.

YES

note: MyViewController is just the name of whatever UIViewController you want to detect going back from.

1) At the top of your file add UINavigationControllerDelegate.

class MyViewController: UIViewController, UINavigationControllerDelegate {

2) Add a property to your class that will keep track of the UIViewController that you are segueing from.

class MyViewController: UIViewController, UINavigationControllerDelegate {

var previousViewController:UIViewController

3) in MyViewController's viewDidLoad method assign self as the delegate for your UINavigationController.

override func viewDidLoad() {
    super.viewDidLoad()
    self.navigationController?.delegate = self
}

3) Before you segue, assign the previous UIViewController as this property.

// In previous UIViewController
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "YourSegueID" {
        if let nextViewController = segue.destination as? MyViewController {
            nextViewController.previousViewController = self
        }
    }
}

4) And conform to one method in MyViewController of the UINavigationControllerDelegate

func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
    if viewController == self.previousViewController {
        // You are going back
    }
}
Brandon A
  • 8,153
  • 3
  • 42
  • 77
  • 1
    Thanks for the helpful answer! Readers beware of setting the UINavigationController's delegate to a specific view controller; if the navigation controller already has a delegate, you run the risk of depriving that other delegate of callbacks it expects. In our app, the UINavigationController's delegate is a shared object (an AppCoordinator) that all view controllers have a pointer to. – Bill Feth Aug 23 '18 at 17:38
7

In my case the viewWillDisappear worked best. But in some cases one has to modify the previous view controller. So here is my solution with access to the previous view controller and it works in Swift 4:

override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        if isMovingFromParentViewController {
            if let viewControllers = self.navigationController?.viewControllers {
                if (viewControllers.count >= 1) {
                    let previousViewController = viewControllers[viewControllers.count-1] as! NameOfDestinationViewController
                    // whatever you want to do
                    previousViewController.callOrModifySomething()
                }
            }
        }
    }
Bernd
  • 169
  • 2
  • 9
  • -viewDidDisappear (or -viewWillDisappear) will be called even if the view is being covered by another view controller's view (not just when the – Bill Feth Aug 23 '18 at 17:30
7

Before leave current controller I need to show alert. So I did it this way:

  1. Add extention to UINavigationController with UINavigationBarDelegate
  2. Add selector to your controller navigationShouldPopOnBack(completion:)

It's worked)

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        if let items = navigationBar.items, viewControllers.count < items.count {
            return true
        }

        let clientInfoVC = topViewController as? ClientInfoVC
        if clientInfoVC?.responds(to: #selector(clientInfoVC?.navigationShouldPopOnBack)) ?? false {
            clientInfoVC?.navigationShouldPopOnBack(completion: { isAllowPop in
                if isAllowPop {
                    DispatchQueue.main.async {
                        self.popViewController(animated: true)
                    }
                }
            })
        }

        DispatchQueue.main.async {
            self.popViewController(animated: true)
        }

        return false
    }
}

@objc func navigationShouldPopOnBack(completion: @escaping (Bool) -> ()) {
        let ok = UIAlertAction(title: R.string.alert.actionOk(), style: .default) { _ in
            completion(true)
        }
        let cancel = UIAlertAction(title: R.string.alert.actionCancel(), style: .cancel) { _ in
            completion(false)
        }
        let alertController = UIAlertController(title: "", message: R.string.alert.contractMessage(), preferredStyle: .alert)
        alertController.addAction(ok)
        alertController.addAction(cancel)
        present(alertController, animated: true, completion: nil)
    }
MGY
  • 7,245
  • 5
  • 41
  • 74
Taras
  • 1,485
  • 1
  • 16
  • 31
  • Hi @mgyky/@Taras, any clue why the navigationShouldPopOnBack is no more called from my side? Does iOS12 could be the issue? – David Nov 20 '20 at 18:55
  • Hi @David. In my side everything is ok in iOS 12. Can you send your code? – Taras Nov 23 '20 at 08:56
  • Hi @Taras, any reason why you did not move to iOS 13 or 14 yet? Maybe your device can't? If you can, please try to use the latest iOS to see the behavior. My original code was this https://gist.github.com/HamGuy/a099058e674b573ffe433132f7b5651e and I also tried this https://www.programmersought.com/article/1529883806/ and – David Nov 23 '20 at 14:43
7

When back button is pressed, ignore interactive pop with screen edge gesture.

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    
    if isMovingFromParent, transitionCoordinator?.isInteractive == false {
      // code here
    }
  }
6

It's not difficult as we thing. Just create a frame for UIButton with clear background color, assign action for the button and place over the navigationbar back button. And finally remove the button after use.

Here is the Swift 3 sample code done with UIImage instead of UIButton

override func viewDidLoad() {
    super.viewDidLoad()
    let imageView = UIImageView()
    imageView.backgroundColor = UIColor.clear
    imageView.frame = CGRect(x:0,y:0,width:2*(self.navigationController?.navigationBar.bounds.height)!,height:(self.navigationController?.navigationBar.bounds.height)!)
    let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(back(sender:)))
    imageView.isUserInteractionEnabled = true
    imageView.addGestureRecognizer(tapGestureRecognizer)
    imageView.tag = 1
    self.navigationController?.navigationBar.addSubview(imageView)
    }

write the code need to be executed

func back(sender: UIBarButtonItem) {

    // Perform your custom actions}
    _ = self.navigationController?.popViewController(animated: true)

    }

Remove the subView after action is performed

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    for view in (self.navigationController?.navigationBar.subviews)!{
        if view.tag == 1 {
            view.removeFromSuperview()
        }
    }
  • Thanks dude . :-) – ARSHWIN DENUEV LAL Feb 27 '18 at 02:19
  • How do you create state when touch down? – quang thang Feb 27 '18 at 08:28
  • This doesn't appear to work in iOS 11. Not when the UIImageView's background colour is clear. Set it to a different colour and it works. – Tap Forms Jun 29 '18 at 07:43
  • We can define a UIImageView with clear color, set its frame, assign tapgesture and place anywhere in the screen. Then why can't we place it over a navigation bar. To be sincere I won't recommend what I wrote . If there is an issue definitely there is a reason but it is not the color matters. Forget the code follow the logic u will succeed. :) – ARSHWIN DENUEV LAL Jul 19 '18 at 16:42
5

This is my solution

extension UINavigationController: UINavigationBarDelegate {
    public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
        if let shouldBlock = self.topViewController?.shouldPopFromNavigation() {
            return shouldBlock
        }
        return true
    }
}

extension UIViewController {
    @objc func shouldPopFromNavigation() -> Bool {
        return true
    }
}

In your view controller, you can handle like this:

@objc override func shouldPopFromNavigation() -> Bool {
        // Your dialog, example UIAlertViewController or whatever you want
        return false
    }
Hiro
  • 391
  • 5
  • 10
  • Hi @Hiro, any clue why the shouldPopFromNavigation is no more called from my side? Does iOS12 could be the issue? – David Nov 20 '20 at 18:55
  • @David: It should work for all. How about the status now? Did you put shouldPopFromNavigation in extension for UIViewController? – Hiro Dec 02 '20 at 04:28
  • I tried the same code on my iphone 5 running iOS 12 and it worked. The same code on my XR running iOS 14, does not. – David Dec 02 '20 at 20:53
  • I use `navigationBar(_ navigationBar: UINavigationBar, didPop item: UINavigationItem)` instead of `shouldPop` and works great fro force large title on the previous screen, thanks! – Maetschl Aug 13 '21 at 15:39
4

Swift 4.2:

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if self.isMovingFromParent {
        // Your code...

    }
}
Md. Najmul Hasan
  • 605
  • 1
  • 6
  • 19
4

For Swift 5, we can check it in view will disappear

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if self.isMovingFromParent {
        delegate?.passValue(clickedImage: selectedImage)
    }
}
Sandu
  • 436
  • 4
  • 8
4

Swift 5 __ Xcode 11.5

In my case I wanted to make an animation, and when it finished, go back. A way to overwrite the default action of the back button and call your custom action is this:

     override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        setBtnBack()
    }

    private func setBtnBack() {
        for vw in navigationController?.navigationBar.subviews ?? [] where "\(vw.classForCoder)" == "_UINavigationBarContentView" {
            print("\(vw.classForCoder)")
            for subVw in vw.subviews where "\(subVw.classForCoder)" == "_UIButtonBarButton" {
                let ctrl = subVw as! UIControl
                ctrl.removeTarget(ctrl.allTargets.first, action: nil, for: .allEvents)
                ctrl.addTarget(self, action: #selector(backBarBtnAction), for: .touchUpInside)
            }
        }
    }


    @objc func backBarBtnAction() {
        doSomethingBeforeBack { [weak self](isEndedOk) in
            if isEndedOk {
                self?.navigationController?.popViewController(animated: true)
            }
        }
    }


    private func doSomethingBeforeBack(completion: @escaping (_ isEndedOk:Bool)->Void ) {
        UIView.animate(withDuration: 0.25, animations: { [weak self] in
            self?.vwTxt.alpha = 0
        }) { (isEnded) in
            completion(isEnded)
        }
    }

NavigationBar view hierarchy

Or you can use this method one time to explore the NavigationBar view hierarchy, and get the indexes to access to the _UIButtonBarButton view, cast to UIControl, remove the target-action, and add your custom targets-actions:

    private func debug_printSubviews(arrSubviews:[UIView]?, level:Int) {
        for (i,subVw) in (arrSubviews ?? []).enumerated() {
            var str = ""
            for _ in 0...level {
                str += "\t"
            }
            str += String(format: "%2d %@",i, "\(subVw.classForCoder)")
            print(str)
            debug_printSubviews(arrSubviews: subVw.subviews, level: level + 1)
        }
    }

    // Set directly the indexs
    private func setBtnBack_method2() {
        // Remove or comment the print lines
        debug_printSubviews(arrSubviews: navigationController?.navigationBar.subviews, level: 0)   
        let ctrl = navigationController?.navigationBar.subviews[1].subviews[0] as! UIControl
        print("ctrl.allTargets: \(ctrl.allTargets)")
        ctrl.removeTarget(ctrl.allTargets.first, action: nil, for: .allEvents)
        print("ctrl.allTargets: \(ctrl.allTargets)")
        ctrl.addTarget(self, action: #selector(backBarBtnAction), for: .touchUpInside)
        print("ctrl.allTargets: \(ctrl.allTargets)")
    }
Miguel Gallego
  • 427
  • 4
  • 7
  • 1
    This works as expected, but in iOS 15 when I tap and hold the back button it pops up the menu actions and executes the back action. – Abdul Momen Dec 09 '21 at 07:44
  • Clever but you are REALLY playing with fire/begging for trouble to create solutions that dig that far into the undocumented implementation! It could change at any time, any update, release and break your app. Almost every time I try to get super clever and complicated and go under the hood to solve a problem it's not worth it, and there's usually a better design under the circumstances that I'm stubbornly refusing to see and appreciate. – clearlight May 04 '22 at 01:56
3

Swift 3:

override func didMove(toParentViewController parent: UIViewController?) {
    super.didMove(toParentViewController: parent)

    if parent == nil{
        print("Back button was clicked")
    }
}
Gal
  • 1,582
  • 2
  • 14
  • 30
  • -did/willMove(toParentViewController:) is possibly better than checking isMovingTfromParentViewController in -viewWillDisappear since it is only called when the view controller is actually changing parents (not when the view is covered by another VC's view) But the more "correct" solution is to implement the UINavigationController delegate method. Be careful, though; if the NavigationController already has a delegate, you run the risk of depriving that other delegate of callbacks it expects. – Bill Feth Aug 23 '18 at 17:35
  • I tested with a splitViewController. There, could not make the difference between added or removed. – claude31 Dec 28 '19 at 14:06
3

just do control + drag the bar item to below func. work like charm

@IBAction func done(sender: AnyObject) {
    if((self.presentingViewController) != nil){
        self.dismiss(animated: false, completion: nil)
        print("done")
    }
}

enter image description here

coders
  • 2,287
  • 1
  • 12
  • 20
2
    override public func viewDidLoad() {
         super.viewDidLoad()
         self.navigationController?.navigationBar.topItem?.title = GlobalVariables.selectedMainIconName
         let image = UIImage(named: "back-btn")

         image = image?.imageWithRenderingMode(UIImageRenderingMode.AlwaysOriginal)

        self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: image, style: UIBarButtonItemStyle.Plain, target: self, action: #selector(Current[enter image description here][1]ViewController.back) )
    }

    func back() {
      self.navigationController?.popToViewController( self.navigationController!.viewControllers[ self.navigationController!.viewControllers.count - 2 ], animated: true)
    }
Shahkar
  • 31
  • 1
  • 3
2
override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if self.isMovingToParent {

        //your code backView
    }
}
Papon Smc
  • 576
  • 4
  • 11
1

Try this .

self.navigationItem.leftBarButtonItem?.target = "methodname"
func methodname ( ) {            
  //    enter code here
}

Try on this too.

override func viewWillAppear(animated: Bool) {
  //empty your array
}
elp
  • 8,021
  • 7
  • 61
  • 120
Alvin George
  • 14,148
  • 92
  • 64
  • Doesn't work. self.navigationItem.leftBarButtonItem is nil when using the default (iOS provided) back button, which @StevenZ appears to be asking about. – Smartcat Jan 04 '23 at 21:50
1

As I understand you want to empty your array as you press your back button and pop to your previous ViewController let your Array which you loaded on this screen is

let settingArray  = NSMutableArray()
@IBAction func Back(sender: AnyObject) {
    self. settingArray.removeAllObjects()
    self.dismissViewControllerAnimated(true, completion: nil)
} 
Valeriy Van
  • 1,851
  • 16
  • 19
Avinash Mishra
  • 797
  • 9
  • 19
1

Here is the simplest possible Swift 5 solution that doesn't require you to create a custom back button and give up all that UINavigationController left button functionality you get for free.

As Brandon A recommends above, you need need to implement UINavigationControllerDelegate in the view controller you want to interact with before returning to it. A good way is to create an unwind segue that you can perform manually or automatically and reuse the same code from a custom done button or the back button.

First, make your view controller of interest (the one you want to detect returning to) a delegate of the navigation controller in its viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    navigationController?.delegate = self
}

Second, add an extension at the bottom of the file that overrides navigationController(willShow:animated:)

extension PickerTableViewController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController,
                              willShow viewController: UIViewController,
                              animated: Bool) {

        if let _ = viewController as? EditComicBookViewController {

            let selectedItemRow = itemList.firstIndex(of: selectedItemName)
            selectedItemIndex = IndexPath(row: selectedItemRow!, section: 0)

            if let selectedCell = tableView.cellForRow(at: selectedItemIndex) {
                performSegue(withIdentifier: "PickedItem", sender: selectedCell)
            }
        }
    }
}

Since your question included a UITableViewController, I included a way to get the index path of the row the user tapped.

John Pavley
  • 5,366
  • 2
  • 14
  • 16
1

My preference was to override the popViewController in the Navigation Controller. The advantages of this is:

  1. Your app keeps the default Back Button look and animations, and you don't have to manage it. This is particularly helpful if a user has Large Text set on their phone, since the default back button will increase or decrease in size based on the user settings.
  2. You can stop the view from popping altogether, unlike using viewWillDisappear.

First, create a custom Navigation Controller class (and be sure to assign it to the Navigation Controller in your Story Board or wherever your navigation controller is created):

class NavControllerWithBackButtonOverride: UINavigationController {

    var backButtonOverride: (() -> Void)? = nil

    override func popViewController(animated: Bool) -> UIViewController? {

        if backButtonOverride != nil {
            //if anything is assigned to the backButtonOverride the override will run
            self.backButtonOverride!()
            return nil
        } else {
            //otherwise the default popViewController will run
            return super.popViewController(animated: animated)
        }
    }
}

Then enable/disable the override in your View Controller by assigning a value to the backButtonOverride variable:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.enableCustomBackButton()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    self.disableCustomBackButton()
}

/**
 Custom Back Button
 */

func customBackButtonAction() {
    print("DO THIS INSTEAD")
}

func enableCustomBackButton() {
    if let nav = self.navigationController as? NavControllerWithBackButtonOverride {
        nav.backButtonOverride = { self.customBackButtonAction() }
        nav.interactivePopGestureRecognizer?.isEnabled = false
    }    
}

func disableCustomBackButton() {
    if let nav = self.navigationController as? NavControllerWithBackButtonOverride {
    nav.backButtonOverride = nil
    nav.interactivePopGestureRecognizer?.isEnabled = true
    }
}

Note: I also disabled interactivePopGestureRecognizer because it was causing issues with the custom setup.

iOS_Mouse
  • 754
  • 7
  • 13
0

I accomplished this by calling/overriding viewWillDisappear and then accessing the stack of the navigationController like this:

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)

    let stack = self.navigationController?.viewControllers.count

    if stack >= 2 {
        // for whatever reason, the last item on the stack is the TaskBuilderViewController (not self), so we only use -1 to access it
        if let lastitem = self.navigationController?.viewControllers[stack! - 1] as? theViewControllerYoureTryingToAccess {
            // hand over the data via public property or call a public method of theViewControllerYoureTryingToAccess, like
            lastitem.emptyArray()
            lastitem.value = 5
        }
    }
}
NerdyTherapist
  • 520
  • 5
  • 15
0

You can do something in your Viewcontroller like

override func navigationShouldPopOnBackButton() -> Bool {
    self.backAction() //Your action you want to perform.
    return true
}

For complete answer use Detecting when the 'back' button is pressed on a navbar

Avinash
  • 4,304
  • 1
  • 23
  • 18
0

Swift 5+ (Back button with alert control)

override func viewDidLoad() {
        super.viewDidLoad()
        
        self.navigationItem.hidesBackButton = true
        let newBackButton = UIBarButtonItem(title: "<Back", style: UIBarButtonItem.Style.plain, target: self, action: #selector(PGWebViewController.back(sender:)))
        self.navigationItem.leftBarButtonItem = newBackButton
}


@objc func back(sender: UIBarButtonItem) {
    
    let alert = UIAlertController(title: "Warning!", message: "Your payment process is not completed yet. Do you want to go back?", preferredStyle: .alert)
        
         let ok = UIAlertAction(title: "OK", style: .default, handler: { action in
             _ = self.navigationController?.popViewController(animated: true)
         })
         alert.addAction(ok)
         let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
         })
         alert.addAction(cancel)
         DispatchQueue.main.async(execute: {
            self.present(alert, animated: true)
    })}
Fahim Rahman
  • 247
  • 3
  • 13
0

Swift 5+ Xcode 14+

I tried multiple ways to override default back button by appending and removing view controllers from navigation stack.

But below solution worked only.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.navigationController?.navigationBar.topItem?.backBarButtonItem = UIBarButtonItem(title: " ", style: .plain, target: nil, action: #selector(popToRoot))
}

@objc func popToRoot() {
    self.navigationController?.popToRootViewController(animated: true)
}
Abdul Karim Khan
  • 4,256
  • 1
  • 26
  • 30
-1

You can simply remove unnecessary controllers from the stack, something like this:

self.navigationController?.viewControllers.removeAll(where: {
        $0 is FirstViewController || $0 is SecondWithPinController
    })
Alex Bro
  • 41
  • 5
-2

This is how I solved it for my own problem

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.navigationItem.leftBarButtonItem?.action = #selector(self.back(sender:))
    self.navigationItem.leftBarButtonItem?.target = self
}

@objc func back(sender: UIBarButtonItem) {

}
amorenew
  • 10,760
  • 10
  • 47
  • 69