In my app, I aligned a label the standard amount from the bottomLayoutGuide using autolayout. When the app first starts everything is layed out as I wanted but when I switch tabs and go back the label has disappeared under the tab bar controller.
If I rotate the device, the landscape view appears correctly and when I rotate it back to portrait the view is back to normal. I can't seem to figure out what is causing this behavior. Thanks for your help!
This happens due to a bug in iOS7, where the bottom layout guide is incorrectly set to height 0 instead of the tab bar's height. When you rotate the device, the bottom layout guide is set correctly.
Currently, your best option is to disable bottom extended layout:
- (UIRectEdge)edgesForExtendedLayout
{
return [super edgesForExtendedLayout] ^ UIRectEdgeBottom;
}
Do this for each view controller that is displayed from the tab bar controller. Remember to set the tab bar view controller's background color to whatever suits your application.
Make sure to open a bug report at https://bugreport.apple.com
To elaborate a little more, it seems viewDidLayoutSubviews is called twice when switching view controllers. First time, everything is set correctly, but the second time bottom layout guide height is 0. You can see from the stack trace that the first one comes from tab bar layout, while the second call is from a scheduled CALayer layout, which is incorrect.
While Leo's answer shows how to do it programmatically, if you want to do this from the interface builder, select your View Controller and uncheck "Under bottom bars" from the Extend Edges section:
Calling setNeedsLayout is all that needs to be done. This essentially patches the framework bug. It needs to be called on the UITabBarController view itself when a new view is selected. Create a delegate for the app's tab bar controller. and put this in the delegate object:
#interface MyPatch : NSObject <UITabBarControllerDelegate>
#end
#implementation MyPatch
-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController
{
[tabBarController.view setNeedsLayout];
}
#end
And initialize it wherever you want... something like this:
#interface AppDelegate : UIResponder <UIApplicationDelegate>
{
MyPatch *patch;
}
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
patch=[MyPatch new];
myTabBarController.delegate=patch;
}
#end
Leo is right, the bottomLayoutGuide is returned incorrectly.
But unsetting the extend edges under bottom bars (or overriding edgesForExtendedLayout) had too much undesired effects on other subviews for me.
If you want to change only the constraint for one view according to the bottom layout guide,
implement viewWillLayoutSubviews and check the value of the bottomLayoutGuide property and adapt that one constraint if required, like so:
- (void)viewWillLayoutSubviews {
[self adaptBottomLayoutGuides];
}
/// Workaround for iOS7 bug returning wrong bottomLayoutGuide length if this is 1st tab in TabViewController
- (void)adaptBottomLayoutGuides {
NSLog(#"%f", self.bottomLayoutGuide.length);
CGFloat expectedHeight = 123;
CGFloat adaptedSpacing = expectedHeight - self.bottomLayoutGuide.length;
self.viewBottomLayoutSpacingConstrain.constant = adaptedSpacing;
}
Related
I'm having a problem with the prompt on a UINavigationItem that I just can't resolve...
I have a master and a detail view controller. When I push from the master to the detail a prompt is shown on the detail view controller:
However, when I pop back to the master view controller, the view isn't resized and the window shows through (the window has been coloured red):
This only happens on iOS7, on iOS6 the view resizes as expected.
I've tried a few things such as setting the prompt to nil in viewWillDisappear or viewDidDisappear but nothing seems to fix it.
If I set the navigation bar in the navigation controller to translucent it does fix this - unfortunately that's not an option.
I've created a very small example project here which demonstrates the issue: https://github.com/InsertWittyName/NavigationItemPrompt
Thanks in advance for any help!
A solution I can think of is to subclass the UIView of the master, and implement viewDidMoveToSuperview to set the frame of the view to be from the navigation bar's height to the end of the superview. Since the navigation bar is not translucent, your job is easier, as you don't have to take into account layout guides and content insets.
A few things to notice. When pushing and popping, the system moves your view controller's view into another superview for the animation and then returns it to the navigation controller's private view hierarchy. Also, when a view goes outside of the view hierarchy, the superview becomes nil.
Here is an example implementation:
#interface LNView : UIView
#end
#implementation LNView
- (void)viewDidMoveToSuperview
{
[super viewDidMoveToSuperview];
if(self.superview != nil)
{
CGRect rect = self.superview.bounds;
rect.origin.y += 44;
rect.size.height -= 44;
[self setFrame:rect];
}
}
#end
This is not a perfect implementation because it uses a hardcoded value for the navigation bar's height, does not take into account a possible toolbar, etc. But all these you can add as properties to this view and in viewDidLoad, before it starts going into the view hierarchy, set the parameters according to your needs.
You can remove the prompt when the user taps the back button, like this
override func willMove(toParentViewController parent: UIViewController?) {
super.willMove(toParentViewController: parent)
if parent == nil {
navigationItem.prompt = nil
}
}
The problem exists whether your nav bars are opaque or translucent. It sucks that Apple has allowed this heinous bug to plague us for over three years now.
All of these solutions are hacks. My solution is to either A) NEVER use prompts, or B) use them in every single view even if you have to set them to "".
You've given the answer yourself - brilliantly. It's a bug, but checking Translucent avoids the bug. Therefore the solution is to check Translucent and then compensate so that the nav bar looks the way you want.
To do so, make a small black rectangle image and include it in your project. Set the background image of the nav bar to this image. Check Translucent. Problem solved! The nav bar is now black opaque in appearance, but it no longer exhibits the bug.
Swift version:
class PromptViewSideEffect: UIView {
override func didMoveToSuperview() {
super.didMoveToSuperview()
if let superview: UIView = self.superview {
let rect: CGRect = superview.bounds
rect.origin.y += 44
rect.size.height -= 44
self.frame = rect
}
}
}
I have an iphone app with 2 ViewControllers . Both screens(viewcontrollers) show a loading screen. I create the loading screen programmatically:
UIView *loadingScreen = [[UIView alloc] initWithFrame:CGRectMake(100,200,144,144)];
loadingScreen.center = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
//{.. Other customizations to loading screen
// ..}
[self.view addSubview:loadingScreen];
For some reason, the second viewcontroller's loadingScreen is significantly lower and it isn't centered on the screen. The first viewcontroller works perfectly and is dead center like I want.
The second viewcontroller is a UITableView and it shows the uinavigationbar, whereas the first viewcontroller doesn't show the uinavigationbar. Also, I use storyboard for my app.
I've outputted to the NSLog self.view.frame.size.height and loadingScreen.center in both instances and THEY HAVE THE SAME COORDINATES! So, not sure why it is showing up lower. Any ideas why the second loadingScreen is lower and how to fix? Thanks!
You mention that one screen displays a UINavigationBar while the other does not. When you display a navigation bar, it offsets the rest of your view - in this case by shifting it down.
There are two quick fixes. You can either adjust your center point up by the size of the UINavigationBar (65 pts - unless it's a custom UINavigationBar and you've changed its size) or you can set the "Adjust Scroll View Insets" value to false in the attributes inspector.
The latter is probably the easiest and comes most recommended. Note though, that the top of your UITableView will now be underneath the UINavigationBar.
My final note would be that if you wanted to do it programmatically than in your UITableView's delegate you can call
- (BOOL)automaticallyAdjustsScrollViewInsets
{
return NO;
}
I have a UIPageViewController with translucent status bar and navigation bar. Its topLayoutGuide is 64 pixels, as expected.
However, the child view controllers of the UIPageViewController report a topLayoutGuide of 0 pixels, even if they're shown under the status bar and navigation bar.
Is this the expected behavior? If so, what's the best way to position a view of a child view controller under the real topLayoutGuide?
(short of using parentViewController.topLayoutGuide, which I'd consider a hack)
While this answer might be correct, I still found myself having to travel the containment tree up to find the right parent view controller and get what you describe as the "real topLayoutGuide". This way I can manually implement automaticallyAdjustsScrollViewInsets.
This is how I'm doing it:
In my table view controller (a subclass of UIViewController actually), I have this:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
_tableView.frame = self.view.bounds;
const UIEdgeInsets insets = (self.automaticallyAdjustsScrollViewInsets) ? UIEdgeInsetsMake(self.ms_navigationBarTopLayoutGuide.length,
0.0,
self.ms_navigationBarBottomLayoutGuide.length,
0.0) : UIEdgeInsetsZero;
_tableView.contentInset = _tableView.scrollIndicatorInsets = insets;
}
Notice the category methods in UIViewController, this is how I implemented them:
#implementation UIViewController (MSLayoutSupport)
- (id<UILayoutSupport>)ms_navigationBarTopLayoutGuide {
if (self.parentViewController &&
![self.parentViewController isKindOfClass:UINavigationController.class]) {
return self.parentViewController.ms_navigationBarTopLayoutGuide;
} else {
return self.topLayoutGuide;
}
}
- (id<UILayoutSupport>)ms_navigationBarBottomLayoutGuide {
if (self.parentViewController &&
![self.parentViewController isKindOfClass:UINavigationController.class]) {
return self.parentViewController.ms_navigationBarBottomLayoutGuide;
} else {
return self.bottomLayoutGuide;
}
}
#end
Hope this helps :)
I might be wrong, but in my opinion the behaviour is correct. The topLayout value can be used by the container view controller to layout its view's subviews.
The reference says:
To use a top layout guide without using constraints, obtain the guide’s position relative to the top bound of the containing view.
In the parent, relative to the containing view, the value will be 64.
In the child, relative to the containing view (the parent), the value will be 0.
In the container View Controller you could use the property this way:
- (void) viewWillLayoutSubviews {
CGRect viewBounds = self.view.bounds;
CGFloat topBarOffset = self.topLayoutGuide.length;
for (UIView *view in [self.view subviews]){
view.frame = CGRectMake(viewBounds.origin.x, viewBounds.origin.y+topBarOffset, viewBounds.size.width, viewBounds.size.height-topBarOffset);
}
}
The Child view controller does not need to know that there are a Navigation and a Status bar : its parent will have already laid out its subviews taking that into account.
If I create a new page based project, embed it in a navigation controller, and add this code to the parent view controllers it seems to be working fine:
you can add a constraint in the storyboard and change it in viewWillLayoutSubviews
something like this:
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.topGuideConstraint.constant = [self.parentViewController.topLayoutGuide length];
}
The documentation says to use topLayoutGuide in viewDidLayoutSubviews if you are using a UIViewController subclass, or layoutSubviews if you are using a UIView subclass.
If you use it in those methods you should get an appropriate non-zero value.
Documentation link:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/Reference/Reference.html#//apple_ref/occ/instp/UIViewController/topLayoutGuide
In case if you have UIPageViewController like OP does and you have for example collection view controllers as children. Turns out the fix for content inset is simple and it works on iOS 8:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
UIEdgeInsets insets = self.collectionView.contentInset;
insets.top = self.parentViewController.topLayoutGuide.length;
self.collectionView.contentInset = insets;
self.collectionView.scrollIndicatorInsets = insets;
}
This has been addressed in iOS 8.
How to set topLayoutGuide position for child view controller
Essentially, the container view controller should constrain the child view controller's (top|bottom|left|right)LayoutGuide as it would any other view. (In iOS 7, it was already fully constrained at a required priority, so this didn't work.)
I think the guides are definitely meant to be set for nested child controllers. For example, suppose you have:
A 100x50 screen, with a 20 pixel status bar at the top.
A top-level view controller, covering the whole window. Its topLayoutGuide is 20.
A nested view controller inside the top view covering the bottom 95 pixels, eg. 5 pixels down from the top of the screen. This view should have a topLayoutGuide of 15, since its top 15 pixels are covered by the status bar.
That would make sense: it means that the nested view controller can set constraints to prevent unwanted overlap, just like a top-level one. It doesn't have to care that it's nested, or where on the screen its parent is displaying it, and the parent view controller doesn't need to know how the child wants to interact with the status bar.
That also seems to be what the documentation--or some of the documentation, at least--says:
The top layout guide indicates the distance, in points, between the top of a view controller’s view and the bottom of the bottommost bar that overlays the view
(https://developer.apple.com/library/ios/documentation/UIKit/Reference/UILayoutSupport_Protocol/Reference/Reference.html)
That doesn't say anything about only working for top-level view controllers.
But, I don't know if this is what actually happens. I've definitely seen child view controllers with nonzero topLayoutGuides, but I'm still figuring out the quirks. (In my case the top guide should be zero, since the view isn't at the top of the screen, which is what I'm banging my head against at the moment...)
This is the approach for the known guide length. Create constrains not to guides, but to view's top with fixed constants assuming guide distance will be.
Swifty implementation of #NachoSoto answer:
extension UIViewController {
func navigationBarTopLayoutGuide() -> UILayoutSupport {
if let parentViewController = self.parentViewController {
if !parentViewController.isKindOfClass(UINavigationController) {
return parentViewController.navigationBarTopLayoutGuide()
}
}
return self.topLayoutGuide
}
func navigationBarBottomLayoutGuide() -> UILayoutSupport {
if let parentViewController = self.parentViewController {
if !parentViewController.isKindOfClass(UINavigationController) {
return parentViewController.navigationBarBottomLayoutGuide()
}
}
return self.bottomLayoutGuide
}
}
Not sure if anyone still got problem with this, as I still did a few minutes ago.
My problem is like this (source gif from https://knuspermagier.de/2014-fixing-uipageviewcontrollers-top-layout-guide-problems.html).
For short, my pageViewController has 3 child viewcontrollers. First viewcontroller is fine, but when I slide to the next one, the whole view is incorrectly offset to the top (~20 pixel, I guess), but will return to normal after my finger is off the screen.
I stayed up all night looking for solution for this but still no luck finding one.
Then suddenly I came up with this crazy idea:
[pageViewController setViewControllers:#[listViewControllers[1]] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
}];
[pageViewController setViewControllers:#[listViewControllers[0]] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished) {
}];
My listViewControllers has 3 child viewcontrollers. The one at index 0 has problem, so I firstly set it as root of pageviewcontroller, and right after that set it back to the first view controller (as I expected).
Voila, it worked!
Hope it helps!
This is an unfortunate behavior that appears to have been rectified in iOS 11 with the safe-area API revamp. That said, you will always get the correct value off the root view controller. For example, if you want the upper safe area height pre-iOS 11:
Swift 4
let root = UIApplication.shared.keyWindow!.rootViewController!
let topLayoutGuideLength = root.topLayoutGuide.length
I'd like to ask about Auto Layout and in-call status bar. Here's a simple scenario that demonstrates my problem:
Create project with "Use Storyboards" enabled
Add "View Controller" and enable its "Is Initial View Controller"
Set background color of controller's view to red
Add "Table View" into controller's view
The table view should have 4 layout constraints (leading, top, trailing, bottom) to Superview with constant set to 0.
Now when I run this app in Simulator and press ⌘ + T I can see red background while the in-call status bar animates in. Is it possible to get rid of this glitch?
(Using answer instead of comment due to lack of reputation, sorry.)
I ran into this issue as well and was trying out e.g. the solution pointed out above: It didn't work for me.
So I created a repository with example code to expose the original poster's problem. There are example applications for these scenarios:
the Custom View Controller is the window's root view controller,
the Custom View Controller is a child of a UINavigationController which is the window's root view controller,
the Custom View Controller is a child of a UITabBarController which is the window's root view controller and
the Custom View Controller is a child of a UINavigationController which is as child of a UITabBarController which is the window's root view controller.
It turned out that the solution from CEarwood actually works… when the custom view controller is a child of a UINavigationController (cases 2 and 4). Hoewever, it does not work in cases 1 and 3.
I hope this information is useful.
For a purely Auto Layout answer you can get a reference to the bottom constraint and adjust its constant when UIApplicationWillChangeStatusBarFrameNotification is received and back to 0 when the DidChange notification is received. Here's the test VC I used:
#interface CEViewController ()
#property (nonatomic, strong) IBOutlet NSLayoutConstraint *bottomConstraint;
#end
#implementation CEViewController
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameWillChange:) name:UIApplicationWillChangeStatusBarFrameNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(statusBarFrameDidChange:) name:UIApplicationDidChangeStatusBarFrameNotification object:nil];
}
- (void)statusBarFrameWillChange:(NSNotification *)note {
NSValue *newFrameValue = [note userInfo][UIApplicationStatusBarFrameUserInfoKey];
self.bottomConstraint.constant = newFrameValue.CGRectValue.size.height;
[self.view setNeedsLayout];
}
- (void)statusBarFrameDidChange:(NSNotification *)note {
self.bottomConstraint.constant = 0;
[self.view setNeedsLayout];
}
#end
This is an effect from the screen resizing.
When the in-call status bar appears, the view resizes to the size it should have with the in-call status bar active and then moves down as the status bar changes size.
For a brief moment, the view under the table view is visible. What you could do is add a view under the table view extending downwards out of the screen to cover-up the background color.
Another approach is with your AppDelegate, implement:
-application:willChangeStatusBarFrame:
and resize the table view to cover the bit that gets exposed. Then when -application:didChangeStatusBarFrame: gets called, resize it back to the original size.
I have a tabBar based application and want to present some custom view above whole screen (not as modal view) and I do it like that:
[self.view.window addSubview:self.myViewController.view];
The reason I did this is because this way view is positioned above UITabBar.
Anyway view is presented nicely and it covers whole screen like I want to. But there is a problem. When I rotate device this top view does not rotate, but view's underneath do.
I've tested on iOS5 and iOS6 without luck. Have also put this code in delegate:
- (NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window
{
return UIInterfaceOrientationMaskAll;
}
Similar code is in myViewController's view:
- (NSUInteger)supportedInterfaceOrientations
{
return UIInterfaceOrientationMaskAll;
}
The view just doesn't rotate... ?
As far as i know only the first subview of the window gets the rotation events. You're adding another view (your view) to the window and therefore need to deal with propagating the rotation events yourself.
Providing some really quick-help for you, just check out the following implementation : AGWindowView (not maintained from 2016)
You can set the rootController for your UIWindow.
e.g:
fileprivate(set) var bottonOverlayWindow = UIWindow()
self.bottonOverlayWindow.rootViewController = self;
// 'self' will the ViewController on which you had added UIWindow view. So whenever you ViewController change the orientation, your window view also change it's orientation.
Let me know if you face any issue.