- PVSM.RU - https://www.pvsm.ru -
В данной статье я хочу представить OLEContainerScrollView [1], который является потомком UIScrollView и позволяет вам добавлять несколько scroll views, таблиц (UITableView) или коллекций (UICollectionView) в один контейнер.
Вы можете использоваться OLEContainerScrollView для достижения следующих целей:
Перед тем, как рассмотреть реализацию моего класса, давайте обратим внимание на то, как таблицы или коллекции вообще работают. Оба класса, UITableView [2] и UICollectionView [3], являются потомками UIScrollView [4] и ведут себя аналогично. Однако, ключевым отличием является то, что таблицы и коллекции повторно используют свои ячейки [5]. Когда вид (view) прокручивается и ячейка выходит за пределы экрана, эта ячейка удаляется из иерархии видов (иными словами, ей посылается сообщение removeFromSuperview) и переносится в очередь для повторного использования (reuse queue). В то же время, перед тем, как новая ячейка должна появиться, таблица вызывает из очереди ожидания повторного использования свободную ячейку и повторного размещает ее на своей иерархии видов. Этот подход позволяет избежать значительного потребления памяти, минимизировать число дорогостоящих операций создания и размещения (allocation) в памяти новых видов и обеспечить максимально быструю прокрутку.
Иллюстрация повторного использования ячеек в UITableView. Невидимые ячейки удаляются из иерархии видов (обозначены синим пунктиром) и добавляются на вид таблицы перед отображение в результате прокрутки. Заметьте, что frame таблицы (светло-синий прямоугольник) меньше, чем размер контента (content size, обведён красной пунктирной линией), как это обычно и бывает у scroll view. Скачать видео (в формате H.264) [6].
Размещение нескольких scroll view в одном общем контейнере, так же являющимся scroll view, — это довольно простая задача:
Таким образом, когда мы каждому вложенному scroll view устанавливаем размер frame больший либо равный размеру их контента, мы получаем ситуацию, в которой эти scroll view никогда не будут прокручиваться — единственным прокручивающимся объектом будет контейнер. Это позволяет избежать помех в обработке касаний между вложенными scroll view и их контейнером.
Эта схема, безусловно работает, но есть один существенный недостаток: чрезмерная трата памяти. Если вложенные scroll view являются таблицами или коллекциями, то они создают ячейку для каждой строки, потому что все ячейки в их понимации являются видимыми. Если коллекция содержит сотни или тысячи ячеек, тогда эффект будет по истине драматическим: прокрутка будет тормозить, а ваше приложение потребит всю доступную память на устройстве.
Пример простого подхода к реализации контейнера для нескольких scroll views. Две таблицы добавлены как подвиды (subviews) в один общий контейнер, который сам при этом является scroll view (прямоугольник с черной обводкой). Frames таблиц (светло-синий и светло-жёлтый прямоугольники) изменены так, чтобы полностью вмещать в себя контент каждой таблицы (красная пунктирная обводка). Заметьте, как это отразилось на повторном использовании ячеек — оно прекратилось, одновременно присутствуют все ячейки каждой строки, независимо от того, видима ли ячейка (внутри frame контейнера, черный прямоугольник) или нет. Скачать видео (в формате H.264) [7].
Теперь я расскажу о своём контейнере для scroll view. OLEContainerScrollView — это наследник UIScrollView, который автоматически упорядочивает вложенные в него виды в манере колоды или стопки (похоже на NSStackView [8] в OS X). Этот контейнер работает со всеми типами видами, не только со scroll views, хотя scroll view он обрабатывает особым образом.
Чтобы добавить вид в контейнер, необходимо использовать метод addSubviewToContainer:. Я был вынужден создать новые методы для добавления и удаления видов в контейнер, а не полагаться на существующую пару addSubview:/removeFromSuperview, потому что я хотел добавлять виды в приватный contentView, а не прямо в контейнер. Этот приём позволил избежать помех со стороны приватных подвидов самого UIScrollView, которые создаются для отображения индикаторов прокрутки, когда мы позже будем перебором вложенных видов подгонять их размеры.
Как только вид добавляется в контейнер, происходит следующее:
Во время прокрутки контейнер непрерывно выравнивает frames вложенных в него видов следующим способом:
Это по-прежнему соответствует простому подходу, описанному ранее. Теперь же нам требуется выровнять frames всех scroll views в контейнере так, чтобы у них получился минимальный размер, который позволит заполнить область видимости (viewport) контейнера (в соответствии с bounds [9]):
Посмотрите на видео ниже, чтобы понять, как этот алгоритм работает. В начале первая таблица заполняет собой всю область видимости контейнера (отмечена черной обводкой) — frame таблицы (светло-синий прямоугольник) точно равен bounds контейнера. Вторая таблица при этом полностью находится вне области видимости — её frame имеет высоту 0, таблица невидима. В итоге пока нет необходимости создавать в ней ячейки (желтый пунктир).
Как только пользователь прокручивает содержимое контейнера, и вторая таблица выходит в область видимости, высота frame таблицы (светло-желтый прямоугольник) начинает увеличиваться от нижней границы области видимости до того момента, пока не сравняется с высотой контейнера. В то же самое время frame первой таблицы сжимается до нуля, пока таблица удаляется за пределы экрана. Обе таблицы могут использовать без ограничений все возможности повторного использования ячеек без ограничений и каких-либо условий.
Демонстрация работы OLEContainerScrollView. Скачать видео (в формате H.264) [10].
Интерфейс класса OLEContainerScrollView выглядит так:
@interface OLEContainerScrollView : UIScrollView
- (void)addSubviewToContainer:(UIView *)subview;
- (void)removeSubviewFromContainer:(UIView *)subview;
@end
А вот реализация метода layoutSubviews, который выполняет всю работу:
@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
Остальной код вполне шаблонный, ознакомиться с ним можно на GitHub [1].
Пара слов о Auto Layout: OLEContainerScrollView не использует эту функцию внутри, и наверно невозможно реализовать подобное поведение с помощью распорок Auto Layout (UIScrollView и Auto Layout не совсем лучшие друзья, в любом случае [11]). Тем не менее, не должно быть проблем с использованием этого класса с другими объектами, которые используют Auto Layout для разметки внутри себя. Как я говорил ранее, вы можете вполне свободно смешивать ручную разметку и auto layout.
Я намеренно не cделал (пока) CocoaPod из OLEContainerScrollView. Я написал этот класс, чтобы решить свою очень специфическую проблему, и я верю, что у него есть достаточный потенциал, чтобы вырасти в общий компонент. Безусловно, пока что он таковым не является. Ограничения следующие:
Если вы заинтересованы в использовании данного класса, я буду рад, если вы загляните в его код и используете его в своих целях. Так же я буду рад добавить ваши улучшения (делайте pull request’ы). И напишите мне, если хотите видеть этот класс в виде CocoaPod’а.
Размещение нескольких (в том числе scroll) видов на одном общем контейнере-scroll view не является приёмом на каждый день, но этот подход может упростить вам работу с источниками данных для таблиц и коллекций, а так же упростить разметку и компоновку, превратив их секции в отдельные виды.
OLEContainerScrollView в данный момент не является полнофункциональным компонентом, но я надеюсь, что он станет таким с вашей помощью. Во всяком случае, написание этого компонента помогло мне углубить моё понимание устройства UIScrollView и координатной системы UIKit.
Автор: egormerkushev
Источник [12]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios/61230
Ссылки в тексте:
[1] OLEContainerScrollView: https://github.com/ole/OLEContainerScrollView
[2] UITableView: https://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html
[3] UICollectionView: https://developer.apple.com/library/ios/documentation/uikit/reference/UICollectionView_class/Reference/Reference.html
[4] UIScrollView: https://developer.apple.com/Library/ios/documentation/UIKit/Reference/UIScrollView_Class/Reference/UIScrollView.html
[5] повторно используют свои ячейки: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableView_Class/Reference/Reference.html#//apple_ref/doc/uid/TP40006943-CH3-SW92
[6] Скачать видео (в формате H.264): http://oleb.net/media/table-view-cell-reuse-simulation.m4v
[7] Скачать видео (в формате H.264): http://oleb.net/media/naive-container-scrollview.m4v
[8] NSStackView: https://developer.apple.com/library/mac/documentation/AppKit/Reference/NSStackView_Class/Chapters/Reference.html
[9] в соответствии с bounds: http://oleb.net/blog/2014/04/understanding-uiscrollview/
[10] Скачать видео (в формате H.264): http://oleb.net/media/olecontainerscrollview-demo.m4v
[11] UIScrollView и Auto Layout не совсем лучшие друзья, в любом случае: https://developer.apple.com/LIBRARY/ios/technotes/tn2154/_index.html
[12] Источник: http://habrahabr.ru/post/224815/
Нажмите здесь для печати.