Force Auto layout to update UIView frame correctly at viewDidLoad
Asked Answered
E

3

5

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.

Eager answered 15/9, 2017 at 1:30 Comment(6)
viewDidLoad is not the place to set up things which require knowledge of frames, that is why you are having this problemTeaching
@Teaching I'm aware of this. Due to a specific scenario I need to solve this (and it's solvable).. I'm just making sure I've solved it the right wayEager
What is it you armé trying to do with the frame this early in the view's lifecycle?Sword
@DavidRönnqvist I need to preload some CIImages that depends on the subview frame. I need to preload them as early as possible to provide the best experience to the user (he might use them right away)Eager
Also, the only 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. The timing has changed, but in a less robust way than it would be to put the same code in either of those methods (because it relies on timing - that wouldn't be true if the view was loaded but not added as a sub view)Sword
@DavidRönnqvist I think I get your point. Bottom line, what would you recommend doing? I've noticed using dispatch async at didLoad, does not give reliable results. What is my best option than? (I need it to run only once, and as early as possible)Eager
L
7

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.

Leninism answered 29/12, 2017 at 4:46 Comment(0)
B
2

Your question is similar to this problem, and my answer will also be the same.

The frame is not guaranteed to be the same in viewDidLoad as it will be when the view is eventually displayed. UIKit adjusts the frame of your view controller's view before presenting it, based on the context in which will appear. For a better understanding of view lifecycle, you can refer this image. enter image description here

The image will help you to understand why your code is working. As the picture shows that viewWillAppear gets called once a view is loaded and when you set up dispatch async it is added to the thread asynchronously after the execution of viewWillAppear. So, once viewWillAppear is called your frames are updated as per your view, and you get the correct frame in dispatch async.

References: Image source

For more information about view life cycle you can visit this answer

So, at last, you can go for either of two options:

  1. If you want to use auto layout then manage frames either in viewDidLayoutSubviews or viewWillAppear. (In either of these viewDidLayoutSubview one should go for as viewWillAppear will be called every time your view comes at top of view hierarchy that’s it should not be preferred).
  2. If you're going to skip auto layout, then you can go for creating and maintaining views programmatically.

Hope this helps!

Bonnard answered 2/1, 2018 at 18:57 Comment(6)
You should take a look at this question #14060502. I think viewWillAppear isn't right choice for (1).Leninism
Agree, That’s why I mentioned either. I will edit it to be more precise. Thanks for pointing it out.Bonnard
Seem like have misunderstanding here ;). I mean When using autolayout in ios6, frames are not set until subviews have been laid out. The right place to get frame data under these conditions is in the viewController method viewDidLayoutSubviews. It's update (ios6) in the answer.Leninism
Its fine I Understand that but nowadays nobody or near to nobody is using ios6 or below.Bonnard
It's not ios6 or below. It means ios6 and above.Leninism
Yeah I know. But its not required to mention as nobody nowadays is using ios6 or below. Almost everybody is using 6 and above only. And more importantly i have added reference of that answer for more detailed information. One can check that as well. Hope this clears your doubt!Bonnard
T
1

After reading the comments, maybe you can try to embed your view controller into another one with a container and do something like this:

  • In the parent view controller add a variable: var viewSize : CGSize?
  • In the child view controller add a variable: var parentViewSize : CGSize?
  • In the parent view controller, get the size in the viewDidLayoutSubViews and store it: viewSize = view.size
  • In the parent view controller, in the prepareForSegue send the size you stored: (destinationViewController as? MyViewController)?.parentViewSize = viewSize
  • In the child view controller, in the viewDidLoad you will access the parentViewSize variable with the good value in it

Would it do it?

Tan answered 28/12, 2017 at 15:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.