My app has four tabs: A, B, C and D. Their UIViewController are managed by UITabBarController. The app supports rotation, and so each view controller returns YES to shouldAutorotateToInterfaceOrientation.
Using springs and struts, most of the rotation is done automatically by iOS. However, tab A also requires further positioning, and it is done in its VC's willRotateToInterfaceOrientation method.
When the VC for tab A is selected and the screen is rotated, that VC receives a willRotateToInterfaceOrientation message (propagated by iOS from UITabBarController), and the resulting rotation is correct.
However, when the selected tab is B and the screen is rotated, A's willRotateToInterfaceOrientation is not called. Makes sense. But if I then select tab A, I get only the results of applying its springs and struts, without the post-processing done by its willRotateToInterfaceOrientation.
After struggling with this for a while, and after failing to find a solution online, I came up with the following. I subclassed UITabBarController and in its willRotateToInterfaceOrientation I call all the VCs' willRotateToInterfaceOrientation regardless of which one is the selectedViewController:
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
if (self.viewControllers != nil) {
for (UIViewController *v in self.viewControllers)
[v willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
}
It works, but it looks like a hack, and my question is whether I was doing the right thing. Is there a way to tell iOS to always call a VC's willRotateToInterfaceOrientation before displaying it for the first time after a screen rotation?
The best way to handle custom layout is by subclassing UIView and overriding the layoutSubviews method. The system sends layoutSubviews to a view whenever its size is changed (and at other times). So when your view A is about to appear on screen with a different size (because the interface was rotated while view B was on screen), the system sends view A a layoutSubviews message, even though it doesn't send view controller A a willRotateToInterfaceOrientation: message.
If you are targeting iOS 5.0 or later, you can override the viewDidLayoutSubviews method of your UIViewController subclass and do your layout there, instead of subclassing UIView. I prefer to do it in my view's layoutSubviews, to keep my view-specific logic separate from my control logic.
It's also a bad idea to do layout in willRotateToInterfaceOrientation: because the system sends that message before actually changing the size of the view, and before the rotation animation block. It sends the willAnimateRotationToInterfaceOrientation:duration:, layoutSubviews, and viewDidLayoutSubviews messages inside the rotation animation block, so the repositioning of your subviews will be animated if the view is on screen during the rotation.
Related
The rotation animation occurs for the status bar (which has the clock and the battery icon), but the view itself just changes size, it doesn't do the page flip animation. In the gif (below), I did a few screencaptures of the rotation animation in slow motion. You can see the clock and battery icon rotate into the view, even though the content just scales.
http://imgur.com/gallery/Q3OXCIH
I found some similar, but not quite the same posts:
iOS Device Rotation Instant Snap rather than animation
iOS 9 Orientation Auto-Rotation Animation Not Working, But Always on Main Thread
This is somewhat repeatable- at first, the rotation occurs correctly, but after I programmatically change the tab view controller index, it can trigger. After it triggers, the rotation animation does not occur for the view until after I reset the app.
Code where I change the tab view controller and then change it back:
[appDelegate.tabBarController setSelectedIndex:0];
...code to operate on the code at index 0...
[appDelegate.tabBarController setSelectedIndex:2];
To emphasize- it DOES animate the rotation correctly when I first run the app. Behaves the same in simulator and in hardware. IOS9. Xcode 7.1.1.
Anyone know why a viewcontroller's content would stop animating during rotation?
edit-
To answer fragilecat's questions:
1) I am set up to use the rotation functions, as described in https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/clm/UIViewController/attemptRotationToDeviceOrientation
I have implemented shouldAutorotateToInterfaceOrientation and shouldAutorotate and supportedInterfaceOrientations. The supportedInterfaceOrientations gets called once, when the viewcontroller loads. shouldAutorotateToInterfaceOrientation and shouldAutorotate are apparently never called.
2) I am receiving size change messages via viewWillTransitionToSize- this is ios9 so there aren't any rotation messages. willTransitionToTraitCollection is apparently never called, though it is overriden. I am calling the super for both.
3) I am not using viewWillLayoutSubviews() or viewDidLayoutSubviews(). I am only overriding viewDidLoad and viewWillAppear. These do not affect rotation.
4) I am not dynamically changing rotation methods.
What I did notice, is that rotation works at first, but then fails (doesn't rotate but just scales), after I change the tabBarViewController selectedViewController programmatically after the use clicks "ok" to an alertview. I haven't figured out why yet, but it is repeatably after that event.
Sequence of the bug:
Works fine, rotates ok.
User hits "ok" to an alertview
I programmatically call [tabBarViewController setSelectedIndex:0]
I call some functions on the viewcontroller at index 0.
I programmatically call [tabBarViewController setSelectedIndex:2] (back to the original)
rotation now does not occur reliably
Anyone know why a viewcontroller's content would stop animating during rotation?
The child view controller's rotation method's are configure incorrectly.
Your child view controller's are not receiving rotation messages.
You have custom code in the view controller's layout methods.
Your code is dynamically altering rotation/layout/animation methods.
I am looking for direction on what kind of bugs can produce this type of error.
This is some what hard to answer with out seeing your code, but here are the steps I would take in trouble shooting this issue.
Confirm that your child view controllers are correctly setup for rotation.
UIViewController Rotation Methods
Confirm that your child view controller's are receiving the following messages so that you can rule out this as the issue as this is how rotation is handled in iOS 9.
func willTransitionToTraitCollection(_ newCollection: UITraitCollection,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
func viewWillTransitionToSize(_ size: CGSize,
withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)
If you are overriding these methods else where you should make sure you call the super's version, see UIContentContainer for details.
Comment out any custom code that you might have in viewWillLayoutSubviews() implementation of your view controllers. Also you can check your frames between viewWillLayoutSubviews() and viewDidLayoutSubviews().
EDIT
You need to execute the tab change on the main thread. If I am correct you are using the delegate of UIAlertView? I don't think your on the main thread went you make your call!
dispatch_async(dispatch_get_main_queue(), ^{
[appDelegate.tabBarController setSelectedIndex:0];
});
We have a MainViewController with a tableView, and it presents a new modalViewController.
The MainViewController is restricted to portrait only, and the modalViewController can rotate.
The problem is in iOS8, that when the modalViewController rotates, the callback method of rotation in iOS8 in MainViewcontroller is called - - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
Thus, the UITableView is getting its data reloaded, which is a behaviour we don't want.
Can we prevent this feature of iOS 8, and not rotate the presenting UIViewController?
So after long days of searching and investigating, I finally came up with a possible solution.
First of all, I can use navigation controller and push the viewController instead of presenting it, but it breaks my code and just isn't so true.
The second thing I can do is not setting constraints. I still can use autolayout, but if I don't set constraints, and let the default constraints to be set, the tableView doesn't get reloaded. of course this is also isn't very smart thing to do, as I have many elements in my viewController.
Finally, I figured out that I can show this "modal" viewController in another UIWindow. I create UIWindow and set the modalViewController as its rootViewController.
I put some example project in git:
https://github.com/OrenRosen/ModalInWindow
Hope it will be helpful.
I did something similar with a navigation controller, that wouldn't rotate unless the top pushed controller does rotate.
In your case check if the main controller is presenting another controller. If it isn't then just reject rotation, otherwise return whatever the presented controller returns for the rotation method.
As for your table view, it shouldn't get reloaded because of rotations.
In iOS 8 the view that rotates when you change the device orientation is the first view added to the UIWindow. So, if you save a reference to it in your presentedController, you can overwrite the shouldAutorotate and supportedInterfaceOrientations values.
My viewController1 has a child view controller (viewController2). viewController2.view is a subview of viewController1's view. At the user's action, I remove the view of viewController2 from it's superview. After a while I have to add it again as subview.
The problem is that if the user rotates the device while viewController2 is not visible, after adding it again to viewController1's view, it's frame and it's subviews are placed as the device was still in the old orientation. Even the viewController2.view.frame has the height and with interchanged.
Does anyone have a solution to this? Thanks in advance!
Without seeing your code, I would guess that you are keeping a reference to view controller 2 even when it isn't visible. In that case, you need to forward the view rotation events to the controller so that it knows about the rotation like this (Do this for each event that you want to forward):
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Forward to view controller 2 if it is not displayed
if (!viewController2.view) {
[viewController2 willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
}
A better design may be to just set view controller 2's view to hidden instead of removing it, and it will still get the events without manual intervention.
Let me guess your problem. Do you want to show the different UIView base on orientation , right?
It's about update your viewController2 to load the nib base on orientation. You can make the landscape and portrait nib to support your different UI due to orientation. Try to look from this Answer
Hope it helps you
Background: I want to make sure my viewControllers rotate properly when it appears. My viewControllers have excellent codes managing the rotation and orientation when it is visible.
Problem: Given two viewControllers in a NavigationController, viewC1 and viewC2. I did the following:
1.) Set rootViewController to viewC1
2.) Push viewC2 into the NavigationController
3.) Rotate
4.) Pop viewC2
5.) viewC1 is still stucked in the old orientation look (as in the transformation code in willAnimateRotationToInterfaceOrientation was not called) with the new orientation.
What can I do to ensure viewC1 call willAnimateRotationToInterfaceOrientation to reconstruct itself to look correctly in the new rotation?
Additional info:
This is all code (no storyboard/xib). I have shouldAutorotateToInterfaceOrientation return YES on all the views. I use willAnimateRotationToInterfaceOrientation to manage all my rotation.
Oh, and please no hacks. For example, copy the code from rotation then check the rotation mannually and manage it in viewDidAppear.
Think about the name of the method, and what you're trying to achieve.
willAnimateRotationToInterfaceOrientation indicates that the view controlled by the view controller is about to animate to a particular orientation. If your view is in the middle of a navigation stack, then it is not being displayed on screen. To animate something that isn't on screen is costly and ultimately worthless. So, that particular method is out of the question, but the problem that remains is there isn't anything else more appropriate in UIKit. The reason is to rotate something (even if not animated) when it's offscreen is worthless cost. It's the responsibility of the developer to handle a change in orientation when the view appears ("transformation on demand" as you will).
You say you don't want hacks, but the method you've described as a hack is probably the most appropriate thing to do. Having a generic method named something like
-(void) updateLayoutForOrientation:(UIInterfaceOrientation)orientation animated:(BOOL)animated { ... }
isn't a bad idea. This can be the handler for orientation change transformations for the whole view controller.
The places you need to possibly check/handle orientation issues are
-(void) viewWillAppear:(BOOL)animated
-(void) willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation) interfaceOrientation duration: (NSTimeInterval) duration
and in both of these, call updateLayoutForOrientation:animated: to do the work for you.
As has been reported in other questions here on SO, iOS 5 changes how rotation callbacks for split view controllers are sent as per this release note. This is not a dupe (I think), as I can't find another question on SO that deals with how to adjust split view controller usage in iOS 5 to cope with the change:
Rotation callbacks in iOS 5 are not applied to view controllers that
are presented over a full screen. What this means is that if your code
presents a view controller over another view controller, and then the
user subsequently rotates the device to a different orientation, upon
dismissal, the underlying controller (i.e. presenting controller) will
not receive any rotation callbacks. Note however that the presenting
controller will receive a viewWillLayoutSubviews call when it is
redisplayed, and the interfaceOrientation property can be queried from
this method and used to lay out the controller correctly.
I'm having trouble configuring the popover button in my root split view controller (the one that is supposed to show the left pane view in a popover when you're in portrait). Here's how my app startup sequence used to work in iOS 4.x when the device is in landscape mode:
Install split view controller into window with [window addSubview:splitViewController.view]; [window makeKeyAndVisible];. This results in splitViewController:willHideViewController:withBarButtonItem:forPopoverController: being called on the delegate (i.e. simulating a landscape -> portrait rotation) even though the device is already in landscape mode.
Present a fullscreen modal (my loading screen) which completely covers the split view underneath.
Finish loading and dismiss the loading screen modal. Since the device is in landscape mode, as the split view controller is revealed, this causes splitViewController:willShowViewController:invalidatingBarButtonItem: to be called on the delegate (i.e. simulating a portrait -> landscape rotation), thereby invalidating the bar button item, removing it from the right-side of the split view, and leaving us where we want to be. Hooray!
So, the problem is that because of the change described in that release note, whatever happens internally in iOS 4.3 that results in splitViewController:willShowViewController:invalidatingBarButtonItem: being called no longer happens in iOS 5. I tried subclassing UISplitViewController so I could provide a custom implementation of viewWillLayoutSubviews as suggested by the release note, but I don't know how to reproduce the desired sequence of internal events that iOS 4 triggers. I tried this:
- (void) viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
UINavigationController *rightStack = [[self viewControllers] objectAtIndex:1];
UIViewController *rightRoot = [[rightStack viewControllers] objectAtIndex:0];
BOOL rightRootHasButton = ... // determine if bar button item for portrait mode is there
// iOS 4 never goes inside this 'if' branch
if (UIInterfaceOrientationIsLandscape( [self interfaceOrientation] ) &&
rightRootHasButton)
{
// Manually invoke the delegate method to hide the popover bar button item
[self.delegate splitViewController:self
willShowViewController:[[self viewControllers] objectAtIndex:0]
invalidatingBarButtonItem:rightRoot.navigationItem.leftBarButtonItem];
}
}
This mostly works, but not 100%. The problem is that invoking the delegate method yourself doesn't actually invalidate the bar button item, so the first time you rotate to portrait, the system thinks the bar button item is still installed properly and doesn't try to reinstall it. It's only after you rotate again to landscape and then back to portrait has the system got back into the right state and will actually install the popover bar button item in portrait mode.
Based on this question, I also tried invoking all the rotation callbacks manually instead of firing the delegate method, e.g.:
// iOS 4 never goes inside this 'if' branch
if (UIInterfaceOrientationIsLandscape( [self interfaceOrientation] ) &&
rightRootHasButton)
{
[self willRotateToInterfaceOrientation:self.interfaceOrientation duration:0];
[self willAnimateRotationToInterfaceOrientation:self.interfaceOrientation duration:0];
[self didRotateFromInterfaceOrientation:self.interfaceOrientation];
}
However this just seems to cause an infinite loop back into viewWillLayoutSubviews :(
Does anyone know what the correct way to simulate the iOS4-style rotation events is for a split view controller that appears from behind a full-screen modal? Or should you not simulate them at all and is there another best-practices approach that has become the standard for iOS5?
Any help really appreciated as this issue is holding us up from submitting our iOS5 bugfix release to the App Store.
I don't know the right way to handle this situation. However, the following seems to be working for me in iOS 5.
In splitViewController:willHideViewController:withBarButtonItem:forPopoverController:, store a reference to the barButtonItem in something like self.barButtonItem. Move the code for showing the button into a separate method, say ShowRootPopoverButtonItem.
In splitViewController:willShowViewController:invalidatingBarButtonItem:, clear that self.barButtonItem reference out. Move the code for showing the button into a separate method, say InvalidateRootPopoverButtonItem.
In viewWillLayoutSubviews, manually show or hide the button, depending on the interface orientation
Here's my implementation of viewWillLayoutSubviews. Note that calling self.interfaceOrientation always returned portrait, hence my use of statusBarOrientation.
- (void)viewWillLayoutSubviews
{
if (UIInterfaceOrientationIsPortrait(
[UIApplication sharedApplication].statusBarOrientation))
{
[self ShowRootPopoverButtonItem:self.barButtonItem];
}
else
{
[self InvalidateRootPopoverButtonItem:self.barButtonItem];
}
}