Search code examples
iosswiftuiviewautolayoutviewdidload

Force Auto layout to update UIView frame correctly at viewDidLoad


As simple as it might be, I still find my self struggling with the correct solution.

I'm trying to understand what is the correct way to find the REAL UIView(or any other subview) frame inside viewDidLoad when using Auto Layout.

The main issue is that in viewDidLoad, the views aren't applied their constraints. I know that the "known" answer for this situation is

override func viewDidLoad() {
        super.viewDidLoad()
     view.layoutIfNeeded()
     stepContainer.layoutIfNeeded() // We need this Subview real frame!
     let realFrame = stepContainer.frame
}

But I found out that it's not ALWAYS working, and from time to time it give's wrong frame (ie not the final frame that is displayed).

After some more researching I found that warping this code under DispatchQueue.main.async { } gives accurate result. But I'm not sure if it's the correct way to handle that, or am I causing some kind of under-the-hood issues using this. Final "working" code:

override func viewDidLoad() {
        super.viewDidLoad() 

        DispatchQueue.main.async {
            self. stepContainer.layoutIfNeeded()
            print(self. stepContainer.frame) // Real frame..
        }

}

NOTE : I need to find what is the real frame only from viewDidLoad, please don't suggest to use viewDidAppear/layoutSubviews etc.


Solution

  • As @DavidRönnqvist pointed out

    The reason dispatch async gives you the "correct" value here is that it get scheduled to run in the next run loop; after viewWillAppear and viewDidLayoutSubviews has run.

    Example

    override func viewDidLoad() {
      super.viewDidLoad()
      DispatchQueue.main.async {
        print("DispatchQueue.main.async viewDidLoad")
      }
    }
    
    override func viewWillAppear(_ animated: Bool) {
      super.viewWillAppear(animated)
    
      print("viewWillAppear")
    }
    
    override func viewDidAppear(_ animated: Bool) {
      super.viewDidAppear(animated)
    
      print("viewDidAppear")
    }
    
    override func viewDidLayoutSubviews() {
      super.viewDidLayoutSubviews()
    
      print("viewDidLayoutSubviews")
    }
    

    viewWillAppear

    viewDidLayoutSubviews

    viewDidAppear

    DispatchQueue.main.async viewDidLoad

    Code inside DispatchQueue.main.async from viewDidLoad even is called after viewDidAppear. So using DispatchQueue.main.async in viewDidLoad gives you right frame but it isn't earliest as possible.

    Answer

    • If you want to get right frame as early as possible, viewDidLayoutSubviews is the correct place to do it.

    • If you have to put some code inside viewDidLoad, you are doing right way. Seem like DispatchQueue.main.async is the best way to do it.