App crashes if view controller transition is done too quickly - objective-c

I am using view controller containment to transition between 6 view controllers. Transitions are controlled using a segmented control. This all works fine unless buttons on the segmented control are pushed before animation of the previous transition has completed. In this situation, the app crashes with
'Children view controllers and must have a common parent view controller when calling -[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:]'
Code is:
[self transitionFromViewController:currentVC
toViewController:newVC
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromRight
animations:nil
completion:^(BOOL finished) {
[currentVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
currentVC = newVC;
}];
Should I disable the segmented control until animation is completed? Or is their a better way to avoid this issue?

You can disable and reenable application interaction by calling
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
when the animation starts and ends respectively. The app will then ignore all interaction (touch events) until the animation is finished, so the segment will never receive event before it is safe (animations are done).
I think this approach is used on some built-in container controllers as well. However be careful about animation duration. If the animation will take a long time, it can look like the app is not responding well, which hurts user experience

Related

How to dismiss 3 modal view controllers at once?

I have an app that has an initial login screen then when the user wants to sign up, they are presented with a registration form that is three view controllers presented modally. When the user completes the form on the third screen (by pressing a "Done" button), I want the user to be taken back to the initial login screen.
I have tried doing this in the third view controller:
[self dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController dismissViewControllerAnimated:NO completion:nil]
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil]
However it only dismissed two of the view controllers and not all 3. Why did this happen?
As other people pointed out, there are more elegant/efficient/easier ways to achieve similar results from the UX perspective: via a navigation controller, or a page view controller, or other container.
Short/quick answer: you need to go one step further in the chain of presenting view controllers, because the dismissal request needs to be sent to the controller that's presenting, and not to the one that's being presented. And you can send the dismiss request to that controller only, it will take care of popping from the stack the child controllers.
UIViewController *ctrl = self.presentingViewController.presentingViewController.presentingViewController;
[ctrl dismissViewControllerAnimated:NO completion:nil]
To explain why, and hopefully help other people better understand the controller presenting logic in iOS, below you can find are more details.
Let's start from Apple documentation on dismissViewControllerAnimated:completion:
Dismisses the view controller that was presented modally by the view controller.
The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal.
Thus [self dismissViewControllerAnimated:NO completion:nil] simply forwarded the request to self.presentingViewController. Which means the first two lines had the same effect (actually the 2nd line did nothing as there was no presented controller after the 1st one executed).
This is why your dismissal of view controllers worked only the top 2 ones. You should've start with self.presentingViewController and go along the chain of presenting view controllers. But this is not very elegant and can cause problems if later on the hierarchy of view controllers changes.
Continuing to read on the documentation, we stumble upon this:
If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack.
So you needn't call dismissViewControllerAnimated:completion: three times, a call on the controller that you want to come back will suffice. At this point, passing a reference to that controller would be more reliable than navigating through the stack of view controllers.
There are some more useful details in the documentation, for example regarding what transitions apply when dismissing multiple controllers at once.
I recommend you go through the whole documentation, not only for this method, but for all methods/classes that you use in your application. You'll likely discover things that will make your life easier.
And if you don't have the time to read all Apple's documentation on UIKit, you can read it when you run into problems, like in this case with dismissViewControllerAnimated:completion: not working as you thought it would.
As a closing note, there are some more subtle issues with your approach, as the actual dismissal takes place in another runloop cycle, as it's possible to generate console warnings and not behave as expected. This is why further actions regarding presenting/dismissing other controllers should be done in the completion block, to give a change to UIKit to finish updating its internal state.
Totally understood. What I will do is embed a navigation controller instead of using modal. I have a case just like you. I have LoginViewController to be the root view controller of the UINavigationController. SignupViewController will be presented by push method. For ResetPasswordViewController, I will use modal because it's supposed to go back to LoginViewController no matter the results. Then, you can dismiss the whole UINavigationController from SignupViewController or LoginViewController.
Second approach will be like, you come up with your own mechanism to reference the presented UIViewController via a shared instance. Then, you can easily dismiss it. Be careful with the memory management. After dismissing it, you should consider whether you need to nil it right away.
I know three ways to dismiss several viewControllers:
Use a chain of completion blocks
~
UIViewController *theVC = self.presentingViewController;
UIViewController *theOtherVC = theVC.presentingViewController;
[self dismissViewControllerAnimated:NO
completion:^
{
[theVC dismissViewControllerAnimated:NO
completion:^
{
[theOtherVC dismissViewControllerAnimated:NO completion:nil];
}];
}];
Use 'viewWillAppear:' method of viewControllers
~
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (self.shouldDismiss)
{
CustomVC *theVC = (id)self.presentingViewController;
theVC.shouldDismiss = YES;
[self dismissViewControllerAnimated:NO completion:nil];
}
}
Pass a reference to LoginVC1 further down the chain.
(This is the best approach so far)
Imagine you have some StandardVC, which presented LoginVC1.
Then, LoginVC1 presented LoginVC2.
Then, LoginVC2 presented LoginVC3.
An easy way of doing what you want would be to call (from inside your LoginVC3.m file)
[myLoginVC1 dismissViewControllerAnimated:YES completion:nil];
In this case your LoginVC1 would lose its strong reference (from StandardVC), which means that both LoginVC2 and LoginVC3 would also be deallocated.
So, all you need to do is let your LoginVC3 know that LoginVC1 exists.
If you don't want to pass a reference of LoginVC1, you can use:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:NO completion:nil];
However, the above approaches are NOT the correct ways of doing what you want to do.
I would recommend you doing the following:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
if (!self.isUserLoggedIn)
{
[UIApplication sharedApplication].keyWindow.rootViewController = self.myLoginVC;
}
return YES;
}
Then, when user finished his login process, you can use
[UIApplication sharedApplication].keyWindow.rootViewController = self.myUsualStartVC;

setStatusBarOrientation:animated: not working in iOS 6

I've used this code to force an orientation change back to portrait when the user is finished watching the video (it allows viewing in landscape mode), before popping the video view controller off the navigation controller:
//set statusbar to the desired rotation position
[[UIApplication sharedApplication] setStatusBarOrientation:UIDeviceOrientationPortrait animated:NO];
//present/dismiss viewcontroller in order to activate rotating.
UIViewController *mVC = [[[UIViewController alloc] init] autorelease];
[self presentModalViewController:mVC animated:NO];
[self dismissModalViewControllerAnimated:NO];
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
This worked perfectly until iOS 5.1.1. I've even tried to use the new present/dismiss methods after reading in another post that those should be used now:
[self presentViewController:mVC animated:NO completion:NULL];
[self dismissViewControllerAnimated:NO completion:NULL];
The problem is it doesn't work at all. After I rotated the video viewer to landscape and then pop it, my settings view (table view controller) comes back, but also in landscape mode.
I've even tried the tip from Here
"The setStatusBarOrientation:animated: method is not deprecated outright. However it now works only if the supportedInterfaceOrientations method of the topmost full screen view controller returns 0. This puts the responsibility of ensuring that the status bar orientation is consistent into the hands of the caller."
So I've experimented with setting a flag to force supportedInterfaceOrientations to return 0 (before calling the first code block above) but it doesn't work either.
Does anybody have a solution for this?
Thanks for your time and effort.
setStatusBarOrientation method has changed behaviour a bit. According to Apple documentation:
The setStatusBarOrientation:animated: method is not deprecated
outright. It now works only if the supportedInterfaceOrientations
method of the top-most full-screen view controller returns 0
Your root view controller should answer false to the method shouldAutorotate in order that your app responds to setStatusBarOrientation:animated
From Apple Documentation: "if your application has rotatable window content, however, you should not arbitrarily set status-bar orientation using this method"
To understand that, put a breakpoint in the shouldAutorotate method and you will see that it is called juste after setting the status bar orientation.
Here is how I fixed.
https://stackoverflow.com/a/14530123/1901733
The current question is linked with the question from the url above.
The statusBarOrientation is a real problem in ios6.

With Modal Partial Curl, I want to reverse the animations. How would I do this?

With the default presentation modal partial curl segue from one view to another, I would like to reverse the animations. So that, instead of lifting a page to reveal the second view, a page falls down on top of the first view and reveals the second view. Then, when the user is done with the second view, the page lifts up to reveal the first view again.
How on earth would I do this? Make the animations myself using a custom transition somehow?
What you are after is UIView tradition with animation.
[UIView transitionFromView:youBaseView
toView:modalView
duration:3.0
options:UIViewAnimationOptionTransitionCurlDown
completion:^(BOOL finished) {
}];
This will get you effect you described. I think quickest and easiest way to get this done is custom segue. Just subclass UIStoryboardSegue. This post talks about it in detail : How to create custom modal segue in 4.2 Xcode using storyboard
Then for dismissing modal view do something like:
[UIView transitionFromView:modalView
toView:yourBaseView
duration:3.0
options:UIViewAnimationOptionTransitionCurlDown
completion:^(BOOL finished) {
[self dismissModalViewControllerAnimated:NO];
}];
Hope this will help.

iPad Orientation issues when switching between two views and from portrait to landscape vice versa

I am building a simple app with two View Controllers, I am testing the code using the iPhone Simulator, everything seem to be working fine. The problem happens when I rotate from Portrait to Landscape or from Landscape to portrait. This is the logic of the app, the app always launched in Portrait, I have a button to on the first View to Switch from View1 to View2. On View2 I have another button to switch from View2 back to View1. Let say, I am in Portrait mode, I switch from View1 to View2, then rotate the iPad (in the simulator) from Portrait to Landscape, when I switch back from View2, i.e to go back to View1. View1 screen/view is displayed in Portrait with View2 screen displayed in the background, ie part of View2 is displayed in the background, I guess because View1 was originally in Portrait mode.
The question is.. Has anyone had this issue before, if so, any code to fix this issue, secondly, how can I identify in the code which orientation the device is and which orientation the view is in.
This method is to switch to View 2:
-(IBAction) switchToView2: (id) sender {
SecondViewController *myViewController = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
[self.view addSubview: myViewController.view];
[UIView commitAnimations];
}
This method is to switch back to View1:
-(IBAction) switchBackToView1:(id) sender {
[self.view removeFromSuperview];
[UIView commitAnimations];
}
From your code:
[self.view addSubview: myViewController.view];
It makes me believe that your 2 views is a subview of myViewController.view. which explains why they're both showing at the same time. it would make sense to have seperate view controllers for different views.
First i think
[UIView commitAnimations];
is not necessary :
From Apple
commitAnimations
Marks the end of a begin/commit animation block and schedules the animations for execution.
second in Interface Builder have you set the property of your controller to landscape ?
Hope this Help (sorry for my bad English)
Yes, that would happen if you dont put any orientation change handling in your code. Check out this guide on how you can set your view to automatically or manually handle adjustment on orientation change.
Or if you do have handling already, then it may be because of how you are adding/removing your views. For better handling, I think you should try the UINavigationController way of managing views. i.e., instead of addSubview: and removeFromSuperview:, you should use pushViewController:animated: and popViewController:animated: instead.
And yes, as Yoos said, [UIView commitAnimations] is not needed in your code above.
Hope this helps.

What is the best method for creating a layer to catch touch events?

I need to put up an info screen above the main interface, but I need it to be alpha'd so you can see the interface underneath it. However, when I touch across the screen, the interface underneath is still running.
What is the best method for intercepting the touch events so they don't pass through? I tried to add a custom UIButton the size of the screen, but that didn't work either :(
There is too much code to post here unfortunately. The view is hundreds of lines long, but the important bit is adding the overlayed subview, which is like this:
InfoScreen *infoScreen = [[UIView alloc] initWithFrame:self.view.frame];
UIButton *invisibleButton = [UIButton buttonWithType:UIButtonTypeCustom];
invisibleButton.frame = self.view.frame;
[self.view addSubview:invisibleButton];
[self.view addSubview:infoScreen];
I am using touchesBegan, touchesMoved, touchesEnded and touchesCancelled in the view below. Is this possibly why the touches are getting through?
Thanks!
:-Joe
If you want to stop everything from responding (for example for a time-based block), you can use
[[UIApplication sharedApplication] beginIgnoringInteractionEvents];
and
[[UIApplication sharedApplication] endIgnoringInteractionEvents];
It won't work if you need a touch to cancel this state ;-). In that case, a transparent UIView on top of your background will get the job done. If it doesn't, post some code.