Perform push segue after an unwind segue - ios

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

Related

viewWillAppear not always executed when viewController is present

I am working on an iOS app.
There is a viewController A that perfoms a segue programmatically to a detail viewController D.
On that detail viewController D the user may change some information that should be updated when clicking the back button to viewController A.
On viewController A I have included this function:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
determineMyCurrentLocation()
print("reloading data")
downloadJSONFavoritos {
print("reloading viewwillappear")
self.collectionViewFavoritos.reloadData()
}
}
My issue is that this function is not always called when back to viewController A, and then the information in A is not updated as it should.
I will create simple example, please adaptate it to your needs.
ViewController A:
class ViewControllerA: UIViewController {
func someMethod() {
let viewControllerD = // creation and displaying logic of your viewController D
viewControllerD.updateCompletionHandler = { [unowned self] in
/// Any updates goes here ...
}
}
}
ViewController D:
class ViewControllerD: UIViewController {
var updateCompletionHandler: (() -> Void)?
/// Just example
#IBAction func triggerUpdateButton(_ sender: UIButton) {
/// Call it, in place where user make changes
updateCompletionHandler?()
}
}
If you have any questions, let me know in the comments.

Fail calling back about protocol and delegate by Swift4

I m confuse about call back using protocol and delegate.
The problem is. I have two viewcontrollers vcA & vcB
and vcA have a tableView, vcB have a button.
vcA click the cell to vcB.
Then I want to click the button in vcB and do the following two things.
1.vcA tableView reloadData.
2.vcB popViewcontroller To vcA.
I can't understand how to solve this issue.
Have any sample to teach me?
Thanks.
This is the delegate solution , but it's better to put the self.tableView.reloadData() method inside viewDidAppear , as it's being called when you pop VcB
class VcA: UIViewController ,TableRefresh {
func reloadTable()
{
// reload here
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let des = segue.destination as! VcB
des.delegate = self
}
}
protocol TableRefresh {
func reloadTable()
}
class VcB: UIViewController {
var delegate: TableRefresh?
#IBAction func closeClicked(_ sender: UIButton) {
self.delegate?.reloadTable()
// pop here
}
}
I hope this could work for you:
class vcA : UIViewController {
override func viewWillAppear(_ animated: Bool) {
tableView.reloadData()
}
}
class vcB : UIViewController {
#IBAction fun button(bin : UIButton){
self.navigationController.popViewController(true)
}
}
It's not possible to call the methods of any VC objects if it's not in your memory.
For the first time, it vcB will not be in memory. Hence the object of the same is not in existence.
If you really want to call a function of that class and if its feasible crate an object of vcB and call the method.
or you might try a shared object of the VC and keep on using the same if it's feasible. you may post exact scenario so that people can suggest something better
Your vcA has a reference to the vcB, so you are able to write something like this in the vcA
class vcA {
var vcB: vcB?
...
vcB?.doSmth()
....
}
but there is no way for you to call vcA from the vcB since it doesn't have a reference to it. So to let vcA know that something happened in vcB or to call some function from vcB you can do several things.
Delegates, Key-Value Observing, Reactive Programming and some others.
Since you asked for the delegates solution lets stick to it.
The general idea behind delegates is as the name says to delegate someone else to do something. In your case, you want to delegate button click handling to the vcA. To do so, you will need a couple of things.
Next steps are just the implementation of the idea described above.
class VcA {
var vcB: VcB?
...
vcB?.delegate = self
...
vcB?.doSmth()
....
}
extension VcA: VcBDelegate {
func buttonIsClicked() {
// reload data
// pop vcB
}
}
protocol VcBDelegate: class {
func buttonIsClicked()
}
class VcB {
var delegate: VcBDelegate?
...
// when the button is clicked
// delegate handling to someone who is 'listening' for this event
self.delegate?.buttonIsClicked()
...
}
Notice how delegate in VcB is optional, meaning that if no one is signed as a delegate for VcB, the event will simply be ignored.
There are multiple ways to do this.
1: Put tableView.reloadData() in viewDidAppear() and then just pop vcB
2: When you are pushing vcB you create a reference to it and then in vcB you have a listener that you apply to the reference. like so:
class viewControllerB: UIViewController {
var reloadTableView: (() -> ())
#objc func buttonPressed() {
reloadTableView()
self.navigationController?.popViewController(animated: true)
}
}
And then:
let vcB = viewControllerB()
vcB.reloadTableView = {
self.tableView.reloadData()
}
present(vcB, animated: true)
3: Do as Sh_Khan

Reload and not reload if press back from different view controllers. Swift

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)])

How can I dismiss a modal segue then perform push segue to new view controller

I'm trying to perform a push segue from a navigation controller, after dismissing a modal that was previously presented over the navigation controller.
Inside modal (UIViewController):
func textFieldShouldReturn(textField: UITextField) -> Bool {
var searchNav = SearchNavigationController()
self.dismissViewControllerAnimated(true, completion: {searchNav.goToNextView()})
return true
}
Inside SearchNavigationController:
func goToNextView() {
performSegueWithIdentifier("searchNavigationToNextView", sender: self)
}
The issue is that when I hit return, I get this error:
Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: 'Receiver
() has no segue with
identifier 'searchNavigationToNextView''
The segue is present in my storyboard, going from the UINavigationController to the next view.
EDIT: tried this, where _sender is declared in SingleSearchViewcontroller as var _sender = self and it did not work.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "searchNavigationToSingleSearch" {
let singleVC = segue.destinationViewController as! SingleSearchViewController
singleVC._sender = self
}
}
Update your code as,
func textFieldShouldReturn(textField: UITextField) -> Bool {
var searchNav = SearchNavigationController()
self.dismissViewControllerAnimated(true, completion: {self.presentingViewController.goToNextView()})
return true
}
I had the same issue that mattgabor had, and i tried Ashish's solution but it didn't work for me.
The problem i had in my code was that inside the completion block of "dismissViewControllerAnimated()", the propery "self.presentingViewController" was already nil when i wanted to call the method from my previous viewController that performed the push (the method "goToNextView()" in this example)
So my solution was just to create a variable to hold the previous viewController before calling "dismissViewControllerAnimated()". Using the current example, it would look like this:
func textFieldShouldReturn(textField: UITextField) -> Bool {
if let searchNav = self.presentingViewController as? SearchNavigationController {
self.dismissViewControllerAnimated(true) {
searchNav.goToNextView()
}
}
return true
}

How do I perform a show detail segue without animation? [duplicate]

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
}

Resources