topLayoutGuide applied after viewWillAppear - ios

I have the issue that the topLayoutGuide.length in a UIViewController (from XIB) gets set after viewWillAppear and i don't know how to hook into the change of topLayoutGuide.length to initially set the contentOffset of a table view.
Code to modally present a UIViewController inside a UINavigationController:
let viewController = UIViewController(nibName: "ViewController", bundle: nil)
let navigationController = UINavigationController(rootViewController: viewController)
present(navigationController, animated: true, completion: nil)
My debugging output about the topLayoutGuide.length
Init view controller
-[UIViewController topLayoutGuide]: guide not available before the view controller's view is loaded
willMove toParentViewController - top layout guide nan
Init navigation controller and pass view controller as root vc
Present navigation controller modally
viewDidLoad - top layout guide 0.0
viewWillAppear - top layout guide 0.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
viewDidAppear - top layout guide 64.0
didMove toParentViewController - top layout guide 64.0
viewWillLayoutSubviews - top layout guide 64.0
viewDidLayoutSubviews - top layout guide 64.0
For now i use a bool flag in the view controller to set the contentoffset in the viewDidLayoutSubviews only once, even though the method is called multiple times.
Any more elegant solution in mind?

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's true:
func viewDidLayoutSubviews() {
if isInitialLayoutPass {
tableView.contentOffset = CGPoint(x: 0, y: topLayoutGuide.length)
}
}
Inside viewDidAppear(), set the property to false 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.

Related

CenterXAnchor with positive offset does not work?

I created a view within my ViewController and created an Outlet. The view is called "chatView".
I would like to hide the view directly after the main view has loaded (and swipe it in after some time, when the user clicks a button).
My approach was to manipulate the centerXAnchor-constraint:
override func viewDidLoad() {
super.viewDidLoad()
view.insertSubview(chatView, belowSubview: view)
chatView.translatesAutoresizingMaskIntoConstraints = false
chatCenterX = chatView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 1500)
chatCenterX.isActive = true
}
But for any reason (I have no idea why), the chatView is already displayed when loading the view, so the offset is not set.
I tried some other things and detected that the offset works if I set it to a negative value (I tried -1500 instead of 1500).
Do you have an idea what I did wrong?
Drag the constraint that you added in storyboard as an IBOutlet and change it's constant value to move the view forward or backward as you like
note : change it in viewDidLayoutSubviews i first launch with a boolean so not to be hidden when you want to show it , not in viewDidLoad
Apple says:
viewDidLayoutSubviews()
When the bounds change for a view controller's view, the view
adjusts the positions of its subviews and then the system calls this
method. However, this method being called does not indicate that the
individual layouts of the view's subviews have been adjusted. Each
subview is responsible for adjusting its own layout.
Your view controller can override this method to make changes after
the view lays out its subviews. The default implementation of this
method does nothing.
So viewDidLayoutSubviews is called every time, that's why we add boolean variable to the function.
override func viewDidLayoutSubviews
{
if(once)
{
once = false
self.chatViewCenterX.constant = 1500
self.view.layoutIfNeeded()
}
}
to show again anywhere
self.chatViewCenterX.constant = 0
self.view.layoutIfNeeded() // viewDidLayoutSubviews is called here.

Custom container does not adjust correctly layout to in-call status bar

I am using my own custom container view controller that is modally
presented from a root view controller (so the container is not the root view controller itself). Root view controller gets its layout handled properly when the in-call status bar is shown. Now if I modally present the custom container with its child (lets say instance of SomeViewController), the child is laid out as expected. If an in-call status bar is shown while the custom container is already presented, the child adjusts correctly.
The problem arises when the in-call status bar is shown while the custom container is not yet presented. If I present the custom container while there is an in-call status bar, the bottom of the child view gets cropped by the size of the extended status bar (i.e., 20 points) - it seems like either the size of the frame is not correct, or there is an offset set to it. If I dismiss in-call status bar, the top gets adjusted to the newly gained space, but the bottom stays cropped.
Following shows the relevant code of the container view:
class ContainerController: UIViewController {
var selectedViewController: UIViewController?
override func viewDidLoad() {
super.viewDidLoad()
if let selectedViewController = selectedViewController {
initialTransition(to: selectedViewController)
}
}
fileprivate func initialTransition(to viewController: UIViewController) {
guard self.isViewLoaded else {
return
}
self.addChildViewController(viewController)
viewController.view.frame = self.view.frame
self.view.addSubview(viewController.view)
viewController.didMove(toParentViewController: self)
}
// rest of the code omitted
}
The container view is presented using this code in the root view controller:
let container = ContainerController()
trainingContainer.selectedViewController = SomeViewController()
self.present(trainingContainer, animated: true, completion: nil)
While there are several SO questions about similar issues (SO question, another SO question, etc.), most of them suggest either solutions that did not work (e.g., old wantsFullScreenLayout and its successors), or seemed a bit too heavyweight (observing status bar did change to adapt layout), especially considering that when the child view controller was presented directly, it was behaving correctly.
After playing around I was able to determine that there was a problem with the frame being set - the frame of the container view controller seemed to be offseted but not resized when the initialTransition(to:) was called (in container's viewDidLoad), thus causing the child the get a frame that overlapped the bottom of the screen by the offset - 20 points.
My first approach was to add setting the frame once again in container's viewDidAppear, which in the end solved the problem, but cause a glitch - for a moment the bottom seemed cropped, and then viewDidAppear was called and the layout was adjusted correctly. This glitch looked bad.
I finally achieved what I wanted by overriding container's viewDidLayoutSubviews and setting the frame of the child there (thus when the container gets notified to adjust its frame to the status bar, the information about a new frame gets passed to the child).
override func viewDidLayoutSubviews() {
self.selectedViewController?.view.frame = self.view.frame
super.viewDidLayoutSubviews()
}

topLayoutGuide equivalent in UIPresentationController?

I have a custom transition for presenting a view controller and I use a UIPresentationController subclass to perform the presentation.
In the presentation controller I add a couple of subviews to its containerView (the chrome). I would like to constrain one of them with something like the topLayoutGuide in order to account for the height of any top bars (like the status bar).
However, UIPresentationController is not a UIViewController subclass and as such it doesn't have a topLayoutGuide property. I tried constraining the views with the presentingViewController's and the presentedViewController's topLayoutGuide but the app crashes because they are not part of the same view hierarchy.
So is there any way to position subviews inside the presentation controller's containerView at its top while still accounting for the height of any top bars?
By trial and error I found out that for topLayoutGuide to work it is important that the added view controllers view height is not larger than the screen size height. In UIViewControllerAnimatedTransitioning in func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
set:
toVC.view.frame = transitionContext.finalFrame(for: toVC)

Size a UIViewController's view to parent UIWindow's bounds

I want to give a UIViewController's view a size that is different from the device's screen size. I know I can usually achieve this by adding the view controller as a child view controller of another parent UIViewController that has defined a frame for the child, but I am in a situation that seems a little different.
I have a UIWindow that only takes up a portion of the screen (it's got a frame that's basically (0, 0, DEVICE_WIDTH, HEIGHT_LESS_THAN_DEVICE_HEIGHT). This window shows up with the proper sizing and positioning when presented. I am setting a view controller as the rootViewController of the window, and then presenting the window by setting its hidden value to false. When this happens, the view controller's view ends up sized to fill the device's screen (i.e. a frame of (0, 0, DEVICE_WIDTH, DEVICE_HEIGHT)).
What I would like is for the view controller to inherit its sizing from the UIWindow it is set as the root view controller of. Is there a way to do this?
I have also tried overriding loadView() and returning a custom-sized view there. Logging the view shows that the view controller's view object is correctly sized during viewDidLoad, but is overwritten with the default size by viewWillAppear:. I would be open to using loadView() to size the view controller if inheriting sizing from the window isn't possible, but I don't know how to make the custom size stick.
Note: The reason why I am trying to add a view controller to the window is because I want to take advantage of the view controller lifecycle methods such as viewDidAppear:, which is why I am not just creating a simple UIView and adding it as a subview of the window.
As counter intuitive as it may seem, if you set set self.view.frame on viewWillAppear (IOS 8) or viewDidAppear (IOS 7) you will be able to make it work.
Swift code (IOS 8):
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// Banner style size, for example
self.view.frame = CGRectMake(0, 0, 320, 50)
}
For IOS 7, I had to use viewDidAppear, which is obviously an unsatisfactory solution. So I had to start the view with alpha = 0.0 and set alpha = 1.0 on viewDidAppear, after modifying self.view.frame.
Hope it helps.

Adjust scroll view's inset within a child view controller

I got a view controller hierarchy with the following parent-child relationship:
UINavigationController (contains) MainViewController (contains) UIPageViewController (contains) UITableViewController
In the inner-most UITableViewController, I have automaticallyAdjustScrollViewInset set to YES, however this setting doesn't seem to work. As can be seen in the screenshot below, the table view's contentInset doesn't seem to be adjusted with the navigation bar.
My goal is to have this table view's contentInset to be automatically adjusted with the outer-most navigation bar. If the navigation bar or the status bar is hidden, I want the content inset to adjust accordingly.
How can I do that? Thanks!
The UINavigationController adjust its child view controller's topLayoutGuide automatically.
In your case it is not being propagated down far enough.
If you make sure this topLayoutGuide makes it down to your table view controller then you won't have to set the contentInset manually.
Also, your view controller hierarchy seems overly complex (I don't know the details of your project). This simplified version of what you have will give you what you are after for free.
Apparently the outer-most UINavigationController is still accessible to the inner-most UITableViewController via its navigationController property.
Because the navigation bar's frame is accessible by the inner-most view controller, we can do something like this in its viewDidLayoutSubviews.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.tableView.contentInset = UIEdgeInsetsMake(CGRectGetMaxY(self.navigationController.navigationBar.frame), 0, 0, 0);
self.tableView.scrollIndicatorInsets = self.tableView.contentInset;
}

Resources