2

Where to put manual layout code (or where to trigger it) for a specific child UIView I have, when the parent views themselves utilise autolayout?

Notes:

  1. layout code for the view needs to use its frame dimensions after any parent view layout, orientation change, trait change etc
  2. I had tried using viewWillLayoutSubviews on the parent viewcontroller but was having difficulty where it was called more than once at times - couldn't track down why however do NOT want to have to do manual layout calculations more than once as they are onerous.
  3. So overall a way to for the various scenarios below end up with only one single "triggering" of the manual layout AND at a time when all rotation & parent view layout has been done:

So:

  • on load (viewDidLoad)
  • redisplay (like view did appear subsequently)
  • rotation
  • trait change

For example should I be looking at a way for:

a) parent view controller to control this and determine when to send a "layoutSubviewNow" call to my specific child view (which is fronted by it's own viewcontroller), but in which case how to do this, or b)

b) leaving it the child view to react on a "layoutSubview()" and then have a delegate available to call back to the main controller to request layout data?, or

c) similar to b) but using the child views view controller "viewWillLayoutSubviews()" to trigger and then have a delegate available to call back to the main controller to request layout data?

or other?

actually I'm thinking of this idea:

d) in the main parent view controller: have a instance variable viewTransistionedOutstandingAction, and use it such that (i) in viewWillTransition set this to true, then in (ii) viewWillLayoutSubviews do the following:

if viewTransistionedOutstandingAction {
  self.triggerModelChange()
  viewTransistionedOutstandingAction = false
}
Greg
  • 34,042
  • 79
  • 253
  • 454
  • Whatever you want to achieve, please explain with screenshot or code chunk. What you are explaining above can be understood by you only because you know what result you want. Simply if someone pick any conceptual answer then it might be wrong for your problem. – Mahesh Agrawal Feb 23 '17 at 07:23
  • Ok. Maybe some pseudo code – Greg Feb 23 '17 at 07:24
  • can you tell me simply, you want a trigger function to be called once after any change in layout constraint in the controller? is this what you want? – Mahesh Agrawal Feb 23 '17 at 07:25
  • Pretty much, but at a time where orientation changes and parent layout has occurred. Then the trigger calls the child controller to tell it to do its layout calculations. – Greg Feb 23 '17 at 07:28
  • check this out http://stackoverflow.com/a/26374669/4030971 – Mahesh Agrawal Feb 23 '17 at 07:33
  • not sure how the "cancelPreviousPerformRequestsWithTarget" works exactly and if there's scope for requests to cancelled mid-stream...but does seem like another way to do what I mentioned at option (d) I think – Greg Feb 23 '17 at 07:45

1 Answers1

4

It's a little unclear to me what exactly your requirements are but I can provide an answer for your very first question:

Where to put manual layout code (or where to trigger it) for a specific child UIView I have, when the parent views themselves utilise autolayout?

There is exactly one place in a UIView implementation where you should put any layout code and that is inside the method layoutSubviews().

The layout engine (the thing that computes the actual frames from your constraints) calls this method right after it has computed the correct frame for this particular view. With other word: layoutSubviews() is the one and only place during the layout process where you can be sure that the view already has its correct frame set.

You are correct that this method might be called not just once but several times. That's because in some cases several layout passes are needed to compute the final layout. (Example: You have some view whose height depends on its superview's width but the superview's height depends on the subview's height.) The layoutSubviews() method might also be called manually from code, e.g. calling setNeedsLayout() followed by layoutIfNeeded() on a view will result in a layoutSubviews() call on that view as well.

However, the system calls layoutSubviews() only as often as it needs to (unless you manually trigger it over and over again) i.e. whenever the current layout is invalidated. There can be many causes for this: Device rotations, constraint changes, changes in the view hierarchy etc. But these are the cases where your view's frame might change and if your custom layout depends on that frame it actually should be recomputed as well.

For that reason layoutSubviews() is just the right place to perform your manual layout and you should refrain from doing it anywhere else.


Note: If you're dealing with view controllers instead of plain views the corresponding method to perform your layout updates is viewWillLayoutSubviews() (or viewDidLayoutSubviews() if you are using Auto Layout and only want to make slight modifications after the layout engine has resolved the constraints for all subviews).

Mischa
  • 15,816
  • 8
  • 59
  • 117
  • thanks Mischa - just wondering then how do people address the overhead of doing layout calculations (e.g. I effectively have a DIY custom scrolling calendar) more than once re layoutSubviews being called more than once? – Greg Mar 01 '17 at 23:29
  • I'm not entirely sure what you mean by "overhead". There are only two cases: 1. Your layout is dependent on the view's frame. Then you _need_ to react to each frame change! Yes, `layoutSubviews()` is called again but there is no overhead. 2. Your layout is independent of the view's frame (which is almost never the case). Then you can do it just once, e.g. during the view's initialization. – Mischa Mar 01 '17 at 23:37
  • *Supplement:* There might be cases where `layoutSubviews()` is called even though the view's frame didn't change. That's the only case where you _would_ have an overhead. You can tackle this problem by storing the view's current frame in a custom property `previousFrame` and only execute your custom layout code if `frame != previousFrame`. – Mischa Mar 01 '17 at 23:43
  • thanks - that makes sense and sounds like a good approach. So you would implement this in the child's viewDidLayoutSubviews(), on the assumption that the parent/enclosing frames have already been calculated at this point correct? – Greg Mar 01 '17 at 23:47
  • Yes. (If you're talking about a view *controller*.) – Mischa Mar 01 '17 at 23:54