topLayoutGuide in child view controller
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 :)
How to set topLayoutGuide position for child view controller
As far as I have been able to tell after hours of debugging, the layout guides are readonly, and derived from the private classes used for constraints based layout. Overriding the accessors does nothing (even though they are called), and it's all just craptastically annoying.
Layout not working when container view controller dynamically changes child view controller
Asteroid is on the right track, but couple other issues...
You are not giving the views of the child controllers any constraints, so they load at their "native" size.
Changing your addViewController(...)
func as advised by Asteroid solves the A
and B
missing constraints, but...
You are calling that same func for your container
controller and adding constraints to its view in layout()
, so you end up with conflicting constraints.
One solution would be to change your addViewController
func to this:
func addViewController(_ child: UIViewController, constrainToSuperview: Bool = true) {
addChild(child)
view.addSubview(child.view)
if constrainToSuperview {
child.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
child.view.topAnchor.constraint(equalTo: view.topAnchor),
child.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
child.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
])
}
child.didMove(toParent: self)
}
then in setup()
:
func setup() {
titleBarView.backgroundColor = .red
view.addSubview(titleBarView)
// change this
//addViewController(container)
// to this
addViewController(container, constrainToSuperview: false)
showViewControllerA()
}
while leaving your other "add view controller" calls like this:
container.addViewController(viewControllerA)
container.addViewController(viewControllerB)
The other thing that may throw you off is the extraneous super.
in your image view constraints:
NSLayoutConstraint.activate([
// change this
//imageView.centerYAnchor.constraint(equalTo: super.view.centerYAnchor),
//imageView.centerXAnchor.constraint(equalTo: super.view.centerXAnchor),
//imageView.heightAnchor.constraint(equalTo: super.view.heightAnchor, multiplier: 0.6),
// to this
imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
imageView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.6),
])
topLayoutGuide applied after viewWillAppear
The documentation for the topLayoutGuide
states explicitly:
Query this property within your implementation of the
viewDidLayoutSubviews()
method.
Judging from your own inspections the earliest point to obtain the topLayoutGuide
's actual length is inside the viewWillLayoutSubviews()
method. However, I would not rely on that and do it in viewDidLayoutSubviews()
as the docs suggest.
The reason why you cannot access the property earlier...
... is that the layout guides are objects that depend on the layout of any container view controllers. The views are laid out lazily when they are needed on screen. So when you add the viewController
to the navigationViewController
as its root view controller it's not laid out yet.
The layout happens when you present the navigationController
. At that point the views of both view controllers are loaded (→ viewDidLoad()
, viewWillAppear()
) and then a layout pass is triggered. First, the navigationViewController
's view is laid out (layout flow: superview → subview). The navigation bar's frame is set to a height of 64 px. Now the viewController
's topLayoutGuide
can be set. And finally the viewController
's view is laid out (→ viewWillLayoutSubviews()
, viewDidLayoutSubviews()
).
Conclusion:
The only way to do some initial layout tweaks that depend on the layout guide's length is the method you suggested yourself:
Have a boolean property in your view controller that you set to
true
initially:var isInitialLayoutPass: Bool = true
Inside
viewDidLayoutSubviews()
check for that property and only perform your initial layout when it'strue
:func viewDidLayoutSubviews() {
if isInitialLayoutPass {
tableView.contentOffset = CGPoint(x: 0, y: topLayoutGuide.length)
}
}Inside
viewDidAppear()
, set the property tofalse
to indicate that the initial layout is done:override func viewDidAppear() {
super.viewDidAppear()
isInitialLayoutPass = false
}
I know it still feels a little hacky but I'm afraid it's the only way to go (that I can think of) unless you want to use key-value-observing (KVO) which doesn't make it much neater in my opinion.
subview vs child view controller z position
Found the bug.
Other views had bringSubViewToFront / layer.zPosition,
some of them inside viewDidLayoutSubview, after I disable this code, everything works O.K
What confused me is the view debugger show the view on top,
but on the device it was hidden.
Related Topics
Cast Value of Type 'Uitableviewcell' to Custom Cell
Gmsgeocoder Reversegeocodecoordinate: Completionhandler: on Background Thread
Sizing a Container View with a Controller of Dynamic Size Inside a Scrollview
How to Make Uibutton's Text Alignment Center? Using Ib
Using iOS Gamekit's "Bluetooth Bonjour" with Other Platforms
How to Custom Modal View Controller Presenting Animation
iOS 9 Searchbar Disappears from Table Header View When Uisearchcontroller Is Active
How to Rearrange Views When Autorotating with Autolayout
Transparent Uinavigationbar in Swift
Xcode 9.2 Upload to App Store Fails with Description Length and Invalid Toolchain Errors
Cannot Assign Value of Type '() -> Void' to Type '(() -> Void)!'
Get Attribute of Viewcontroller Created with Storyboard
Iphone: Can a Dev Other Than Team Agent Build an App for Distribution
How to Convert Nsinteger to Nsstring Datatype
How to Make Sure API Requests Come from Our Mobile (Ios/Android) App