36

I am working on a camera app where the camera views are shown modally. After I am done with cropping. I perform an unwind segue to the MainPageViewController. (Please see the screenshot)

storyboard

My unwind function inside MainPageViewController is as follows;

@IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
    self.performSegueWithIdentifier("Categories", sender: self)
}

where "categories" is the push segue identifier from MainPageViewController to CategoriesTableViewController.

The program enters the unwindToMainMenu function but it does not perform the push segue. Any idea how to fix this?

Note: I found the same question but the answer suggests to change the storyboard structure.

Community
  • 1
  • 1
Berkan Ercan
  • 1,187
  • 3
  • 13
  • 31

7 Answers7

55

A bit late to the party but I found a way to do this without using state flags

Note: this only works with iOS 9+, as only custom segues support class names prior to iOS9 and you cannot declare an exit segue as a custom segue in storyboards

1. Subclass UIStoryboardSegue with UIStoryboardSegueWithCompletion

class UIStoryboardSegueWithCompletion: UIStoryboardSegue {
    var completion: (() -> Void)?

    override func perform() {
        super.perform()
        if let completion = completion {
            completion()
        }
    }
}

2. Set UIStoryBoardSegueWithCompletion as the class for your exit segue

note: the action for this segue should be unwindToMainMenu to match the original question

Select exit segue from storyboard Add custom class

3. Update your unwind @IBAction to execute the code in the completion handler

@IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
    if let segue = segue as? UIStoryboardSegueWithCompletion {
        segue.completion = { 
            self.performSegueWithIdentifier("Categories", sender: self) 
        }
    }
}

Your code will now execute after the exit segue completes its transition

wyu
  • 1,622
  • 13
  • 31
  • 4
    I could only get this to work reliably by wrapping the completion inside a transition coordinator: `destinationViewController.transitionCoordinator()?.animateAlongsideTransition(nil, completion: { _ in completion() })` – Bringo Jun 29 '16 at 01:43
  • @Bringo that's interesting... If you're setting the transition coordinator then perhaps you can just directly set the completion block of the transition coordinator from the unwind action, instead of having to go through a custom segue – wyu Jul 05 '16 at 15:40
  • okay you can't since there's no transition active during the unwind segue call, therefore the transition coordinator is nil (ie the transition happens after the unwind segue call returns) – wyu Jul 05 '16 at 16:01
  • This is causing memory leaks in my app! :( – Thomás Pereira May 31 '19 at 14:29
  • you probably want to put weak self in the segue completion. Let me know if that fixes your memory leak and I can update the answer @TomCalmon – wyu Jun 06 '19 at 18:44
  • No, that didn't fix the problem. I ended up setting some flags and execute code in `viewDidDisappear(animated:)`. – Thomás Pereira Jun 06 '19 at 19:04
12

I want to provide my own solution to this problem for now. Any further answers are always welcome.

I put a boolean variable and viewDidAppear function to MainPageViewController.

var fromCamera = false

override func viewDidAppear(animated: Bool) {
    if fromCamera {
        self.performSegueWithIdentifier("categorySelection", sender: self)
        self.fromCamera = false
    }
}

I set fromCamera to true before I perform unwind segue from CropViewController. By that way, I perform segue to category screen only if an unwind segue from crop view is performed.

Berkan Ercan
  • 1,187
  • 3
  • 13
  • 31
  • The more reliable solution. But unfortunately I have to resort to the hackish solutions when unwinding from a modally presented view as `viewDidAppear` is not called in that case. – mohonish Mar 30 '16 at 06:18
6

Taking forward this answer (I only had Objective-C code)

Subclass UIStoryBoardSegue

#import <UIKit/UIKit.h>

@interface MyStoryboardSegue : UIStoryboardSegue

/**
 This block is called after completion of animations scheduled by @p self.
 */
@property (nonatomic, copy) void(^completion)();

@end

And call this completion block after completion of animations.

@implementation MyStoryboardSegue

- (void)perform {
  [super perform];
  if (self.completion != nil) {
    [self.destinationViewController.transitionCoordinator
     animateAlongsideTransition:nil
     completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
       if (![context isCancelled]) {
         self.completion();
       }
     }];
  }
}

@end
Community
  • 1
  • 1
Ayush Goel
  • 3,086
  • 25
  • 35
5

Taking forward the previous two answers, there is a bit more detail about the objective c version here (I too only had Objective-C code)

  1. Subclass UIStoryboardSegue with UIStoryboardSegueWithCompletion

    class UIStoryboardSegueWithCompletion: UIStoryboardSegue { var completion: (() -> Void)?

    override func perform() {
        super.perform()
        if let completion = completion {
            completion()
        }
    }
    

    }

UIStoryboardSegueWithCompletion.h

#import <UIKit/UIKit.h>

@interface MyStoryboardSegue : UIStoryboardSegueWithCompletion

@property (nonatomic, copy) void(^completion)();

@end

UIStoryboardSegueWithCompletion.m

#import "UIStoryboardSegueWithCompletion.h"

@implementation UIStoryboardSegueWithCompletion

- (void)perform {
  [super perform];
  if (self.completion != nil) {
    [self.destinationViewController.transitionCoordinator
     animateAlongsideTransition:nil
     completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
       if (![context isCancelled]) {
         self.completion();
       }
     }];
  }
}
@end
  1. Set UIStoryBoardSegueWithCompletion as the class for your exit segue

note: the action for this segue should be unwindToMainMenu to match the original question [image showing segue ui][1] [image showing segue ui 2][2]

Select exit segue from storyboard Add custom class

-(IBAction)unwindToMainMenu(UIStoryboardSegue *)segue {
    if([segue isKindOfClass:[UIStoryboardSegueWithCompletion class]]){
        UIStoryboardSegueWithCompletion *segtemp = segue;// local prevents warning
        segtemp.completion = ^{
            NSLog(@"segue completion");
            [self performSegueWithIdentifier:@"Categories" sender:self];
        };
    }
}

Your code will now execute after the exit segue completes its transition

moride
  • 61
  • 1
  • 1
4

I'm guessing the performSegue is not firing because the unwind segue has not yet finished. The only thing I can think of at the moment, is to delay calling the performSegue using dispatch_after. This seems very "hacky" to me though.

@IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
    dispatch_after(1, dispatch_get_main_queue()) { () -> Void in
        self.performSegueWithIdentifier("Categories", sender: self)
    }
}
Ron Fessler
  • 2,633
  • 1
  • 14
  • 22
  • I also find a way which might also be "hacky"; I set a boolean variable to true inside the unwind function and inside the viewDidAppear function, I call the performForSegue function if that boolean is true. What do you think about this solution? – Berkan Ercan Dec 15 '14 at 20:08
  • 1
    I think that's a good solution. `dispatch_after`, due to the variable wait time, may have issues. Using the boolean and viewDidAppear ensures the perfromSegue will execute when the view is ready. – Ron Fessler Dec 15 '14 at 20:15
3

The exit segue IBAction method happens before the actual unwind segue is finished. I had the same issue and resolved it this way (if you don't mind my paraphrasing of your code). It avoids the extra time and animations from relying on ViewDidAppear.

@IBAction func unwindToMainMenu(segue: UIStoryboardSegue) {
   let categoriesTable = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("CategoryTableViewController")
   self.navigationController?.viewControllers.append(categoriesTable)
   self.navigationController?.showViewController(categoriesTable, sender: self)
}

Hope this is helpful for anyone else who runs into this and just wants an instantaneous transition!

Cole
  • 31
  • 4
3

Updated @moride's answer for Swift 5. The transition coordinator is now optional, so we run completion immediately if this is the case.

class UIStoryboardSegueWithCompletion: UIStoryboardSegue {
    var completion: (() -> Void)?

    override func perform() {
        super.perform()

        guard let completion = completion else { return }
        guard let coordinator = destination.transitionCoordinator else {
            completion()
            return
        }

        coordinator.animate(alongsideTransition: nil) { context in
            guard !context.isCancelled else { return }
            completion()
        }
    }
}
John Rogers
  • 1,983
  • 16
  • 28