I am using normal storyboarding and push segues in xcode, but I want to have segues that just appear the next view, not slide the next view (as in when you use a tab bar and the next view just appears).
Is there a nice simple way to have normal push segues just "appear" and not "slide", without needing to add custom segues?
Everything is working completely fine, I just want to remove that slide animation between the views.
I was able to do this by creating a custom segue (based on this link).
Create a new segue class (see below).
Open your Storyboard and select the segue.
Set the class to PushNoAnimationSegue (or whatever you decided to call it).
Swift 4
import UIKit
/*
Move to the next screen without an animation.
*/
class PushNoAnimationSegue: UIStoryboardSegue {
override func perform() {
self.source.navigationController?.pushViewController(self.destination, animated: false)
}
}
Objective C
PushNoAnimationSegue.h
#import <UIKit/UIKit.h>
/*
Move to the next screen without an animation.
*/
#interface PushNoAnimationSegue : UIStoryboardSegue
#end
PushNoAnimationSegue.m
#import "PushNoAnimationSegue.h"
#implementation PushNoAnimationSegue
- (void)perform {
[self.sourceViewController.navigationController pushViewController:self.destinationViewController animated:NO];
}
#end
You can uncheck "Animates" in Interface Builder for iOS 9
Ian's answer works great!
Here's a Swift version of the Segue, if anyone needs:
UPDATED FOR SWIFT 5, MAY 2020
PushNoAnimationSegue.swift
import UIKit
/// Move to the next screen without an animation
class PushNoAnimationSegue: UIStoryboardSegue {
override func perform() {
if let navigation = source.navigationController {
navigation.pushViewController(destination as UIViewController, animated: false)
}
}
I have now managed to do this using the following code:
CreditsViewController *creditspage = [self.storyboard instantiateViewControllerWithIdentifier:#"Credits"];
[UIView beginAnimations:#"flipping view" context:nil];
[UIView setAnimationDuration:0.75];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.navigationController.view cache:YES];
[self.navigationController pushViewController:creditspage animated:NO];
[UIView commitAnimations];
Hope this helps someone else!
Here's the Swift version adapted to modally present a ViewController without animation:
import UIKit
/// Present the next screen without an animation.
class ModalNoAnimationSegue: UIStoryboardSegue {
override func perform() {
self.sourceViewController.presentViewController(
self.destinationViewController as! UIViewController,
animated: false,
completion: nil)
}
}
answer using Swift3 -
for "push" segue:
class PushNoAnimationSegue: UIStoryboardSegue
{
override func perform()
{
source.navigationController?.pushViewController(destination, animated: false)
}
}
for "modal" segue:
class ModalNoAnimationSegue: UIStoryboardSegue
{
override func perform() {
self.source.present(destination, animated: false, completion: nil)
}
}
For anyone using Xamarin iOS your custom segue class needs to look like this:
[Register ("PushNoAnimationSegue")]
public class PushNoAnimationSegue : UIStoryboardSegue
{
public PushNoAnimationSegue(IntPtr handle) : base (handle)
{
}
public override void Perform ()
{
SourceViewController.NavigationController.PushViewController (DestinationViewController, false);
}
}
Don't forget you still need set a custom segue in your story board and set the class to the PushNoAnimationSegue class.
Just set animated false on UINavigationController.pushViewController in Swift
self.navigationController!.pushViewController(viewController, animated: false)
For me, the easiest way to do so is :
UIView.performWithoutAnimation {
self.performSegueWithIdentifier("yourSegueIdentifier", sender: nil)
}
Available from iOS 7.0
I'm using Visual Studio w/ Xamarin, and the designer doesn't provide the "Animates" checkmark in dtochetto's answer.
Note that the XCode designer will apply the following attribute to the segue element in the .storyboard file: animates="NO"
I manually edited the .storyboard file and added animates="NO" to the segue element(s), and it worked for me.
Example:
<segue id="1234" destination="ZB0-vP-ctU" kind="modal" modalTransitionStyle="crossDissolve" animates="NO" identifier="screen1ToScreen2"/>
PUSH WITHOUT ANIMATION : Swift Here is what worked for me.
import ObjectiveC
private var AssociatedObjectHandle: UInt8 = 0
extension UIViewController {
var isAnimationRequired:Bool {
get {
return (objc_getAssociatedObject(self, &AssociatedObjectHandle) as? Bool) ?? true
}
set {
objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
-------------------- SilencePushSegue --------------------
class SilencePushSegue: UIStoryboardSegue {
override func perform() {
if self.source.isAnimationRequired == false {
self.source.navigationController?.pushViewController(self.destination, animated: false)
}else{
self.source.navigationController?.pushViewController(self.destination, animated: true)
}
}
}
Usage : Set the segue class from storyboard as shown in picture. set the isAnimationRequired from your viewcontroller to false from where you want to call performSegue, when you want to push segue without animation and set back to true after calling self.performSegue. Best of luck....
DispatchQueue.main.async {
self.isAnimationRequired = false
self.performSegue(withIdentifier: "showAllOrders", sender: self);
self.isAnimationRequired = true
}
Related
It is possible in some cases (iPhone X, iOS 13) to dismiss presented view controllers with a gesture, by pulling from the top.
In that case, I can't seem to find a way to notify the presenting view controller. Did I miss something?
The only I found would be to add a delegate method to the viewDidDisappear of the presented view controller.
Something like:
class Presenting: UIViewController, PresentedDelegate {
func someAction() {
let presented = Presented()
presented.delegate = self
present(presented, animated: true, completion: nil)
}
func presentedDidDismiss(_ presented: Presented) {
// Presented was dismissed
}
}
protocol PresentedDelegate: AnyObject {
func presentedDidDismiss(_ presented: Presented)
}
class Presented: UIViewController {
weak var delegate: PresentedDelegate?
override func viewDidDisappear(animated: Bool) {
...
delegate?.presentedDidDismiss(self)
}
}
It is also possible to manage this via notifications, using a vc subclass but it is still not satisfactory.
extension Notification.Name {
static let viewControllerDidDisappear = Notification.Name("UIViewController.viewControllerDidDisappear")
}
open class NotifyingViewController: UIViewController {
override open func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.post(name: .viewControllerDidDisappear, object: self)
}
}
There must be a better way to do this?
From iOS 13 Apple has introduced a new way for the users to dismiss the presented view controller by pulling it down from the top. This event can be captured by implementing the UIAdaptivePresentationControllerDelegate to the UIViewController you're presenting on, in this case, the Presenting controller. And then you can get notified about this event in the method presentationControllerDidDismiss. Here is the code example :-
class Presenting: UIViewController, UIAdaptivePresentationControllerDelegate {
func someAction() {
let presented = Presented()
presented.presentationController?.delegate = self
present(presented, animated: true, completion: nil)
}
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Note:
This method only gets triggered for dismissing by swiping from the top and not for the programmatic dismiss(animated:,completion:) method.
You don't need any custom delegate or Notification observer for getting the event where the user dismisses the controller by swiping down, so you can remove them.
Adopt UIAdaptivePresentationControllerDelegate and implement presentationControllerDidAttemptToDismiss (iOS 13+)
extension Presenting : UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
presentationController.presentingViewController.presentedDidDismiss(self)
}
}
UIPresentationController has a property presentingViewController. The name is self-explanatory. You don't need the explicit delegate protocol.
The method is actually called to be able to show a dialog for example to save changes before dismissing the controller. You can also implement presentationControllerDidDismiss()
And do not post notifications to controllers which are related to each other. That's bad practice.
Say we have a custom control, that at some state should pop the option list for user to choose from (UIAlertController). Usually when we are in a view controller we use presentViewController method, but in this case we have no access to a parent UIViewController which holds this method. Although there's seems to be some way to get to UIViewController from subviews, it's considered against MVC design pattern. Then how would you do this?
I want to keep view controller as lean as possible and custom view (uicontrol) self sufficient, so I wouldn't want to move this logic to view controller.
Although all your advice and suggestions are welcome and appreciated.
You can always use application rootviewController to present your alert view controller :) You dont always have to opt for your view controller's view :) rootview controller is always accessible no matter where your control is :)
Here is what you can do :)
Objective C
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[delegate.window.rootViewController presentViewController:your_alert_view_controller animated:YES completion:nil];
Swift
let appDelegate : AppDelegate? = UIApplication.sharedApplication().delegate as? AppDelegate
if let unwrappedAppdelegate = appDelegate {
unwrappedAppdelegate.window!.rootViewController! .presentViewController(alert, animated: true, completion: nil)
}
Hope my answer helped you :)
I would prefer delegation or target-action. It's much cleaner way because in this case you don't use an AppDelegate. Also it's not a subview's responsibility to show something. Example with delegation:
protocol MyViewDelegate: class {
func somethingHappenedToMyView(view: MyView)
}
class MyView: UIView {
weak var delegate: MyViewDelegate?
func somethingHappened() {
delegate?.somethingHappenedToMyView(self)
}
}
class ViewController: UIViewController, MyViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
myView.delegate = self
}
func somethingHappenedToMyView(view: MyView) {
presentViewController(someViewController, animated: true, completion: nil)
}
}
Example with target-action:
class MyView: UIControl {
func somethingHappened() {
sendActionsForControlEvents(.ValueChanged) // .ValueChanged is an example. Maybe other events will be more meaningful for your situation
}
}
class ViewController: UIViewController, MyViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
myView.addTarget(self, action: #selector(self.somethingHappenedToMyView(_:)), forControlEvents: .ValueChanged)
}
func somethingHappenedToMyView(view: MyView) {
presentViewController(someViewController, animated: true, completion: nil)
}
}
Presenting from delegate root view controller will not work in many cases (nested presentation, tab's more controller etc).
Better find parent UIViewController responder by looping through view's responder chain. Use this macro:
#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})
In view:
UIViewController *parentViewController = UIViewParentController(self);
Related questions: one, two.
The top three answers can solve my questions. It is hard to pick which one is the best. So, I just pick the one who is the first to answer my question. Sorry for amateur and iOSEnthusiatic. Thank you for your help. I appreciate it.
ViewController 1 has a table view.
My question is how to reload the table view only if I click back from view controller 2, and not reload the table view if I click back from view controller 3.
Right now, my code for back button is
#IBAction func backButtonTapped(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
In view controller 1. I know that the table view would be reloaded from either view controller 2 or 3
override func viewDidAppear(animated: Bool) {
loadTable()
}
I tried to put loadTable() in viewDidLoad and try to write the below code for back button in view controller 2. But, it doesn't work.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("UserHomePageViewController") as! UserHomePageViewController
controller.viewDidLoad()
Any suggestion what I should do? Thank you for your help.
EDIT:
I think this is an easier way to do it, but it still does not work as I thought. I guess it is because the viewDidAppear is executed before the call of reloadTableBool. Correct? Is there any way to fix it? Thank you. You help would be appreciated.
class 2ViewController
#IBAction func backButtonTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("1ViewController") as! 1ViewController
print("viewcontroller2 before call: \(controller.reloadTableBool)")
controller.reloadTableBool = false
print("viewcontroller2 after call: \(controller.reloadTableBool)")
self.dismissViewControllerAnimated(true, completion: nil)
}
class 1ViewController
var reloadTableBool = true
override func viewDidAppear(animated: Bool) {
print("viewcontroller1: \(reloadTableBool)")
if reloadTableBool == true {
loadTable()
}
}
When I click back on view controller 2, it prints
viewcontroller2 before call: true
viewcontroller2 after call: false
viewcontroller1: true
Here is a link to a question I answered a couple days ago. Use the navigation controller delegate to handle the back button. In your second view controller, set the delegate to self and reload the tableview when you press the back button.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if let controller = viewController as? FirstViewController {
controller.tableView.reloadData()
}
}
NOTE:
I'm assuming you're using the back button of the navigation controller here.
EDIT: Another example using your manually added back button:
#IBAction func backButtonTapped(sender: AnyObject) {
if let viewControllers = app.window?.rootViewController?.childViewControllers {
viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
}
self.dismissViewControllerAnimated(true, completion: nil)
}
Seeing as you are using a navigation controller:
#IBAction func backButtonTapped(sender: AnyObject) {
navigationController?.viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
self.dismissViewControllerAnimated(true, completion: nil)
}
If displaying vc2 is performed by vc1 and is always sure to invalidate the data in vc1, you could do the following:
add a needsReload boolean instance variable to vc1
set it to true whenever you display vc2 (and when instanciating vc1 eg in awakeFromNib if coming from a storyboard)
only perform the content of loadTable if needsReload is true (maybe refactor this logic into a loadTableIfNeeded)
don't forget to set needsReload to false in the end of loadTableIfNeeded
This invalidation pattern is found throughout UIKit, see for example UIView setNeedsLayout/layoutIfNeeded. The advantage is that even if several events cause the data to invalidate, it will only actually get refreshed when you need it.
In your situation it has the additional advantage of keeping the logic contained in vc1 and not creating unnecessary coupling between your VCs, which is always good.
---UPDATE: sample implementation (ObjC but you'll get the idea)
You only need to handle this in VC1, forget about all the back button stuff in VC2. This implementation will mark VC1 for reload as soon as VC2 is presented, but will actually reload only on viewWillAppear, when VC2 is dismissed.
---UPDATE 2: Added a conditional reload based on a delegate callback
Note that _needsReload is now set in the delegate callback, not when VC2 is first presented. Instead we set VC1 as the delegate of VC2. (_needsReload logic is actually unnecessary using this method, kept it for reference)
//VC2: add a delegate to the interface
#class VC2;
#protocol VC2Delegate
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges;
#end
#interface VC2
#property (nonatomic, weak) id<VC2Delegate> delegate
#end
#implementation VC2
- (IBAction) finishWithChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:YES];
}
- (IBAction) finishWithoutChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:NO];
}
#end
//VC1: implement the VC2Delegate protocol
#interface VC1 () <VC2Delegate>
#end
#implementation VC1
{
BOOL _needsReload
}
- (void) awakeFromNib
{
//adding this for completeness but the way you did it in Swift (at init) is correct
[super awakeFromNib];
_needsReload = YES;
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self reloadTableIfNeeded];
}
- (IBAction) displayVC2
{
VC2* myVC2 = ... //instanciate your VC2 here
myVC2.delegate = self; //set as the delegate of VC2
[self presentViewController:myVC2 animated:YES completion:nil];
}
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges
{
_needsReload = hasChanges;
[self reloadTableIfNeeded];
}
- (void) reloadTableIfNeeded
{
if (_needsReload) {
[self.tableView reloadData];
_needsReload = NO;
}
}
#end
You can use notification approach easily for this.
Add observer in your 1st ViewController in viewDidLoad method.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadTable:", name: "reloadTable", object: nil)
func reloadTable(notification : NSNotification){
let isReload : NSNumber = notification.userInfo!["isReload"] as! NSNumber
if (isReload.boolValue) {
self.tableView.reloadData()
}
}
Then post notification like this from your 2nd and 3rd ViewController respectively when you call dismissViewController.
// From 2nd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: false)])
// From 3rd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: true)])
In my project I have a button on the bottom right side of the screen and i added another uiviewcontroller to the storyboard, did control-drag to the uiviewcontroller I wanted as the popover, then set that viewcontroller size to (300, 300) and checked 'use preferred explicit size'. When I load the app and click the button, the entire screen gets covered by the "popover". I also tried to go into the popoverViewController's .m file and set the size but that didn't work either.
Any ideas?
Edit: Since it looks like I have to have it be full screen that is fine however I am still running into some other problems I was having earlier. My popup screen will come up and I make the background black and alpha as .5 to make it see through however it'll do the animation, then once the animation is finished the screen will go from .5 opacity to completely black and the only thing I can see is the battery icon thing.
The OP uses Objective-C. This answer presents code in swift. Converting swift to Objective-C should be easy.
In the newly added ViewController, under “Simulated Metrics” change “Size” to “Freeform” and “Status Bar” to “None.”
Under “Simulated Size” change your view’s height and width to the actual size you want your popover’s content to be.
Create a segue to the newly added VC. Use segue type as "Present As Popover" and give a name for the segue, for example "popoverSegue".
In the ViewConroller from which this segue is to be triggered, add the UIPopoverPresentationControllerDelegate protocol.
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {
}
Override the prepareForSegue function to catch your popover segue. Set the modalPresentationStyle to .Popover to explicitly state that you want a popover and then assign the delegate property of the view’s popoverPresentationController to self:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "popoverSegue" {
let popoverViewController = segue.destinationViewController as! UIViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
Implement the adaptivePresentationStyleForPresentationController function to tell your app that you really want that popover presentation and will accept no substitutions:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
Following these, I could get a popup on iPhone which is not full screen but the size set for the ViewController.
Source: iPad Style Popovers on the iPhone with Swift
Thanks to Bharat for the great answer, I personally use a UIStoryboardSegue that does pretty much the same thing. That way, I can change the class of the segue in the storyboard, have what I want, and not pollute my controllers:
class AlwaysPopupSegue : UIStoryboardSegue, UIPopoverPresentationControllerDelegate
{
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{
super.init(identifier: identifier, source: source, destination: destination)
destination.modalPresentationStyle = UIModalPresentationStyle.popover
destination.popoverPresentationController!.delegate = self
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
}
Swift 3-5 version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SEGUE_IDENTIFIER" {
let popoverViewController = segue.destination as! YourViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
Swift 4 Version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SegueIdentifier" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = .popover
popoverViewController.presentationController?.delegate = self
}
}
Don't forget to add
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
On iPhone you can creat a custom view controller that can manage all the popovers. Since each view controller has its own navigation controller, you can add a new view controller to the app.window.rootviewcontroller as a du view and bring all to front.
If you didn't want to write your own, you can use something like this for instance: http://cocoapods.org/pods/FPPopover
This is Swift 5 code, some/most of the above mentioned solutions are all valid. This is an effort to present whole solution. This example supposes you are using a xib for popover view controller but this would work otherwise as well, say, in prepare for segue. Here's a complete code:
Presenting ViewController:
let popoverVC = PopoverVC(nibName: "popoverVC", bundle: nil)
popoverVC.completionHandler = { [unowned self] (itemIndex : Int?) in
if let itemIndex = itemIndex
{
// Do completion handling
}
}
popoverVC.preferredContentSize = CGSize(width: 200, height: 60)
popoverVC.modalPresentationStyle = .popover
if let pvc = popoverVC.popoverPresentationController {
pvc.permittedArrowDirections = [.down]
pvc.delegate = self
pvc.sourceRect = button.frame
pvc.sourceView = button // Button popover is presented from
present(popoverVC, animated: true, completion: nil)
}
This is important:
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}
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)
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.
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
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
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.
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
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)
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
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
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)
}
}
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!
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()
}
}
}