Home » Swift » In my viewDidAppear, how do I know when it's being unwound by a child?

In my viewDidAppear, how do I know when it's being unwound by a child?

Posted by: admin November 30, 2017 Leave a comment

Questions:

When my child performs an unwind segue, my controller’s viewDidAppear gets called.

In this method (and this method alone, I need to know whether it was from an unwind or not)

Note: the child is unwinding to the very first view controller, so this is an intermediate view controller, not the true root.

Answers:

You should be able to use the following to detect in each controller if the exposure of the view controller was as a result of being pushed/presented, or as a result of being exposed as a result of pop/dismiss/unwind.

This may or may be enough for your needs.

- (void) viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];

    // Handle controller being exposed from push/present or pop/dismiss
    if (self.isMovingToParentViewController || self.isBeingPresented){
        // Controller is being pushed on or presented.
    }
    else{
        // Controller is being shown as result of pop/dismiss/unwind.
    }
}

If you want to know that viewDidAppear was called because of an unwind segue as being different from a conventional pop/dismiss being called, then you need to add some code to detect that an unwind happened. To do this you could do the following:

For any intermediate controller you want to detect purely an unwind in, add a property of the form:

/** BOOL property which when TRUE indicates an unwind occured. */
@property BOOL unwindSeguePerformed;

Then override the unwind segue method canPerformUnwindSegueAction:fromViewController:withSender: method as follows:

- (BOOL)canPerformUnwindSegueAction:(SEL)action
                 fromViewController:(UIViewController *)fromViewController
                         withSender:(id)sender{
  // Set the flag indicating an unwind segue was requested and then return
  // that we are not interested in performing the unwind action.
  self.unwindSeguePerformed = TRUE;

  // We are not interested in performing it, so return NO. The system will
  // then continue to look backwards through the view controllers for the 
  // controller that will handle it.
  return NO;
}

Now you have a flag to detect an unwind and a means to detect the unwind just before it happens. Then adjust the viewDidAppear method to include this flag.

- (void) viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];

    // Handle controller being exposed from push/present or pop/dismiss
    // or an unwind
    if (self.isMovingToParentViewController || self.isBeingPresented){
        // Controller is being pushed on or presented.
        // Initialize the unwind segue tracking flag.
        self.unwindSeguePerformed = FALSE;
    }
    else if (self.unwindSeguePerformed){
        // Controller is being shown as a result of an unwind segue
    }
    else{
        // Controller is being shown as result of pop/dismiss.
    }
}

Hopefully this meets your requirement.

For docs on handling the unwind segue chain see: https://developer.apple.com/library/ios/technotes/tn2298/_index.html

Questions:
Answers:

Here is a simple category on UIViewController that you can use to track whether your presented view controller is in the midst of an unwind segue. I suppose it could be flushed out more but I believe this much works for your case.

To use it you need to register the unwind segue from your unwind action method on the destination view controller:

- (IBAction) prepareForUnwind:(UIStoryboardSegue *)segue
{
    [self ts_registerUnwindSegue: segue];
}

That’s it. From your intermediate view controller, you can test if you are in the midst of an unwind segue:

- (void) viewDidAppear:(BOOL)animated
{
    [super viewDidAppear: animated];

    BOOL unwinding = [self ts_isUnwinding];

    NSLog( @"%@:%@, unwinding: %@", self.title, NSStringFromSelector(_cmd), unwinding ? @"YES" : @"NO" );
}

There’s no need to clean anything up; the segue will self-deregister when it ends.

Here’s the full category:

@interface UIViewController (unwinding)

- (void) ts_registerUnwindSegue: (UIStoryboardSegue*) segue;
- (BOOL) ts_isUnwinding;

@end


static NSMapTable* g_viewControllerSegues;

@implementation UIViewController (unwinding)

- (void) ts_registerUnwindSegue: (UIStoryboardSegue*) segue
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        g_viewControllerSegues = [NSMapTable weakToWeakObjectsMapTable];
    });

    for ( UIViewController* vc = segue.sourceViewController ; vc != nil ; vc = vc.presentingViewController )
    {
        [g_viewControllerSegues setObject: segue forKey: vc];
    }
}

- (BOOL) ts_isUnwinding
{
    return [g_viewControllerSegues objectForKey: [self ts_topMostParentViewController]] != nil;
}

- (UIViewController *)ts_topMostParentViewController {
    UIViewController *viewController = self;
    while (viewController.parentViewController) {
        viewController = viewController.parentViewController;
    }
    return viewController;
}

@end

Questions:
Answers:

Your question was really interesting to me, because I never used IB and segues before (don’t judge me for that) and wanted to learn something new. As you described in your comments:

viewDidAppear will be called on B when C rewinds to A

So I come up with an easy custom solution to this:

protocol ViewControllerSingletonDelegate: class {

    func viewControllerWillUnwind(viewcontroller: UIViewController, toViewController: UIViewController)
}

class ViewControllerSingleton {

    static let sharedInstance = ViewControllerSingleton()

    private var delegates: [ViewControllerSingletonDelegate] = []

    func addDelegate(delegate: ViewControllerSingletonDelegate) {

        if !self.containsDelegate(delegate) {

            self.delegates.append(delegate)
        }
    }

    func removeDelegate(delegate: ViewControllerSingletonDelegate) {

        /* implement any other function by your self :) */
    }

    func containsDelegate(delegate: ViewControllerSingletonDelegate) -> Bool {

        for aDelegate in self.delegates {

            if aDelegate === delegate { return true }
        }

        return false
    }

    func forwardToDelegate(closure: (delegate: ViewControllerSingletonDelegate) -> Void) {

        for aDelegate in self.delegates { closure(delegate: aDelegate) }
    }
}


class SomeViewController: UIViewController, ViewControllerSingletonDelegate {

    let viewControllerSingleton = ViewControllerSingleton.sharedInstance

    func someFunction() { // some function where you'll set the delegate

        self.viewControllerSingleton.addDelegate(self)
    }

    /* I assume you have something like this in your code */
    @IBAction func unwindToSomeOtherController(unwindSegue: UIStoryboardSegue) {

        self.viewControllerSingleton.forwardToDelegate { (delegate) -> Void in

            delegate.viewControllerWillUnwind(unwindSegue.sourceViewController, toViewController: unwindSegue.destinationViewController)
        }

        /* do something here */
    }

    // MARK: - ViewControllerSingletonDelegate
    func viewControllerWillUnwind(viewcontroller: UIViewController, toViewController: UIViewController) {

        /* do something with the callback */
        /* set some flag for example inside your view controller so your viewDidAppear will know what to do */
    }
}

You also could modify the callback function to return something else, like controller identifier instead the controller itself.

I do everything programmatically, so please don’t judge me for that too. 😉

If this code snippet won’t help you, I’d still love to see some feedback.

Questions:
Answers:

Suppose the segue navigation is ViewController -> FirstViewController -> SecondViewController. There is an unwind from SecondViewController to ViewController. You can add in the intermediary FirstViewController the following code to detect unwind actions.

import UIKit

class FirstViewController: UIViewController {

    var unwindAction:Bool = false
    override func viewDidAppear(animated: Bool) {
        if unwindAction {
            println("Unwind action")
            unwindAction = false
        }
    }
    override func viewControllerForUnwindSegueAction(action: Selector, fromViewController: UIViewController, withSender sender: AnyObject?) -> UIViewController? {
        self.unwindAction = true

         return super.viewControllerForUnwindSegueAction(action, fromViewController: fromViewController, withSender: sender)
    }

}

EDIT
After giving this some thought, I decided the solution to this depends on the kind of complexity that you are dealing with here. What exactly do you do when you do the unwind segue? The solutions given here are viable and they work — only if you want to detect whether it is an unwind action. What if you want to pass the data between the point where the unwind is happening to the root? What if there is a complex set of preparations that you wanna do in one of the intermediate view controllers? What if you want to do both of these?

In such complex scenarios, I would immediately rule out overriding the unwind methods of the view controller. Doing such operations there will work, but it won’t be clean. A method will be doing what it isn’t supposed to do. Smell that? That’s code smell.

What if, somehow a view controller could inform the next view controller in the hierarchy of the event happening? Better yet, how do we do this without tightly coupling these two?

Protocol.

Have a protocol definition something like:

protocol UnwindResponding {
    prepareForUnwindSegue(segue:UISegue , formViewController:UIViewController, withImportantInfo info:[String,AnyObject])
}

Using protocol you will keep the relationship between the objects — the hierarchy of view controllers in this case — explicit. At the point of occurrence of a particular event, you will delegate the call to the next controller in the hierarchy informing about the happening of a particular event in another view controller. Here is an example:

override func prepareForSegue(segue:UIStoryboardSegue, sender:AnyObject?) {

    if let unwindResponder = self.presentingViewController as? UnwindResponding where segue.identifier = "unwindSegue" {
        unwindResponder.prepareForUnwindSegue(segue:UISegue, fromViewController:self,info:info)
    }


}

In the intermediary view controller you can do something like:

extension IntermediaryViewController : UnwindResponding {
    prepareForUnwindSegue(segue:UISegue , fromViewController:UIViewController, withImportantInfo info:[String,AnyObject]) {
        if let unwindResponder =  self.presentingViewController {
            unwindResponder.prepareForUnwindSegue(segue,fromViewController:fromViewController, info:info)
        }
        unwindSegue = true
    }
}

Granted, you wouldn’t wanna do this if you just want to detect unwind segues. Maybe you do, you’ll never know what will happen in the future. Never hurts to keep your code clean.

Questions:
Answers:

Add method in your parent view controller

@IBAction func unwindToParent(unwindSegue: UIStoryboardSegue) {
    if let childViewController = unwindSegue.sourceViewController as? ChildViewController {
        println("unwinding from child")
    }
}

As an exemple if the unwind segue is related to a button, in the storyboard link your button to it’s view controller exit

enter image description here

It will propose to link to unwindToParent method

enter image description here

Then each time the unwind segue is performed, the unwindToParent method will be called