Perform push segue after an unwind segue
Asked Answered
C

7

38

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.

Chiliasm answered 15/12, 2014 at 12:15 Comment(3)
wether this unwinding properly to the MainPageViewController ?Knowledge
I can unwind properly, But I want to perform push segue immediately after I unwind to MainPageViewControllerChiliasm
You can catch the end of the segue. See that question: #18310896Pigeonwing
P
59

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

Parisian answered 2/6, 2016 at 21:27 Comment(6)
I could only get this to work reliably by wrapping the completion inside a transition coordinator: destinationViewController.transitionCoordinator()?.animateAlongsideTransition(nil, completion: { _ in completion() })Flowerpot
@Flowerpot 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 segueParisian
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)Parisian
This is causing memory leaks in my app! :(Cerda
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 @TomCalmonParisian
No, that didn't fix the problem. I ended up setting some flags and execute code in viewDidDisappear(animated:).Cerda
C
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.

Chiliasm answered 16/12, 2014 at 12:31 Comment(1)
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.Nostology
H
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
Hectogram answered 5/7, 2016 at 7:37 Comment(0)
B
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

Burgage answered 24/3, 2017 at 17:6 Comment(0)
C
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)
    }
}
Cowrie answered 15/12, 2014 at 19:56 Comment(2)
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?Chiliasm
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.Cowrie
S
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!

Schutz answered 24/9, 2015 at 22:40 Comment(0)
S
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()
        }
    }
}
Scarlatti answered 7/4, 2020 at 3:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.