Custom UIStoryboardSegue using segueWithIdentifier:source:destination:performHandler: - ios

I am trying to use a custom UIStoryboardSegue to implement a transition between two view controllers. I can do this by subclassing UIStoryboardSegue, and then setting this class in IB. However, I was looking at the docs which say:
If your segue does not need to store additional information or provide anything other than a perform method, consider using the segueWithIdentifier:source:destination:performHandler: method instead.
Implying that you don't need to create the custom subclass, just use the custom performHandler.
I am confused as to where this code should go, and how I go about using it. Do I create the segue as normal in IB and then override that before it is fired (maybe in shouldPerformSegue: or similar). Elsewhere in apple's documentation it says:
Your app never creates segue objects directly; they are always created on your behalf by iOS when a segue is triggered
So I don't quite understand why they are then saying to instantiate a segue using a class creator method.

The point of segueWithIdentifier:source:destination:performHandler:
Provide an alternative to UIViewController performSegueWithIdentifier:sender in cases where you also want to create a custom transition, without creating a segue subclass.
Vend a segue that can be used as the return for segueForUnwindingToViewController:fromViewController:identifier
As noted above, this approach is only viable for segues which you would call manually -- i.e. not for segues that would otherwise be triggered via IB triggers.
So, for example, if you have a segue that needs to be triggered after a certain timeout period (such as a custom lock-screen), you could use segueWithIdentifier:source:destination:performHandler: to handle the custom transition.
-(void)appTimeoutLockScreen
{
UIStoryboardSegue *segue =
[UIStoryboardSegue segueWithIdentifier:#"LockScreenSegue"
source:sourceVC
destination:destinationVC
performHandler:^{
// transition code that would
// normally go in the perform method
}];
// Dev is responsible for calling prepareForSegue and perform.
// Note, the order of calls for an IB triggered segue as well as
// a performSegueWithIdentifier segue is perform first, then
// prepareForSegue:sender. Manual segues need to inverse the call
// in order to ensure VC setup is finished before transition.
[self prepareForSegue:segue sender:self];
[segue perform];
}
Another practical use for the method is unwinding segues. Using a similar scenario
to the previous example, we could use it to return a segue to transition from a lock screen back to the previous viewController:
-(UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController*)toVC
fromViewController:(UIViewController *)fmVC
identifier:(NSString *)identifier
{
UIStoryboardSegue *segue =
[UIStoryboardSegue segueWithIdentifier:#"FromLockScreenSegue"
source:fmVC
destination:toVC
performHandler:^{
// transition code
}];
return segue;
}

Related

Initialising a ViewController before Segue

I am performing a segue to a view controller that requires an initialisation process. At present the initialisation happens in ViewDidLoad of the target view controller. However the initialisation is fairly lengthy and I would like to show a spinner while it is happening.
If I create a UIActivityIndicatorView within ViewDidLoad and run the initialisation on another thread, of course ViewDidLoad exits and the rest of the loading process happens - in particular shouldAutorotate is called, and this contains code that assumes the initialisation process has occurred. (Even if it didn't, I do not want to show the target view before it has been initialised.)
The answer seems to be to initialise the target view controller before calling the segue. However I can't do that in prepareForSegue in the calling view controller, for the same reason - it exits and the segue is called before the initialisation has happened.
So I seem to need to instantiate the target controller, initialise it and then perform the segue with the initialised controller as the destination. My problem is that I don't know how to do that. The only possible way I have come across is to subclass UIStoryboardSegue and put the initialisation in the init for the subclass. Then I presume I call
UIStoryboardSegue * segue = [[SubclassedSegue alloc]initWithIdentifier:#"??what should this be??" source:self destination:targetViewContoller];
[segue perform]; // which just calls [super perform];
from the source view controller. Is this correct? Can anyone please show me some example code that uses this process - or preferably a simpler way that I haven't thought of? I can't help thinking there must be an easier way to show a spinner.
Thank you for your help.
Segues should be subclassed only when you need to show custom animation/transition during the segue.
In usual scenario, you would want to do this:
__ block Destination *destinationVC = [self.storyboard instantiateViewControllerWithIdentifier:#"destination "];
//START BUSY CURSOR HERE
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
// init whatever you want for destinationVC HERE.
dispatch_async(dispatch_get_main_queue(),
^{
//STOP BUSY CURSOR
//PERFORM UI UPDATE HERE
[self presentViewController:destinationVC animated:YES completion:nil];
});
just have the destinationViewController always add an activityIndicator and in viewWillAppear .. just hide it if you don't need it anymore
Alternatively pass a flag to the destinationViewController in prepareForSegue.
You shouldn't need to subclass UISegue!

How prepareForSegue works?

I am curious about this method
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender;
I find this method in UIViewController.h, and it's a regular method.
I check it, it is not a delegate method (it is not a protocol).
Because UITableViewController extends from UIViewController, we can use this method in our custom class for any table views.
Strangely, this method behaves like a delegate, it fires without any calls.
It fires when the view is going to do a transition.
As far as I know, this behaviour only exist in delegation.
How can a UIViewController calls prepareForSegue method in it's child class?
prepareForSegue is a method that is called as and when a transition is going to occur between ViewControllers. So, whenever a segue is made, this method will be called mandatorily. Now if your main View has a subclassed View and you want to perform a Segue from it, you can do it using the following function.
[self performSegueWithIdentifier:#"segueIdentifier" sender:self];
In this case, this method will invoke a transition with the specified identifier which corresponds to a segue. And whenever this method is called, prepareForSegue is called immediately after this, after which transition occurs.
For information, it is better to connect segue between ViewControllers rather than creating segue directly from Controls in the view. As sometimes, some conditions need to be checked before performing a Segue.
Hope this helps.
Like many other methods of UIViewController, your subclass can override the default behavior of many UIViewController methods. A couple of examples include viewDidLoad and viewWillAppear.

Return values to presenting view controller when navigation back button pressed

I'm having trouble piecing this all together. I have a view controller that opens up another (pushes it on to the navigation stack). On that presented view controller, the user enters a value in a text view. When the user pushes the back button in the navigation, I want to be able to pass the value that they entered in the text view back to the presenting controller.
I've looked for a way to use unwind segue with the back button but haven't found anything. When I create my back button (programmatically) I use initWithTitle:style:target:action but I'm not sure how in implementing the action method that I'll be able to access the value set in the presented controller. Might have to use a delegate to link the two, but not sure of the exact integration point for this scenario.
I feel like I'm so close here and a little help would get me there. Thanks!
The two most common models to use for this interaction are for the child view controller to have either a delegate or a completion block. Either would be set in the prepareForSegue method. My personal preference is the completion block method just because it keeps code contained, but ymmv.
There are also multiple models for detecting when your child view controller is dismissed and you need to invoke the delegate and/or completion:
Use a custom back button. Not a fan of this as it can be an issue to create a back button that really looks and acts like the Apple original, especially if supporting iOS 6 and iOS 7.
Hook viewDidDisappear and see if you're still in the navigation controller's viewControllers array. This is better as the back button works right, but it still feels kind of hokey.
Use the UINavigationBarDelegate method navigationBar:shouldPopItem: This is attractive, especially if you have other validation that needs to happen like checking for saved/unsaved values. To implement this you'll have to subclass UINavigationController and forward the method to your child view controller.
EDIT: Details on Option 2:
-(void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
if(![self.navigationController.viewControllers containsObject:self])
{
// We're not still in the navigation stack so we must've been
// popped. If we were pushed, viewDidDisappear would be called
// but viewControllers containsObject:self would be true
}
}
EDIT: Clarified Option 3: in your navigation controller subclass
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
UIViewController* top = self.topViewController;
if([top respondsToSelector:#selector(navigationBar:shouldPopItem:)])
return [(id)top navigationBar:navigationBar shouldPopItem:item];
return [super navigationBar:navigationBar shouldPopItem:item];
}
Then you can implement navigationBar:shouldPopItem: in the classes that need the functionality.
the back button does not actually comes up with any event associated with itself so that you can pass the values between the previous and to be Popped ViewController.
You would have to implement Delegate pattern to pass values. In this case as you cant catch when backButton is pressed, you need to use custom leftBarButtonItem or use a image with < in itself.

Drawing in segue vs writing code to transition views - why does data sometimes not transfer?

Let's say I have a UILabel on ViewControllerA and and UITextField on ViewControllerB. I want to go to ViewControllerB and input text then press a button to go back to ViewControllerA. The UILabel should now read whatever was typed in the UITextField.
I was able to accomplish the above by using NSUserDefaults and also using delegation. I am using Storyboards to do this. My question is about the segues used in the storyboards.
It seems when using delegation I must go to and from the storyboard with code and not visually connect the view controllers with a segue in order for the data to transfer. Here is the code when I press a button on my ViewController A:
- (IBAction)pressFirstButton:(id)sender {
UIStoryboard* sb = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
RBViewController2 *vc2 = [sb instantiateViewControllerWithIdentifier:#"ViewController2"];
[vc2 setDelegate:self];
[self presentViewController:vc2 animated:YES completion:NULL];
}
when passing the data back from ViewControllerB to ViewControllerA I do this:
- (IBAction)buttonSegueBackTo1:(id)sender {
NSString *sendThis = self.textFieldVC2.text;
[self.delegate passTextFieldInput:sendThis];
[self dismissViewControllerAnimated:YES completion:NULL];
}
No segue has been drawn between the view controllers and everything works fine. If I don't write this same code, and draw in a segue, the data won't pass backwards. However when I try passing data like this using NSUserDefaults I don't have to write the code to go to and from the view controllers. Instead I can simply connect the view controllers with a drawn segue. The weird thing is, if I'm trying to pass the data in using NSUserDefaults when manually coding the view controllers (an not drawing the segue) the data doesn't transfer.
I'm thinking maybe instead of writing the code in the -(IBAction) pressFirstButton:(id) sender method, I should be putting the code in the prepareForSegue method.
My question is why do drawn segues sometime cause data to be lost? Must all delegation be done without drawn segues? If NSUserDefaults require segues to transfer properly and delegation require code to transfer properly then if I have a view that requires both, it seems that NSUserDefaults will trump the delegation b/c the manual segue being used "resets" the view and only the NSUserDefaults data remains.
Normal segues (any other than unwind segues) ALWAYS create new view controllers. So, if you're using anything other than an unwind segue to go back to A, you're really not "going back", you're creating a new ViewControllerA. Unwind segues aren't normally used in a case like you're presenting, just going back one controller, but you could.
This situation also isn't a good place to use user defaults. The Apple recommended way, is to use delegation, like you do in the code your question. The way you show, is probably the best way to do it, rather than using a segue to go back. You certainly could use a segue to go forward though, and in that case you would implement prepareForSegue: so you can set yourself as the delegate and/or pass any data forward.
Your question is a little dense to parse but I think you are asking if there is a way to get pass data through segues. The answer is yes, and much better than the workarounds you are trying.
In your button you would call:
[self performSegueWithIdentifier:#"scrollerSegue" sender:self];
This will trigger the method below and it is here that you can set the data on the destination viewController. You can set abstract properties on the destination viewController but you can't populate an UIKit elements (labels, imageViews, etc.) because they don't exist yet. Instead set properties and then in viewWillAppear in the destination viewController, do the set up as needed. (alternatively, instead of passing data, you could just set the delegate and then call methods on the delegate to get the data as needed).
For getting data back, using the delegate and calling methods on it seems to be the Apple recommended way of doing things.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"scrollerSegue"])
{
ScrollViewController * target = segue.destinationViewController;
target.assetsArray = self.assetsArray;
target.delegate = self;
}
}

Understanding performSegueWithIdentifier

Can someone more knowledgeable than I explain performSegueWithIdentifier:sender: for me? I need to switch views (and classes) and also carry a few NSStrings and IDs over to that view's class. I was wondering if this is possible with performSegueWithIdentifier:sender:
Thanks!
First, you have to have set up the segue in your storyboard and give it the appropriate identifier. (Click on the segue (left panel) and then click on Attributes (right panel).
You can then link this to buttons or selection of table rows from your storyboard, or you can call it in code using performSegueWithIdentifier:sender:.
After this, your view controller will be sent the prepareForSegue:sender: message. You override this method in your view controller subclass, and can configure the target view controller as follows:
TargetViewController *targetVC = (TargetViewController*)segue.destinationViewController;
targetVC.string1 = string1;
And so forth. The sender in this method will be the object that you use as the sender in the original method call.
Most segues are initiated automatically as the result of some user interaction. For instance, if you have a segue that is wired up from a button to a scene in a storyboard, when the button is tapped the segue will automatically initiate.
Occasionally, it makes sense to trigger a segue programmatically - e.g. you have a High Scores scene that is displayed when the user wins a round of a game. There's no way to express the concept of winning in the storyboard itself, so you can instead create a segue, assign an identifier to it, and invoke -performSegueWithIdentifier:sender: at runtime.
The other segue related method on UIViewController, -prepareForSegue:sender:, is the method you should override to perform any customization on the destination view controller.
In prepareForSegue:sender: you get a chance to configure the destinationViewController: that's where you'd pass it the data it needs. It's discussed in Cocoa Application Competencies for iOS.
Today I ran into the issue of performSegueWithIdentifier: not executing due to the fact of not having set a delegate queue on my URL session.
So by any chance, check if you are actually setting a delegate queue when creating your URLSession, else URLSession will create it's own.
urlSession = [NSURLSession sessionWithConfiguration:sessionConfigObject
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
I mention this here because I quite often see URLSession handling ending up calling some sort of UI related activity. And performSegue needs to be executed on main, or else it will do just nothing.

Resources