Efficient scrolling UIStackView
Or how to build scrolling stack container and keep an healthy usage of the memory.
In these days mobile UIs became a complex job; lists (tables or, more often, collections) may contains heterogeneous groups of items, showing in a single scroll interaction a great amount of data.
Take for example the IMDB application; the home page contains:
- an horizontal list with the highlighted movies
- highlighted news
- an horizontal list with photo gallery
- an horizontal list with nearby movies
- another horizontal list with coming soon movies
- vertical list of news
- … and yeah, much more stuff
everything inside a single vertical scroll view!
Usually, if you still lack attention while writing this kind of code, your view controllers are likely to become a massive piece of spaghetti code, assembling several responsibilities and making your app more fragile and much less testable.
That’s the world of Massive View Controllers and the main reason behind alternative architectures like Viper, MMVM and several others.
Separation of concerns (along with Single Responsibility Principle) is a design principle for separating a computer program into distinct sections, such that each section addresses a separate concern.
The easiest way to bring these kind of layouts to home is to use table or collection and put the objects inside (ie. rich cells with complex layout inside); while it substantially works with few objects, with a fair number of heterogeneous objects, the resulting architecture is a crazy asylum you may avoid.
<a class="markup--anchor markup--p-anchor" href="https://github.com/justeat/ScrollingStackViewController" target="_blank" rel="noopener" data-href="https://github.com/justeat/ScrollingStackViewController">ScrollingStackViewController</a> class by JustEat UK uses the approach of a
UIScrollView in order to simulate something like vertical layout of Android.
As the ReadMe states, its a more suitable approach in situations when you’re building a scrolling controller with a limited number or dynamic and rich cells.
However when your layout became complex the resulting memory footprint maybe relevant or unsustainable.
This is the case where one or more sub-viewcontrollers contains vertical tables or collections.
In fact, in order to make them work properly, you need to expand these views in order to show the entire inner tables/collections content and it ends with huge allocations of the cells inside (even if are not currently visible): a big fuck you to table’s caching.
See the example below:
The work below was inspired by an old post of Ole Begamnn “Scroll Views Inside Scroll Views” which I’ve heavily used years ago at CreoLabs while I was making an
UITableView clone for CreoKIT.
The main difference is it works with UIViewControllers and manage correctly then where these are containers of UIScrollView subclasses (like UITableView or UICollectionView); and yeah, it works with Swift 3+.
Basically it’s a
UIViewController which expects an
UIScrollView inside (you may design it via storyboard and link the outlet, so you are free to keep your own custom layout).
It allows to achieve the following things:
- Place multiple UIViewControllers (views) below each other that their scrolling behavior still feels perfectly normal. If views are table or vertical collections the built-cell reuse functionality is not affected and works out of the box as you may expect.
- Turn one complex UITableViewDataSource/UICollectionViewDataSource into several different UIViewControllers which manages their homogeneous (?) data independently.
This is a first version; my main goal was to have an efficient stack container; there are no trinkets or special features (but, even with your help, it will get better, iteration after iteration).
For example it currently supports only vertical stacking.
UICollectionView are subclasses of
UIScrollView; they mostly behave like standard scroll views but the key difference is the recycle operation made with inner cells.
As the view is scrolled, cells that scroll of the screen get removed from the view hierarchy and added to an internal pool; whenever a new cell is scrolled into a view, the table view dequeues a cell from the pool and re-adds it to the view hierarchy (this is the reason behind the prepareForReuse() function you can override).
This efficient design:
- renders only visible cells (your table may contains hundrends or thousand cells and it does not affect the performance of your app)
- avoid unnecessary cell allocations
How it works
When you stack tables or collections in a parent scrollview, in order to keep the standard behaviour, you necessary need to expand the content (your’s table height is the same of the content it provides) and it means you have fucked off the recycler algorithm.
The trick I’ve used in ScrollingStackContainer allows me to keep alive the standard cell recycler algorithm while I can stack multiple scroll views one below the other inside a parent UIScrollView with the illusion of a single, continuous scrolling.
- simple UIView are allocated and stacked entirely ( frame is not changed)
- a special consideration is done for UIViewController which contains UIScrollView.
In the last case, when the parent UIScrollView (the most outer) did scroll, the algorithm iterate over each stacked UIViewController’s view; if it’s not visible the height of the inner UIScrollView is set to zero (and it means: “you don’t need to allocate anything inside the scroll!”).
When the view became partially visible the inner UIScrollView’s frame is set to cover exactly only the visible region inside the parent scrollview (also the offset is adjusted automatically to simulate a continuous scroll, as if it were a single table).
At its maximum the Inner
UIScrollView may cover the entire parent; also in this case only the visible region of the inner scroll is allocated.
Mantra is: only visible cells are allocated; everything (can scroll) you can’t see does not use your device’s memory.
With this accurate layout management you are free to create complex layouts without worrying of unnecessary memory usage.
How to use Scrolling Stack Container
ScrollingStackContainer is pretty easy to use.
For fixed height view you should set a valid height (grater than zero). This can be done in the following ways:
- set an height constraint on UIViewController’s .view
- … or return a value into preferredContentSizefunction
- … or set the height via self.frame
- … or by implementing the StackContainableprotocol and returning .view(height: <height>)with a valid value.
For UIViewController which contains inner’s scroll views (table or collections) you are forced to implement the StackContainbleprotocol by returning .scroll(<inner scroll instance>, <insets of the inner scroll in superview>).
That’s all! All the business logic is managed by the class and you can enjoy!
As usual <a class="markup--anchor markup--p-anchor" href="https://github.com/malcommac/ScrollingStackContainer" target="_blank" rel="noopener" data-href="https://github.com/malcommac/ScrollingStackContainer">ScrollingStackContainer</a> is available on my GitHub account along with an small example project. Feel free to post your PRs and Issues, I’ll be happy to discuss with you further evolutions of the class.