37
votes

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.

7
wether this unwinding properly to the MainPageViewController ?Anil Varghese
I can unwind properly, But I want to perform push segue immediately after I unwind to MainPageViewControllerBerkan Ercan
You can catch the end of the segue. See that question: stackoverflow.com/questions/18310396/…Jean-Baptiste Yunès

7 Answers

58
votes

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

12
votes

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.

6
votes

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
5
votes

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

4
votes

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)
    }
}
3
votes

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!

3
votes

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()
        }
    }
}