In this article, I want to present an
OLEContainerScrollView , which is a descendant of
UIScrollView and allows you to add several scroll views, tables (
UITableView ) or collections (
UICollectionView ) into one container.
Possible use
You can use the
OLEContainerScrollView to achieve the following goals:
- Placing several scroll views (or tables, or collections) one below the other so that their usual behavior during scrolling does not suffer. In the case of tables or collections, we are talking about preserving the functionality of the cell reuse mechanism.
- Transform one complex UITableViewDataSource or UICollectionViewDataSource into several simple data sources by splitting a table or collections, consisting of several sections, into several single-section tables or collections, arranged one after another.
- Adding a header or footer ( header or footer ) above or below the collection without having to manage their markup. In this case, it will be a simple UIScrollViews or UIViews .
Reuse cells in tables and collections
Before considering the implementation of my class, let's take a look at how tables or collections work at all. Both classes,
UITableView and
UICollectionView , are descendants of
UIScrollView and behave similarly. However, the key difference is that the tables and collections
reuse their cells . When the
view scrolls and the cell goes off the screen, this cell is removed from the
view hierarchy (in other words, the message
removeFromSuperview is sent to
it ) and transferred to the queue for reuse (reuse queue). At the same time, before the new cell should appear, the table calls the free cell from the waiting queue of the reuse and re-places it on its hierarchy of views. This approach avoids significant memory consumption, minimizes the number of costly creation and allocation (allocation) operations in the memory of new types and ensures the fastest possible scrolling.
')
Illustration of reuse cells in a UITableView . Invisible cells are removed from the view hierarchy (indicated by a blue dotted line) and added to the table view before being displayed as a result of scrolling. Note that the table frame (light blue rectangle) is smaller than the content size (content size, circled in red dotted line), as is usually the case with scroll view. Download video (in H.264 format) .
A simple approach to the implementation of the container
Placing multiple scroll views in one common container, also being a scroll view, is a fairly simple task:
- Create a UIScrollView that will be the container for the nested scroll view.
- Add nested scroll views to the container. It can be as simple scroll view, and tables or collections.
- Set the nested scroll view frame so that they contain all their content ( contentSize ). Set scroll views one above the other.
- Set the content size ( contentSize ) of the container as the sum of the frames of the nested views.
Thus, when we set each
frame scroll view size
frame larger or equal to the size of their content, we get a situation in which these scroll view will never scroll - the only scrolling object will be the container. This avoids interference with touch processing between nested scroll views and their container.
This scheme certainly works, but there is one major drawback: excessive memory waste. If nested scroll views are tables or collections, then they create a cell for each row, because all the cells in their understanding are visible. If the collection contains hundreds or thousands of cells, then the effect will be truly dramatic: scrolling will slow down, and your application will consume all the available memory on the device.
An example of a simple approach to implementing a container for several scroll views. Two tables are added as subviews into one common container, which itself is a scroll view (rectangle with black stroke). The frames of the tables (light blue and light yellow rectangles) are modified to fully accommodate the content of each table (red dotted outline). Notice how it affected the reuse of cells - it stopped, all cells of each row are present at the same time, regardless of whether the cell is visible (inside the frame of the container, black rectangle) or not. Download video (in H.264 format) .
OLEContainerScrollView
Now I will talk about my scroll view container.
OLEContainerScrollView is a descendant of
UIScrollView , which automatically organizes nested views in a deck or stack style (similar to
NSStackView on OS X). This container works with all types of views, not only with scroll views, although it handles the scroll view in a special way.
Adding Views
To add a view to a container, you need to use the
addSubviewToContainer: method. I was forced to create new methods to add and remove views to the container, rather than relying on the existing
addSubview: / removeFromSuperview pair , because I wanted to add views to the private
contentView , not directly to the container. This technique allowed us to avoid interference from private subspecies of the
UIScrollView itself, which are created to display scroll indicators, when we later search through nested types to adjust their sizes.
As soon as the view is added to the container, the following happens:
- If a new view is an instance of UIScrollView (or a descendant), then its own scrolling is disabled by using the property subview.scrollEnabled = NO; . So only our container will handle the scrolling gestures.
- Subscribe to KVO notifications (notifications) about resizing nested views. For normal UIViews , we observe changes in the frames and bounds properties. For scroll view, we observe the changes in the size of the content through the contentSize property. This must be done in order to subsequently customize (relayout) the nested views when they change their sizes from the outside.
Align while scrolling
During scrolling, the container continuously aligns the
frames of the views nested in it in the following way:
- Enumerates all the nested views in the order from the appendix and positions them in a pile: each next view after the previous one. The width of all species is adjusted to the width of the container so that the species fits in container 1 . For ordinary UIViews , space is allocated according to the height of their frame . For scroll view, enough space is allocated to accommodate their content; height is taken from contentSize.height . This means that the view following the scroll view will be placed in the container so that the entire contents of the scroll view fits in it.
- The size of the content of the container consists of the size of the content of all nested views.
This still corresponds to the simple approach described earlier. Now we need to align the
frames of all scroll views in the container so that they get the minimum size that will allow to fill the viewport of the container (
according to bounds ):
- To do this, we define for each nested scroll view in container 2 how their content areas intersect with the current scope of the container, and set the frame for them accordingly. This means that the frame of any scroll view will never be larger than the size of the container, and that views that are not currently in scope will have a frame with a height of 0.
Take a look at the video below to see how this algorithm works. At the beginning, the first table fills the entire scope of the container (marked with a black stroke) - the table
frame (light blue rectangle) is exactly equal to the
bounds of the container. The second table is completely out of scope - its
frame has a height of 0, the table is invisible. As a result, there is no need to create cells in it (yellow dotted line).
As soon as the user scrolls the contents of the container, and the second table goes into view, the height of the
frame of the table (light yellow rectangle) starts to increase from the lower limit of the scope until it reaches the height of the container. At the same time, the
frame of the first table is compressed to zero until the table is removed off the screen. Both tables can use without restriction all possibilities of cell reuse without any restrictions and conditions.
Demonstration of the OLEContainerScrollView . Download video (in H.264 format) .
Code
The interface of the
OLEContainerScrollView class looks like this:
Code@interface OLEContainerScrollView : UIScrollView - (void)addSubviewToContainer:(UIView *)subview; - (void)removeSubviewFromContainer:(UIView *)subview; @end
But the implementation of the
layoutSubviews method, which does all the work:
@implementation OLEContainerScrollView ... - (void)layoutSubviews { [super layoutSubviews];
The rest of the code is quite template, you can read it
on GitHub .
Auto Layout
A
few words about
Auto Layout :
OLEContainerScrollView does not use this function inside, and it is probably impossible to implement this behavior using
Auto Layout struts (
UIScrollView and Auto Layout are not exactly best friends, anyway ). However, there should be no problems with using this class with other objects that use
Auto Layout for marking inside. As I said earlier, you can mix the manual layout and auto layout quite freely.
Hey, where's the podspec?
I deliberately did not (yet)
make CocoaPod from
OLEContainerScrollView . I wrote this class to solve my very specific problem, and I believe that it has enough potential to grow into a common component. Of course, so far it is not. The limitations are as follows:
- The class supports only the vertical markup of nested views and only vertical scrolling.
- Vertical markings are very inflexible. With it, all nested views are stretched to the width of the container. It does not support gaps between nested views or their free placement.
- The size of the content changes following the dimensions of the nested views, but does not animate well enough.
If you are interested in using this class, I will be glad if you look at its code and use it for your own purposes. I will also be happy to add your improvements (
pull pull requests ). And email me if you want to see this class as a
CocoaPod .
Conclusion
Placing several (including scroll) views on one common container — the scroll view is not a daily trick, but this approach can make it easier for you to work with data sources for tables and collections, as well as simplify markup and layout, turning their sections into individual species.
The OLEContainerScrollView is not currently a full-featured component, but I hope that it will become so with your help. Anyway, writing this component helped me to deepen my understanding of the
UIScrollView device and the
UIKit coordinate system.
Footnotes:
- This is something I would like to make more flexible in a future version. Currently, the class only supports vertical markup.
- I have done this so far only for species inheriting from UIScrollView . The frame height of a regular UIView remains unchanged, even if the view is not in scope. I did so in order to avoid changes in the markup (or even errors in auto layout ) that could be caused by zeroing the size of a view that does not expect such manipulations with its size. Changing this behavior will be easy in the future.