1

It appears that presenting and dismissing a view controller both prompt the presenting view to layout its subviews and/or update its constraints. With a heavy view hierarchy, this is introducing performance issues. Again - this is the existing, currently displayed view. The modal being created and displayed is very light.

This occurs whether I use autolayout (as in my example project) or not.

I have built a demo project that approximates an app I am working on. There is a main parent controller with a horizontally scrolling UIScrollView. Multiple child controllers are added to the parent controller, and their views are added to the scrollview and arranged using NSLayoutConstraints. Each child view has one subview itself, a simple UIView, also arranged with a constraint.

In the navigation bar, there is a button to launch a modal. When presented, the parent controller makes a call to setNeedsLayout on each child view, multiple times. In my demo project, I am overriding setNeedsLayout to log when it is accessed. The same occurs when closing the modal. Open and close the modal a few times and observe the console.

I can see no reason why a new layout is needed, and with more complex views I am finding that hundreds of these calls are firing, with a noticeable performance impact.

Note that when the layout code from ChildView is omitted, setNeedsLayout is not called. I encourage you to comment out the constraints and see the difference in the logging.

Why is this happening? How can I prevent an unnecessary layout pass when presenting and dismissing a modal?

Ben Packard
  • 26,102
  • 25
  • 102
  • 183

3 Answers3

3

First of all, you are logging setNeedsLayout, which is just a flagging mechanism and does not really incur any work yet. Multiple calls to setNeedsLayout may only trigger a single layout. You should be logging -[UIView layoutSubviews] or -[UIViewController viewDidLayoutSubviews] instead, because these are where the actual heavy-lifting happen.

Second, layout-related methods are meant to be called repeatedly and rapidly during presentations because:

  1. The window needs to rotate all its subviews to respect the presented view controller's preferred interface orientation.
  2. Animations will need to know the initial and final states of your views.
  3. When layouts happen on parent views for whatever reason, all their subviews (which may include views of your view controllers) will of course need to update their layouts too.

If you want to minimize the number of layout passes, you can try give up using presentViewController:animated: and instead use addChildViewController: and animate just the necessary views manually. But even then, you may still trigger the parent controller's layout anyway.

John Estropia
  • 17,460
  • 4
  • 46
  • 50
  • Thanks - but still not clear why any of these three are necessary when a modal is presented? Animating my own child view controller is an interesting idea though, I wonder if it too would trigger a layout pass. I might check. – Ben Packard Apr 20 '14 at 22:38
  • 1
    1 and 2 will trigger because even if the presenter won't change, in order to do rotations/animations the layer system needs to know the very final state of the view underneath (there might be something else in mid-animation, for example). It's safer for the view controllers to handle all cases similarly (layout code is expected to be lightweight), than to make assumptions on whether to update layout or not. A better way to handle this is to look for tight loops in your layout code, and prevent triggering subview layouts unless you are already sure of their final frames. – John Estropia Apr 21 '14 at 01:44
1

You are doing a very, very, very odd thing: you're maintaining a custom parent view controller with 10 child view controllers all of whose views are in the interface simultaneously. View controllers are not designed for that sort of thing. It is this that is triggering the multiple layoutSubviews calls that you are seeing. It is fine to have multiple child view controllers, but their views should not all be in the hierarchy - especially in your case, where only one such child view is actually visible.

In fact, the interface that you've constructed - a paging scroll view, each of whose "pages" is a view managed by a view controller - is already implemented for you by UIPageViewController, which is far more efficient, as it only actually maintains at most three view controllers at a time: the view controller managing the visible view within the scroll view, and the view controllers managing the views to its right and left. It is also wonderfully convenient and easy to use.

So either:

  • You should use UIPageViewController, or

  • You should imitate what UIPageViewController does, removing view controllers' views (and perhaps even releasing the view controllers) when they have scrolled out of sight - as we had to do in the days before UIPageViewController existed - see the Advanced Scroll View Techniques video from WWDC 2011. My Latin "flashcard" app worked this way before UIPageViewController came along; it has thousands of vocabulary cards, each of which is managed by a view controller, but only a maximum of three card view controllers ever exist at any one moment.

(By the way, you should also not be using your own self.childControllers mutable array, as this list is already maintained for you as self.childViewControllers.)

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Great answer. You should write a book or something ;) – jrturton Apr 20 '14 at 18:41
  • This is more a review of the application architecture than it is an answer for *why* the views must be laid out. As for your recommendations - imagine an application where initial load time and memory use are secondary concerns to scrolling performance. And lets say each view is sufficiently complex that lazy loading on scroll introduces dropped frames (this is easily reproducible and not an academic assertion). Now it becomes preferable to load all ten controllers and views on launch. The scrolling is smooth but having to wait while presenting a modal becomes the sole issue. – Ben Packard Apr 20 '14 at 22:28
  • PS - the lazy loading approach was my original design and I think it is the correct approach in most cases. But given a fixed number of heavy views, and insisting on avoiding 'juddering' on scroll, I'm not so sure. Also, as an interesting (to me) aside... if you prioritize scroll performance, and implement recycling of views and controllers, the bottleneck actually becomes adding the (already existing) subview. I've found that it becomes better to keep the view in the hierarchy, hide it, and then move it to the correct location when it is recycled. – Ben Packard Apr 20 '14 at 22:30
  • For posterity and having tested some stuff - the performance on UIPagedViewController is fairly poor so I would recommend option 2 of those above. If scrolling performance is important, UIPagedViewController will not re-use your controllers or views. Even if you handle recycling yourself in the datasource, there is an impact of removing and adding the views. My best performance so far has come from re-using controllers and moving views instead of removing them. It is a shame since I have basically had to re-write UIPagedViewController entirely to provide this functionality. – Ben Packard Apr 21 '14 at 02:31
0

I think layoutSubviews is getting called because the presenting controller's view changes superviews while animating out of the screen once hidden by the presented view.

If you want to avid skip layoutSubviews when the frame hasn't changed, just save a reference to the last frame and if equal return without doing anything. Also there is no need to call setNeedslayout on subviews as the system will trigger it automatically if you resize them.

Anyway, your main problem is your approach:

  1. Only use view controllers if you're going to use them as such (inside a tab bar controller, pushed to a navigation controller, as a window's rootController, presented modally, etc.). If you want to manually add views do not use view controllers and just use custom views! This is a very common error and you can see more details here.

  2. Load views and objects lazily and reuse them. For instance you should only load 1~3 pages of contents and load new ones only when the user scrolls to them. When loading a new one remove one of the old views, or better yet reuse it.


You can separate the logic not only with controllers but also with custom views. Some reasons why you should not use controllers in your particular case:

  • Controllers won't get retained by a container controller or window as you're manually adding their views.
  • Controllers won't get orientation, memory, viewDidAppear, etc., events. Again because you're not using them as proper view controllers.

If you properly implemented a custom container controller (which is a lot of work to do properly), then you could use controllers. Otherwise stick to custom views.

Community
  • 1
  • 1
Rivera
  • 10,792
  • 3
  • 58
  • 102
  • Thanks, that's helpful and I will investigate. Do you know where/why the presenting view moves to during in a presentation? Is that part of how transitions work? Regarding your implementation points - 1) I think the presence of view controller containment undermines this argument. If for example you have very different child views with very different functionality, its better that each controller holds the behavior. 2) See my comment to @matt's answer - recycling and demand loading is usually better but in some instances I think advance loading can be justified. – Ben Packard Apr 21 '14 at 01:21
  • Advance loading yes (my "1~3 pages" part), full loading not. – Rivera Apr 21 '14 at 01:38
  • A 5 page app, heavy, complex views, no on-scroll frame drops allowed. I would use full loading for an example like that (except for the modal presentation issue, hence the question). – Ben Packard Apr 21 '14 at 01:42