I have an app with 3 view controllers. A is the root, B is next and C is a modal view that appears / transitions when a button on B is clicked. What I want to do is sometimes move from C to A directly when a button on C is clicked (other times, the C->B transition should happen, which is ok). I've tried various options and incorporated info from different SO posts but in the end, stuck at the scenario where the direct transition from C to A happens but does show B momentarily.
Is there a way I can remove this momentary B appearance altogether. Also, I do need to have animations when the user goes from B to C or from C to B. It is the C to A transition that need not have an animation if there is a limitation there.
Hopefully you can chip in... have copied relevant code and further details below.
ViewControllerC.h:
#protocol ViewControllerCDelegate <NSObject>
- (void)didNavigateBackFromViewC:(ViewControllerC *)viewControllerC;
#end
#interface ViewControllerC : UIViewController
#property (nonatomic, weak) id < ViewControllerCDelegate > viewCDelegate;
#end
ViewControllerC.m:
- (IBAction)navigateToViewA:(id)sender
{
[self.viewCDelegate didNavigateBackFromViewC:self];
}
For the B->C and C->B transitions, I've used a separate transitioning controller that is a delegate of ViewControllerB. Also, ViewControllerB is a delegate of UIViewControllerTransitioningDelegate... I've not included those details for the sake of simplicity here but hopefully am able to convey the problem here about the C->A transition.
ViewControllerB.h:
#interface ViewControllerB () <ViewControllerCDelegate>
#end
ViewControllerB.m:
- (void)didNavigateBackFromViewC:(ViewControllerC *)viewControllerC
{
__block ViewControllerB *me = self;
[viewControllerC dismissViewControllerAnimated:YES completion:^{
[me.navigationController popToRootViewControllerAnimated:YES];
}];
}
really interesting question #vikram17000. First of all, you might want to try unwind segues, it seems that they can do what you want but I haven't tested that. It appered you were going the programmatic route, and here's what i found. I split (what I called) the 'back' and 'home' actions into two separate actions. Here's my ViewControllerC code:
-(IBAction)goBack:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
-(IBAction)goHome:(id)sender
{
[self.delegate ViewControllerCWillGoHome:self];
[self dismissViewControllerAnimated:YES completion:nil];
}
ViewControllerB is the delegate that gets the -...WillGoHome, and it popsToRootVC before VC C dismisses. This gets the behavior we want, C animates out and A is left - yay! Unfortunately there's a catch -
Unbalanced calls to begin/end appearance transitions for ViewControllerB: 0x7fc3c857ca80.
So is it possible? Yes. Does the os like it? No it does not. Hopefully this helps you get going though.
I've created sample project for your question.
There is protocol GoBackProtocol which contains method
-(void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button;
A and B controllers adopt GoBackProtocol. B controller implements -controller:didPressBackButton: method and decides based on button parameter what to do: hide presented controller with animation or hide it without animation and forward call to his delegate, which is controller A.
- (void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button
{
ViewControllerC *modalController = (ViewControllerC *)controller;
if (button == modalController.buttonB)
{
[controller.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
else if (button == modalController.buttonA)
{
[controller.presentingViewController dismissViewControllerAnimated:NO completion:nil];
[self.delegate controller:self didPressBackButton:nil];
}
}
Controller A implements -controller:didPressBackButton: as well and pops viewController B.
- (void)controller:(UIViewController *)controller didPressBackButton:(UIButton *)button
{
[self.navigationController popViewControllerAnimated:YES];
}
All Together it makes desired effect.
Please forget all custom protocol solutions here. Apple says:
The presenting view controller is responsible for dismissing the view controller it presented.
IMO you have at least two options here:
1) You could add the close button on C to public interface like
#property (nonatomic, strong, readonly) UIButton *closeButton;
Set the target differently and use a combination of dismissViewControllerAnimated:completion: and popToRootViewControllerAnimated: as in your method didNavigateBackFromViewC
2) You could use self.navigationController.presentingViewController in C to get access to the non-modal viewcontrollers.
As non-official option you could set the viewcontrollers array on a UINavigationController instance directly. Then you will get a Push Animation from C to A.
I haven't fully testing this out yet but results so far seem encouraging. Given that I wanted to be able to transition to B or C from A, and that B and C could navigate between each other, I've started playing around with making ViewControllerA as the container view controller for ViewControllerB and ViewControllerC. That allows me easy access to B / C from A, as well as the ability to transition between B and C seamlessly using custom transitions.
Related
On iPad simulator, I have a ViewController A that presents an UIPopoverController whose contentViewController is ViewController B, inside which I have a button to dismiss the UIPopoverController.
When it is dismissed, I need to update the view of ViewController A based on some field in ViewController B.
In order to do this, I am declaring ViewController A as a property (weakref) of ViewController B so that within ViewController B where it dismisses the popover, I can say:
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;
self.viewControllerA.textLabel.text = self.someField
Is this the correct way of doing it? Since there is no callback when we dismiss the popover pragmatically, I can't think of any better solution.
Anybody has a better idea? Passing view controllers around just seems awkward to me.
The best way is use of Delegation, just declare the delegate in your controller B like
#protocol ControllerSDelegate <NSObject>
-(void) hidePopoverDelegateMethod;
#end
and call this on action for passing the data and dismiss of controller like
if (_delegate != nil) {
[_delegate hidePopoverDelegateMethod];
}
and
in your controller A you can handle this delegate call
-(void) hidePopoverDelegateMethod {
[self.paymentPopover dismissPopoverAnimated:YES];
if (self.paymentPopover) {
self.paymentPopover = nil;
}
[self initializeData];
}
I think, delegates or sending NSNotification will make better.
Note:
A change of the execution sequence will do more perfection to your current code.
self.viewControllerA.textLabel.text = self.someField
[self.viewControllerA.popover dismissPopoverAnimated:YES];
self.viewControllerA.popover = nil;
in my app, there is a chatting service. when the user wants to message someone new, he press on "new chat" and a Table View Controller is shown to select from the list of friends. i'm using unwind segues in order to go back to the previous view controller and share the data between the two view controllers (mainly the data will be the friend to have a chat with). the unwind segue works perfectly, however in my app, when the user goes back to the main VC, i fire a new segue in order to go to another VC directly where the user has the chat; and this isn't working. All segues are well connected and i tried NsLog in every corner and it's entering there and even the prepareForSegue: is being accessed. i tried putting an NsTimer, thought there might be some technical conflict, and it didn't work. i change the segue to the chat VC to a modal and now it's giving me this error:
Warning: Attempt to present on whose view is not in the window hierarchy!
i can access this VC in other ways and it's in the hierarchy. my questions are: what could be wrong? does unwind segues alter the windows hierarchies ?
PICTURE:
to present the problem more visually, the main VC i'm talking about is on the bottom left connected to a navigation view controller. when a user presses new chat, the two VC on the top right are presented (one to choose between friends or group, the other to show the friends/ groups). so when a friend is selected for say from the top right VC i should unwind segue to the main VC. as you can see from the main VC there is other segues. non of them can work if i do unwind segue and they do work if i operate normally.
The reason it is not working and is giving you that error is because things arent happening in the order you think they are.
When the unwind happens the view which is visible is not dismissed yet, therefore you are trying to perform a segue on a view which is in fact not in that hierarchy like the error says, take this for example, placing NSLog statements in the final view and then in the unwind method in your main view controller you can see the following:
2013-11-27 14:51:10.848 testUnwindProj[2216:70b] Unwind
2013-11-27 14:51:10.849 testUnwindProj[2216:70b] Will Appear
2013-11-27 14:51:11.361 testUnwindProj[2216:70b] View Did Disappear
Thus the unwind in the main view is getting called, the view will appear (your main view controller), and then your visible view is dismissed. This could be a simple fix:
#interface ViewController () {
BOOL _unwindExecuted;
}
#end
#implementation ViewController
- (void)viewWillAppear:(BOOL)animated
{
NSLog(#"Will Appear");
if (_unwindExecuted) {
_unwindExecuted = NO;
[self performSegueWithIdentifier:#"afterunwind" sender:self];
}
}
- (IBAction)unwind:(UIStoryboardSegue *)segue
{
NSLog(#"Unwind");
_unwindExecuted = YES;
}
Don't use timers or delays to try and anticipate when a view may exist.
Instead, use calls like: - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
The completion block will let you know when you've arrived back at the main VC. Alternatively, look at the various calls associated with segues so that you know precisely when you can perform operations on the new window.
If all else fails, there's always UIViewController viewDidAppear.
This is a common problem for the view controller that is handling the unwinding, because during unwind, that view controller is likely to not be in the window hierarchy.
To solve, I added a property segueIdentifierToUnwindTo to coordinate the unwinding.
This is similar to the answer by JAManfredi, but extending it to be able to segue to any view controllers.
#interface FirstViewController ()
// Use this to coordinate unwind
#property (nonatomic, strong) NSString *segueIdentifierToUnwindTo;
#end
#implementation FirstViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Handle unwind
if (_segueIdentifierToUnwindTo) {
[self performSegueWithIdentifier:_segueIdentifierToUnwindTo sender:self];
self.segueIdentifierToUnwindTo = nil; // reset
return;
}
// Any other code..
}
// Example of an unwind segue "gotoLogin"
- (IBAction)gotoLogin:(UIStoryboardSegue*)sender {
// Don't perform segue directly here, because in unwind, this view is not in window hierarchy yet!
// [self performSegueWithIdentifier:#"Login" sender:self];
self.segueIdentifierToUnwindTo = #"Login";
}
#end
I also shared how I use unwind segues with a FirstViewController on my blog: http://samwize.com/2015/04/23/guide-to-using-unwind-segues/
okay solved this, the problem is that the view wasn't there yet and i had to delay the process by this:
[self performSelector:#selector(fireNewConv) withObject:nil afterDelay:1];
However, something interesting is that i tried delaying by NSTimer but it didn't work.
I have a VC named Dashboard (D) which can open a VC named Login (L) and a VC named Register (R). Login can open VC Register too.
I try to use storyboard as often as possible, so I have created with it three Segues, D to L, D to R, L to R
So, in case of D -> L -> R and in case of D -> R, when I close R, I have to close L if it necessary and inform D which he can begin to load the user infos (launch function in nutshell).
So, I would like get the sender of Segue in destination vc, knowing that I put it in sender entrie of performSegueWithIdentifier like that :
[self performSegueWithIdentifier:#"SegueToFbRegister" sender:self];
I'd do this by having R send a notification when the registration/login is done, and having D listen to it then pop everything and load your data.
If however you insist on getting a reference to the sender, you can add this property on your destination VC and set it in the source VC's prepareForSegue:sender:
This sounds like a great place to use Delegates. In your RegisterViewController.h define a protocol like this
#protocol RegisterViewDelegate <NSObject>
- (void)tellRegisterDelegateSomething:(NSObject*)something;
#end
Then on your class keep a pointer to your delegate
#interface RegisterViewController : UIViewController
#property (weak, nonatomic) id <RegisterViewDelegate> delegate;
#end
Now tell the presenting view controllers that they implement the new protocol you just created. This is done in the .h files of the other viewcontrollers that present this view.
In LoginViewController.h
#interface LoginViewController : UIViewController <RegisterViewDelegate>
#end
In DashboardViewController.h
#interface DashboardViewController : UIViewController <RegisterViewDelegate>
#end
In the .m files of the above classes, implement the protocol's method
- (void)tellRegisterDelegateSomething:(NSObject*)something
{
}
Now you need to assign the delegate when you perform your segue from either presenting view controller like this.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"SegueToFbRegister"])
{
RegisterViewController* destination = [segue destinationViewController];
destination.delegate = self;
}
}
Now you can call the presenting view controller (delegate) and have it do something with any information you need to send back like this (this would be called in your RegisterViewController.m).
if ([self.delegate respondsToSelector:#selector(tellRegisterDelegateSomething:)])
{
// Tell the delegate something.
[self.delegate tellRegisterDelegateSomething:something];
}
The instance where you need to pass back through two controller you follow the same basic pattern.
#protocol LoginViewDelegate <NSObject>
- (void)tellLoginDelegateSomething:(NSObject*)something;
#end
Then on your class keep a pointer to your delegate
#interface LoginViewController : UIViewController
#property (weak, nonatomic) id <LoginViewDelegate> delegate;
#end
Now tell the Dashboard view controller that it implements the protocol. This is done in the .h files of the Dashboard viewcontrollers that present this view.
In DashboardViewController.h
#interface DashboardViewController : UIViewController <RegisterViewDelegate, LoginViewDelegate>
#end
In the .m files of the DashboardViewController implement the protocol's method
Follow the above pattern of setting the delegate on the viewcontroller when you perform the segue. Now when the delegate method is called in the LoginViewController, you call the delegate in the DashboardViewController as well.
in LoginViewController.m
- (void)tellRegisterDelegateSomething:(NSObject*)something
{
if ([self.delegate respondsToSelector:#selector(tellLoginDelegateSomething:)])
{
// Tell the delegate something.
[self.delegate tellLoginDelegateSomething:something];
}
}
Now you are all connected so you can pass data back through both controllers (or just one) and do something with it. You will know which scenario you are in because different delegate methods will be called in the DashboardViewController based on which viewcontroller was visible.
Hope this helps.
Create a delegate for R and make D and L to implement the delegate methods.Use prepareForSegue:sender to assign the delegate of R.When you finish task in R use your delegate to perform the rquired action.
Another way would be to use an unwind segue.
Place the following code in you Dashboard (D) view controller.
#IBAction func loadUserInfoAfterRegistration(segue: UIStoryboardSegue) {
}
In Interface Builder, do the following steps for the Register (R) view controller:
Select the button that will be pressed on the completion of registration.
Ctrl + drag to the exit symbol on top of the view.
Select loadUserInfoAfterRegistrationWithSegue: from the list displayed.
Using this approach, the Register (R) view controller will always navigate to the Dashboard (D) view controller, regardless of what is between them. The view controllers between them will not have to be touched. The loading of the user data in the Dashboard (D) view controller can also be customized in the method declared above.
You can get it easily by using the type of the parent controller such as
let temp = self.navigationController?.viewControllers
if (temp != nil){
if let _parent = temp![temp!.count-2] as? UIControllerClass {
//do what you want here with the _parent
}
}
You have to subtract 2 because last one is the current view that you want to get its parent.
using Instrument after my migration to ARC I realise that the transition from screens does not clean memory.
example of steps :
1)Home screen A -> game screen B = rising of memory usage
2)Game is finished and I go from screen B back to Home screen A
For step 2, memory usage does not go lower. I'd like to have the memory consumed by screen B being freed when removing screen B from screen ... What should I do to be sure this freeing to happen ?
Going from A to B :
GameVC_iPad *game = [[GameVC_iPad alloc]initWithNibName:#"ClassicGameVC_iPad" bundle:nil];
[self presentViewController:game animated:YES completion:nil];
Going back to A from B is done using this code :
HomeVC_iPad *home = [[HomeVC_iPad alloc]initWithNibName:#"HomeVC_iPad" bundle:nil];
home.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:home animated:YES completion:nil];
Any clue ?
When you go back to A you should do
[self dismissModalViewControllerAnimated:YES];
What you are currently doing is creating a new view controller wich is wrong and navigate to it another time, so this is what is happening
A presents B wich then you present a new A wich then presents a new B ans so on...
Also note that when you navigate to a new viewController iOS caches some view data, so you will never be able to achieve a perfect memory usage before and after you went back,
Don't create a new copy of your home controller. Use dismissViewControllerAnimated:completion: to return to the existing one.
If you create a delegate class for b, so lets say for example you called it BDelegate and made the ViewController for A conform to that protocol, then you can easily pass a message back to A that you want B to be removed. So for example you could create :
BDelegate :
#protocol BDelegate <NSObject>
- (void)dismissViewB;
#end
Then change view controller for A (header file) to :
#interface AViewController : UIViewController <BDelegate>
Obviously using the actual name of your view controller in there.
In the body of the view controller A, add the following method
- (void)dismissViewB {
[self dismissViewControllerAnimated:YES completion:NULL];
}
Almost there! Now in your B view controller, wherever you want to actually remove the view, so I assume where you currently have
HomeVC_iPad *home = [[HomeVC_iPad alloc]initWithNibName:#"HomeVC_iPad" bundle:nil];
home.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentViewController:home animated:YES completion:nil];
Replace that with
[delegate dismissViewB];
Now all you need inside view controller B is an instance variable pointing to the delegate of A and to assign it. So in the header of controller B add something like
NSObject<BDelegate> *delegate;
And add the appropriate #property for it and #synthesise it in the body. Then when you create view controller B as you are in your first post, simply add
game.delegate = self
Then if it's all gone well, when you tap the button or do whatever you need to do to remove the view, the view controller A will dismiss it for you :)
Hope this helps
I'm trying to implement an iBooks-like flip transition as a storyboard. The segue should push resp. pop the destinationViewController onto/from the UINavigationControllers stack.
I can push viewcontrollers in my segues perform method but I am not able to pop. When I pop the controller right after creating my flip animation the animation does not run and its callback - that should perform [[UIApplication sharedApplication] endIgnoringInteractionEvents] gets never called and my App results dead.
So I tried to push/pop in the animationDidStop:anim:flag delegate method but it never gets called with the flag set to true.
I assume that the segue is deallocated before the delegate method gets called. What else could I do?
Forgive me if I am completely misunderstanding this question, but it seems like you just want to do a basic horizontal flip back and forth between two view controllers. And even if you've already figured this out, maybe it will help anyone else who has the same question.
(1) In your storyboard (that has ViewController A & B) create a Modal Segue from A to B. Give it an identifier (showViewControllerB) and choose Transition:Flip Horizontal.
We set up the protocol and delegates:
(2a) In ViewControllerB.h add above #interface:
#class ViewControllerB;
#protocol ViewControllerBDelegate
- (void)viewControllerBDidFinish:(ViewControllerB *)controller;
#end
(2b) Add the delegate as a property:
#property (weak, nonatomic) id <ViewControllerBDelegate> delegate;
(3a) In ViewControllerB.m synthesize:
#synthesize delegate;
(3b) And delegate in the method to flip back:
- (IBAction)flipBack:(id)sender
{
[self.delegate viewControllerBDidFinish:self];
}
(4) In ViewControllerA.h add at the very top #import "ViewControllerB.h" and on the end of #interface <ViewControllerBDelegate>
(5a) In ViewControllerA.m add the method to conform to the protocol:
- (void)viewControllerBDidFinish:(ViewControllerB *)controller
{
[self dismissModalViewControllerAnimated:YES];
}
(5b) Then set it as the delegate in prepareForSegue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"showViewControllerB"]) {
[[segue destinationViewController] setDelegate:self];
}
}
I hope this answers your question. If I misunderstood, just let me know.
Your question is a bit confusing as so mingle pop, push, flip and backflip. I´m not sure if ai can answer your question, but i can tell what i did.
If i push a viewController into the navigation controller stack and set the Storyboard Segue Style to Push, it will be pushed into the view from right to left. A back button appears and shows the title of the presentingViewController in it.
If i set the Storyboard Segue Style to Modal i can set the Transition to Flip Horizontal (what seems to be what you want). But then no Back Button will appear. In the presentedViewController i dismiss the view with:
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
It will flip the second view back with a right flip.
But this is the dirty solution and it is not recommended by apple.
http://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ManagingDataFlowBetweenViewControllers/ManagingDataFlowBetweenViewControllers.html#//apple_ref/doc/uid/TP40007457-CH8-SW9
Luke Dubert gave you an example how to implement the delegate.