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.
Related
iOS 10, the gift that keeps on breaking, seems to have changed another behavior.
Assume two UIViewControllers are pushed onto a UINavigationController.
On iOS 8/9, calling navigationController?.popViewController(animated: true) to pop the top UIViewController (say VC2) caused viewDidLayoutSubviews on the bottom view controller (say VC1) to get called.
We relied on this to refresh VC1. Sometimes VC2 adds subviews to VC1 (via the data model), and this needs to get reflected when popping back to VC1.
Accurate frame information is required. We can't use viewWillAppear because frame data is wrong on iOS 9. The problem with viewDidAppear is that there is a momentary glitch between seeing the view initially and the adjustment.
Now VC1's viewDidLayoutSubviews does not get invoked when popping VC2.
1) Is this a bug for viewDidLayoutSubviews to not get invoked?
2) What's the right way to refresh view controllers when popping with UINavigationController?
Relying on viewDidLayoutSubviews was never the proper solution. UIViewController provides viewWillAppear: or viewDidAppear: for such a use. When VC2 is popped from the navigation controller, those two methods will be called on VC1 to let you know that is will be or now is visible again.
viewDidLayoutSubviews should only be used to adjust view frames and layout.
viewWill|DidAppear: should be used to handle the view controller becoming visible originally or again. In your case you should use this to update data and add/update views as needed. Those new views should be setup based on the view controller's current frame. They will be adjusted in your implementation of viewDidLayoutSubviews as needed.
I will complement rmaddy's answer. You need to decouple performing layout and updating your data. If your flow is such that data should updated as the view is about to appear, you should update your controller's view-backing data in viewWillAppear:, reload your views and then mark the view as needing layout using setNeedsLayout. This will cause the system to perform a layout on the controller's view, and will trigger the layout. This way, you can ensure the layout is performed once the view is ready, and not before (as often the case is in viewWillAppear:.
in my iPad-app I connected a UIButton to another UIViewcontroller by just dragging in Storyboard and chose popover as segue. Everything is working fine, but the user can dismiss the popover by touching just somewhere besides the popover right.
How can I detect that the popover has dismissed in iOS8? In iOS7 I could just use the UIPopoverDelegate -popoverDidDidmiss...
But this is not working anymore!? I googled a lot but could not find any solution.
You put your
-(void)popoverControllerDidDismissPopover:(UIPopoverController *)popoverController
in the UIViewController where the start UIButton is ? (not in another popover UIViewcontroller ?)
That work well for me with iOS 8.1...
You have to delegate to the initial UIViewController for that.
I assume you set the delegate properly, but do you retain the popover, i.e. assign it to a strong property? In ios7 if you didn't retain the popover you would get exception: '[UIPopoverController dealloc] reached while popover is still visible.' In ios8 this is not longer the case, so you get the working popover and you can dismiss it, but the delegate methods are not called anymore.
(Frankly speaking, I'm not sure why this is so. I'd suppose that at least "should dismiss popover" should be called anyway).
You should probably use UIPopoverControllerDelegate and the method
popoverControllerDidDismissPopover:
to accomplish what you need.
in iOS8, it is using the UIViewController's UIPopoverPresentationController to present the popover. (Optionally you still can use back the old UIPopoverController to build the popover manually.
If you are using storyboard on iOS8, you can set the UIViewController's popoverPresentationController delegate to handle the followings:
popoverPresentationControllerShouldDismissPopover:
popoverPresentationControllerDidDismissPopover:
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.
I am trying to track down a problem where the viewWillLayoutSubviews (and viewDidLayoutSubviews) method do not get called after dismising a controller displayed using -
[self presentModalViewController:controller animated:YES];
and dismissing with
[self dismissModalViewControllerAnimated:YES];.
This view controller is displayed on top of a UISplitViewController as the result of a button being pressed in the detail area. When I rotate the device, without the modal up, I do get the viewWillLayoutSubviews callback. However, the problem is, when I rotate during the presentation of the model, it does not update the views correctly and recalculate the view bounds after dismissing it. According to the IOS 5 release notes I should get a viewDidLayoutSubviews after dismissing the modal view controller.
For comparison, I created a bare bones app with none of my other code in it and it works as documented, it will call viewWillLayoutSubviews after the modal is dismissed.
I have been going over and over my real app code and can't find anything wrong. I am looking for suggestions for things to do help figure this out. Why would the callback work when rotating but not work when the modal is dismissed? Could it be something with my view hierarchy?
Thanks for any help!
Try using the delegate method viewWillAppear instead of viewWillLayoutSubviews. The WillLayoutSubviews is only called when the view's bounds change (which happens when you rotate the device).
I'm having a hard time understanding why the following is happening (and how to fix it).
I've created an application using the split-view based application.
I've added a UiBarButtonItem called showTheModal which calls this method found in RootViewController.m:
- (IBAction)showTheModal:(id)sender {
theModalController.modalPresentationStyle = UIModalPresentationFullScreen;
theModalController.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[self presentModalViewController:theModalController animated:YES];
if ([detailViewController popoverController] != nil)
[[detailViewController popoverController] dismissPopoverAnimated:YES];
The BarButtonItem of course, is shown at the bottom of the Default Root Controller (left side of the of the split view in landscape) or at the bottom of the popup (if in landscape).
The modal view is dismissed by a button placed in a toolbar. It calls the following:
[self dismissModalViewControllerAnimated: YES];
The problem I'm having is if rotate the screen, while the modal is up. Here is what happens in different scenarios (start refers to the orientation when the showTheModal button is hit, end refers to the orientation when I hit the dismissModal button).
1)Start landscape, end landscape: Everything appears fine. willHideViewController and willShowViewController methods are not called in the RootViewController (as expected)
2) Start landscape, end portrait: UI appears fine. willHideViewController is run TWICE (WHY?)
3) Start portrait, end portrait: UI appears fine. willHideViewController is run once (as expected)
4) Start portrait, end landscape: The 'Root List' button remains in the detail view (right side of the split view. Neither willHideViewController and willShowViewController are invoked (WHY??)
Any thoughts as to why #2 and #4 don't behave quite the expected way?
I've had exactly the same problem (#4, above). I worked around it using viewDidAppear:animated, and then checking the height of the view to see if it is in landscape vs. portrait. (Yuck, gag, etc.) I'm not satisfied at all with that "solution".
Possibly related: I've noticed that the button in portrait mode is slow to disappear after rotating to landscape, i.e. the button appears for a second after the rotation finishes. However, in Mail.app, the "Inbox" button disappears as soon as the rotation starts. Is Apple doing things differently than they recommend in their docs? Perhaps there is a more efficient way to show/hide the master view button?
Unfortunately this is not a bug. It appears to be an expected behavior.
I found this in iOS Release Notes for iOS 5.0, in "Notes and Known Issues" section:
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.
For diagnostics, have you tried dismissing the popover view first? Or logging who is calling the method by printing (id) sender?
I was having the same exact problem.
In answer to (2), it appears to be a bug. I noticed that when a modal view is pushed over a splitview, the orientation messages are queued up somewhere and not processed until the modal view is dismissed and the splitview is visible but I would still expect to only get one callback.
For (4), this too appears to be a bug. Fortunately, the didRotate... events still get through, so my solution was to subclass UISplitViewController and explicitly call the delegate's willShowViewController method in this case:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
//Work around a bug where UISplitViewController does not send
//willShowViewController after a modal is presented in portrait
//but dismissed in landscape.
UIInterfaceOrientation orientation = self.interfaceOrientation;
if ( (orientation == UIInterfaceOrientationLandscapeLeft )
|| (orientation == UIInterfaceOrientationLandscapeRight) )
{
UINavigationItem* item = [detail.navigationBar.items objectAtIndex:0];
UIBarButtonItem* barButtonItem = [item leftBarButtonItem];
[super.delegate splitViewController:self willShowViewController:master invalidatingBarButtonItem:barButtonItem];
}
}
Here, "master" is an IBOutlet that refers to the master view controller (left hand side) of the splitview and "detail" is an IBOutlet for the detail view controller (right hand size).
Note that in my case, the detail view is a UINavigationController. You may require different code to get the barButtonItem from your view controller.
Also, this has the side-effect of calling willShowViewController twice for normal rotation, but that is not an issue in my case.
I think that this is a bug that needs to be reported to Apple Development.
I worked around part of this issue by presenting my modal view using the UIModalPresentationPageSheet format.