Search code examples
iosswiftuiviewcontrollerautolayout

topLayoutGuide applied after viewWillAppear


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?


Solution

  • 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:

    1. Have a boolean property in your view controller that you set to true initially:

      var isInitialLayoutPass: Bool = true 
      
    2. 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)
          }
      }
      
    3. 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.