⬆️ ⬇️

Scroll Views inside Scroll Views

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:



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:





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:





Align while scrolling



During scrolling, the container continuously aligns the frames of the views nested in it in the following way:





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 ):





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]; // Translate the container view's content offset to contentView bounds. // This keeps the contentview always centered on the visible portion of the container view's // full content size, and avoids the need to make the contentView large enough to fit the // container view's full content size. self.contentView.frame = self.bounds; self.contentView.bounds = (CGRect){ self.contentOffset, self.contentView.bounds.size }; // The logical vertical offset where the current subview (while iterating over all subviews) // must be positioned. Subviews are positioned below each other, in the order they were added // to the container. For scroll views, we reserve their entire contentSize.height as vertical // space. For non-scroll views, we reserve their current frame.size.height as vertical space. CGFloat yOffsetOfCurrentSubview = 0.0; for (UIView *subview in self.contentView.subviews) { if ([subview isKindOfClass:[UIScrollView class]]) { UIScrollView *scrollView = (UIScrollView *)subview; CGRect frame = scrollView.frame; CGPoint contentOffset = scrollView.contentOffset; // Translate the logical offset into the sub-scrollview's real content offset and frame size. // Methodology: // (1) As long as the sub-scrollview has not yet reached the top of the screen, set its scroll position // to 0.0 and position it just like a normal view. Its content scrolls naturally as the container // scroll view scrolls. if (self.contentOffset.y < yOffsetOfCurrentSubview) { contentOffset.y = 0.0; frame.origin.y = yOffsetOfCurrentSubview; } // (2) If the user has scrolled far enough down so that the sub-scrollview reaches the top of the // screen, position its frame at 0.0 and start adjusting the sub-scrollview's content offset to // scroll its content. else { contentOffset.y = self.contentOffset.y - yOffsetOfCurrentSubview; frame.origin.y = self.contentOffset.y; } // (3) The sub-scrollview's frame should never extend beyond the bottom of the screen, even if its // content height is potentially much greater. When the user has scrolled so far that the remaining // content height is smaller than the height of the screen, adjust the frame height accordingly. CGFloat remainingBoundsHeight = fmax(CGRectGetMaxY(self.bounds) - CGRectGetMinY(frame), 0.0); CGFloat remainingContentHeight = fmax(scrollView.contentSize.height - contentOffset.y, 0.0); frame.size.height = fmin(remainingBoundsHeight, remainingContentHeight); frame.size.width = self.contentView.bounds.size.width; scrollView.frame = frame; scrollView.contentOffset = contentOffset; yOffsetOfCurrentSubview += scrollView.contentSize.height; } else { // Normal views are simply positioned at the current offset CGRect frame = subview.frame; frame.origin.y = yOffsetOfCurrentSubview; frame.size.width = self.contentView.bounds.size.width; subview.frame = frame; yOffsetOfCurrentSubview += frame.size.height; } } self.contentSize = CGSizeMake(self.bounds.size.width, fmax(yOffsetOfCurrentSubview, self.bounds.size.height)); } @end 




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:





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:



  1. This is something I would like to make more flexible in a future version. Currently, the class only supports vertical markup.
  2. 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.




From translator
It is also desirable for a deeper understanding to read the article "Understanding UIScrollView" .

Source: https://habr.com/ru/post/224815/



All Articles