6

First, I should mention, this is mostly an efficiency issue.

There are many discussions as to where to do frame calculations where viewWillAppear is too early and viewDidAppear is too late (view is already visible).

The common answer is to do frame calculations in viewDidLayoutSubviews. Problem is, it gets called multiple times. Worse, the most accurate call, the one where all frames have their final size is the last one. To my knowledge there is no way to know which call is that final one.

We use to have a 'framesAreSet' flag (initialized false) and a check, where if frame is not zero (something like self.view.frame.size.width != 0) and 'framesAreSet' is false, it goes in, turn the flag and calculate only once. Something like:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];

    if (self.view.frame.size.width != 0 && !framesAreSet)
    {
        framesAreSet = true;

        //Calculate frames here
    }
} 

This looks ok, but in truth, a check for something like self.view.frame.size.width != 0 does not guarantee that frames are indeed set. The fact that viewDidLayoutSubviews will get called after suggests that some frames were not set to their final state.

Would be great to have a viewCompleteLayoutSubviews. Any ideas of what's the best way to accomplish a 'one time' frame calculations when all frames are set and view is not yet visible?

(This is probably an issue with views not using NSConstraints)

bauerMusic
  • 5,470
  • 5
  • 38
  • 53
  • 1
    Why is it a problem that `viewDidLayoutSubviews` is called more than once? What problem are you facing? – rmaddy Nov 26 '15 at 15:34
  • Are you by any chance updating the bounds of `self.view` in there? If so that's your problem. – Clafou Nov 26 '15 at 15:45
  • @rmaddy Redundancy is not good coding practice. Suppose you have a complex ViewController, adding many subviews in code (in fact created mostly in code) and do many calculations for settings frames. In fact, imagine having 'heavy' calculations calls as many times as adding subviews plus. Mind you, all but the last one are redundant. Sometimes 'it works' isn't good enough. – bauerMusic Nov 26 '15 at 16:01
  • So your issue is strictly one of efficiency. You should make that clear in your question. One thing I've done in the past is to track the `self.view.frame.size` and inside `viewDidLayoutSubviews` I only do calculations if the size has changed since the last call. – rmaddy Nov 26 '15 at 16:03
  • @rmaddy Updated question. Thanks. – bauerMusic Nov 26 '15 at 16:10
  • Are you doing something that would trigger layout early? E.g. calling `layoutIfNeeded` or `systemLayoutSizeFittingSize:` or (perhaps) `intrinsicContentSize`? There might be other things that trigger extra layout passes that I'm not aware of. Put a breakpoint in `viewDidLayoutSubviews` to see if you can figure out why it's getting called extra times. – rob mayoff Nov 26 '15 at 16:42
  • @robmayoff See http://stackoverflow.com/a/5330162/3276518 – bauerMusic Nov 26 '15 at 18:50
  • I know what causes the `needsLayout` flag to be set. What is your app doing to cause layout to happen more than once? Normally the layout phase happens at most once per turn of the event loop, so perhaps you are doing something to trigger extra layout phases. Just getting the `needsLayout` flag set isn't enough to cause layout to run more than once. – rob mayoff Nov 26 '15 at 19:01
  • @robmayoff No `needsLayout` or `needsLayout`. Which isn't the point really. It is normal for `viewDidLayoutSubviews` to be called multiple times. Unless, you're suggesting finding the last call (on my end) that cause `viewDidLayoutSubviews` to be called. That could work if `viewDidLayoutSubviews` is always triggered by an action on my end – bauerMusic Nov 27 '15 at 17:03

3 Answers3

2

The app gets only one didLayoutSubviews per draw (up to that point, view state changes are just noted with setNeedsLayout). In that sense, didLayoutSubviews is your idea of didCompleteLayoutOfSubviews. After the draw, if another view state change happens, the layout is incomplete again.

In other words, number of didLayout calls doesn't depend on the number of subview adds or frame changes, it depends on the number of draws (not to be confused with the run loop). Before a draw, if the needsLayout flag has been set, layoutSubviews and then didLayoutSubviews will be called exactly once, no matter how much the view hierarchy has been rearranged.

danh
  • 62,181
  • 10
  • 95
  • 136
  • If I understand correctly, the numbers of calls for `didLayoutSubviews` depends (apart from the initial one), on my alteration (change frame, add subview) in code. That is, it's predictable and I can have a flag after my last 'didLayoutSubviews trigger', after which `didLayoutSubviews` will be called for the final time (for that cycle)? – bauerMusic Nov 28 '15 at 13:28
  • 2
    I don't think your understanding is quite right. The number of didLayout calls doesn't depend on the number of subview adds or frame changes, it depends on the number of draws (not to be confused with the run loop). Before a draw, if the needsLayout flag has been set, layoutSubviews and then didLayoutSubviews will be called exactly once, no matter how much the view hierarchy has been rearranged. – danh Nov 28 '15 at 18:28
  • Good answer. It'd be great if you could add your last comment to your answer, I'll mark it as 'accepted'. I need to find where my views are called to draw after the initial 'layoutSubviews'. But this is somewhat a different question. Thanks! – bauerMusic Nov 28 '15 at 21:34
  • @bauerMusic - hope it was helpful. If you want, post the code that's generating layouts (and maybe the layout code,too). Maybe we can improve it. – danh Nov 29 '15 at 03:54
  • Wish I could do that (company wise). In fact, this issue is not any project-specific, been thinking about it through various projects. I am doing something very similar, creating a test case where I can pinpoint what's causing a view-draw to call `layoutSubviews` (or where in the life cycle). So far, I found that adding a UILabel trigger a second call. A xib view ref, changing its frame, or color in `viewDidLoad` or `viewWillAppear` does not. – bauerMusic Nov 29 '15 at 05:05
1

You should always re-layout for the dimension your view currently has.

You set the frames, then rotate the device. Do you need to recompute the frames? Sure you do, the bounds size has changed! You are making dangerous assumptions with your framesAreSet.

If performance really is key for you, you may want to cache the computed frames an some dictionary in invalidate those if the bounds size changes.

But you are certainly not going to get away with performing layout only once.

An alternative would be to make the view controller's view an instance of a custom class and override -layoutSubviews in there.

Christian Schnorr
  • 10,768
  • 8
  • 48
  • 83
  • _"You are making dangerous assumptions with your framesAreSet"_. At some point, it is set, like say, `viewDidAppear`. Consider a view that does not rotate, does it make sense to run the same frames calculation multiple times? Worse, if a view does rotate, should every rotation run the frames calculation multiple time, where the only useful time it the last one? – bauerMusic Nov 28 '15 at 13:11
  • "Consider a view that does not rotate, does it make sense to run the same frames calculation multiple times?" - Yes, there are other things that can cause your view to change its size. – Christian Schnorr Nov 28 '15 at 20:52
0

You can calculate frame in viewDidLayoutSubviews method using dispatch once token (GCD) that if frames are initialized once this will not get inside your dispatchonce block.

Abhishek
  • 509
  • 3
  • 12
  • Great point. However, for my purpose, would not behave any different than a flag. Also, there's the issue of 'catching' the last and most updated `viewDidLayoutSubviews` call. – bauerMusic Nov 26 '15 at 18:36
  • But i dont think that we can get the last call in viewDisLayoutSubview ....you can do frame calculations in viewWillAppear. – Abhishek Nov 26 '15 at 19:41
  • As I wrote, in different conditions, `viewWillAppear` frames do not have their final size. – bauerMusic Nov 28 '15 at 13:14
  • Can you tell what exactly what you are trying to do , so that i can have better understanding and so we can reach to solution of this. – Abhishek Nov 28 '15 at 13:35
  • dispatch_once DOES behave differently than a regular flag, it is thread safe while the flag is not. – Christian Schnorr Nov 28 '15 at 21:51