Подтвердить что ты не робот

Использование контекстов недействительности для UICollectionViewLayout

Итак, я внедрил рабочие липкие заголовки в свой UICollectionView частично, возвращая YES из shouldInvalidateLayoutForBoundsChange:. Однако это влияет на производительность, и я не хочу отменять весь макет, только мой раздел заголовка.

Теперь, согласно официальной документации, я могу использовать UICollectionViewLayoutInvalidationContext для определения пользовательского контекста недействительности для моего макета, но документации очень не хватает. Он просит меня "определить пользовательские свойства, которые представляют части ваших данных макета, которые могут быть пересчитаны независимо", но я не понимаю, что они означают этим.

Кто-нибудь получил какой-либо опыт подклассификации UICollectionViewLayoutInvalidationContext?

4b9b3361

Ответ 1

Это для iOS8

Я немного экспериментировал, и я думаю, что я понял, как использовать макет недействительности, по крайней мере, до тех пор, пока Apple немного не расширится.

Проблема, которую я пыталась решить, заключалась в получении липких заголовков в представлении коллекции. У меня был рабочий код для этого, используя подкласс FlowLayout и переопределяющий layoutAttributesForElementsInRect: (вы можете найти рабочие примеры в google). Это потребовало, чтобы я всегда возвращал true из mustInvalidateLayoutForBoundsChange: это предполагаемый основной удар производительности в орехах, которые Apple хочет, чтобы мы избегали контекстной недействительности.

Недействительность чистого контекста

Вам нужно только подклассифицировать UICollectionViewFlowLayout. Мне не нужен подкласс для UICollectionViewLayoutInvalidationContext, но тогда это может быть довольно простой случай использования.

По мере того, как прокручивается представление коллекции, макет потока начинает принимать shouldInvalidateLayoutForBoundsChange: calls. Поскольку компоновка потока уже может обрабатывать это, мы вернем ответ суперкласса в конце функции. При простой прокрутке это будет ложным и не будет повторно размещать элементы. Но нам нужно переустановить заголовки и оставить их в верхней части экрана, поэтому мы расскажем, что представление коллекции делает недействительным только тот контекст, который мы предоставим:

override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool {
    invalidateLayoutWithContext(invalidationContextForBoundsChange(newBounds))
    return super.shouldInvalidateLayoutForBoundsChange(newBounds)
}

Это означает, что нам также необходимо переопределить функцию invalidationContextForBoundsChange:. Поскольку внутренние действия этой функции неизвестны, мы просто спросим суперкласс для объекта контекста недействительности, определим, какие элементы представления коллекции мы хотим аннулировать, и добавим эти элементы в контекст недействительности. Я взял часть кода, чтобы сосредоточиться на основных моментах здесь:

override func invalidationContextForBoundsChange(newBounds: CGRect) -> UICollectionViewLayoutInvalidationContext! {

    var context = super.invalidationContextForBoundsChange(newBounds)

    if /... we find a header in newBounds that needs to be invalidated .../ {

            context.invalidateSupplementaryElementsOfKind(UICollectionElementKindSectionHeader, atIndexPaths:[NSIndexPath(forItem: 0, inSection:headerIndexPath.section)] )
    }
    return context
}

Что это. Заголовок и ничего, кроме заголовка, недействительны. Макет потока получит только один вызов layoutAttributesForSupplementaryViewOfKind: с indexPath в контексте недействительности. Если вам нужно сделать недействительными ячейки или декораторы, в UICollectionViewLayoutInvalidationContext есть другие недействительные * функции.

Самая сложная часть - это определение indexPaths заголовков в функции invalidationContextForBoundsChange:. Как мои заголовки, так и ячейки имеют динамический размер, и потребовалось некоторое акробатику, чтобы заставить его работать, просто глядя на границы CGRect, поскольку наиболее очевидная полезная функция indexPathForItemAtPoint:, ничего не возвращает, если точка находится на верхнем, нижнем колонтитуле, декораторе или интервал между строками.

Что касается производительности, я не сделал полного измерения, но быстро взглянул на Time Profiler, пока прокрутка показывает, что он делает что-то правильно (меньший шип справа при прокрутке). UICollectionViewLayoutInvalidationContext performance comparison

Ответ 2

Я просто задавал один и тот же вопрос об этом сегодня, а также путался с частью: "определить пользовательские свойства, которые представляют части ваших данных макета, которые могут быть пересчитаны независимо"

То, что я сделал, было подклассом UICollectionViewLayoutInvalidationContext (назовем его CustomInvalidationContext) и добавило мое собственное свойство. В целях тестирования я хотел узнать, где я могу настроить и получить эти свойства из контекста, поэтому я просто добавил массив как свойство, называемое атрибутами.

Затем в моем подклассе UICollectionViewLayout я перезаписываю +invalidationContextClass, чтобы вернуть экземпляр CustomInvalidationContext, который возвращается другим способом, который я перезаписываю, который: -invalidationContextForBoundsChange. В этом методе вам нужно вызвать super, который возвращает экземпляр CustomInvalidationContext, который затем настраивает свойства и возвращает. Я установил для массива атрибутов объекты @["a","b","c"];

Затем он затем извлекается еще одним перезаписанным способом - invalidateLayoutWithContext:. Я смог получить атрибуты, которые я установил из переданного контекста.

Итак, что вы можете сделать, это установить свойства, которые позже позволят вам рассчитать, какие indexPaths будут отправлены в -layoutAttributesForElementsInRect:.

Надеюсь, что это поможет.

Ответ 3

Начиная с iOS 8

Этот ответ был написан перед семенем iOS 8. В нем упоминаются надежды на функциональность, которая не совсем существует в iOS 7, и предлагает работу. Эта функциональность теперь существует и работает. Еще один ответ, который в настоящее время находится далее на странице, описывает подход для iOS 8.


Обсуждение

Прежде всего - с какой-либо оптимизацией, действительно важная осторожность - сначала профилировать и понять, где именно находится ваше узкое место производительности.

Я посмотрел UICollectionViewLayoutInvalidationContext и согласен, что, возможно, он предоставит необходимые функции. В комментариях к этому вопросу я описал свои попытки получить эту работу. Теперь я подозреваю, что, хотя он позволяет вам удалять перекомпоновку макета, это не поможет вам избежать внесения изменений макета в ячейки содержимого. В моем случае вычисления компоновки не особенно дороги, но я не хочу, чтобы рамки, применяющие изменения макета к простым прокручивающимся ячейкам (из которых у меня довольно много), и применяют их только к "специальным" ячейкам.

Резюме выполнения

В свете неспособности сделать это, как казалось, его предназначение Apple, я обманул. Я использую 2 UICollectionView экземпляры. У меня есть обычный прокручиваемый контент в фоновом режиме, а заголовки - на втором переднем плане. В макетах представлений указано, что фоновый вид не отменяется при изменении границ, а представление переднего плана делает.

Сведения о реализации

Существует ряд неочевидных вещей, которые вам нужно, чтобы получить право на эту работу, и у меня также есть несколько советов по реализации, которые, как я нашел, сделали мою жизнь проще. Я пройду через это и предоставит фрагменты кода, взятые из моего приложения. Я не собираюсь предлагать полное решение здесь, но я дам все необходимые вам части.

UICollectionView имеет свойство backgroundView.

Я создаю фоновый просмотр в моем методе UICollectionViewController viewDidLoad. К этому моменту контроллер представления уже имеет экземпляр UICollectionView в свойстве collectionView. Это будет представлением переднего плана и будет использоваться для элементов со специальным поведением прокрутки, таких как фиксация.

Я создаю второй экземпляр UICollectionView и устанавливаю его как свойство backgroundView в представлении коллекции переднего плана. Я создал фон, чтобы также использовать подкласс UICollectionViewController в качестве источника данных и делегирования. Я отключу взаимодействие с пользователем в фоновом режиме, потому что в противном случае все события получаются. Вам может потребоваться более тонкое поведение, чем это, если вы хотите выбирать и т.д.:

…
UICollectionView *const foregroundCollectionView = [self collectionView];
UICollectionView *const backgroundCollectionView = [[UICollectionView alloc] initWithFrame: [foregroundCollectionView frame] collectionViewLayout: [[STGridLayout alloc] init]];
[backgroundCollectionView setDataSource: self];
[backgroundCollectionView setDelegate: self];
[backgroundCollectionView setUserInteractionEnabled: NO];
[foregroundCollectionView setBackgroundView: backgroundCollectionView];
[(STGridLayout*)[backgroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: NO];
[(STGridLayout*)[foregroundCollectionView collectionViewLayout] setInvalidateLayoutForBoundsChange: YES];
…

В заключение - на данный момент у нас есть два представления коллекции друг на друга. Задний будет использоваться для статического контента. Передняя часть будет для закрепленного контента и тому подобного. Они оба указывают на тот же UICollectionViewController, что и их делегат и источник данных.

Свойство invalidateLayoutForBoundsChange в STGridLayout - это то, что я добавил в свой собственный макет. Макет просто возвращает его, когда вызывается -(BOOL) shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds.

Затем более общий общий вид для обоих представлений, который в моем случае выглядит следующим образом:

for(UICollectionView *collectionView in @[foregroundCollectionView, backgroundCollectionView])
{
  // Configure reusable views.
  [STCollectionViewStaticCVCell registerForReuseInView: collectionView];
  [STBlockRenderedCollectionViewCell registerForReuseInView: collectionView];
}

Метод registerForReuseInView: добавляется к UICollectionReusableView категорией вместе с dequeueFromView:. Код для них находится в конце ответа.

Следующий фрагмент, чтобы войти в viewDidLoad, является единственной основной головной болью с таким подходом.

Когда вы перетаскиваете представление переднего плана, вам нужно просмотреть фоновое представление для его прокрутки. Я покажу код для этого через мгновение: он просто отображает представление переднего плана contentOffset в фоновом режиме. Однако вы, вероятно, захотите, чтобы прокручиваемые представления могли "отскакивать" по краям содержимого. Похоже, что UICollectionView будет зажимать contentOffset, когда он программно установлен, чтобы содержимое не отрывалось от границ UICollectionView. Без средства правовой защиты только передние липкие элементы будут отскакивать, что выглядит ужасно. Однако добавление следующего к вашему viewDidLoad будет исправлено:

CGSize size = [foregroundCollectionView bounds].size;
[backgroundCollectionView setContentInset: UIEdgeInsetsMake(size.width, size.height, size.width, size.height)];

К сожалению, это исправление будет означать, что при просмотре на экране смещение содержимого фона не будет соответствовать переднему. Чтобы исправить это, вам необходимо реализовать это:

-(void) viewDidAppear:(BOOL)animated
{
  [super viewDidAppear: animated];
  UICollectionView *const foregroundCollectionView = [self collectionView];
  UICollectionView *const backgroundCollectionView = (UICollectionView *)[foregroundCollectionView backgroundView];
  [backgroundCollectionView setContentOffset: [foregroundCollectionView contentOffset]];
}

Я уверен, что было бы разумнее сделать это в viewDidAppear:, но это не сработало для меня.

Последнее, что вам нужно, это сохранить синхронизацию фона с передним планом следующим образом:

-(void) scrollViewDidScroll:(UIScrollView *const)scrollView
{
  UICollectionView *const collectionView = [self collectionView];
  if(scrollView == collectionView)
  {
    const CGPoint contentOffset = [collectionView contentOffset];
    UIScrollView *const backgroundView = (UIScrollView*)[collectionView backgroundView];
    [backgroundView setContentOffset: contentOffset];
  }
}

Рекомендации по внедрению

Вот некоторые советы, которые помогли мне реализовать методы источника данных UICollectionViewController.

Сначала я использовал раздел для каждого из видов, которые многоуровневы. Это сработало для меня. Я не использовал UICollectionView дополнительные или украшения. Я указываю каждому разделу имя в перечислении в начале моего контроллера представления следующим образом:

enum STSectionNumbers
{
  number_the_first_section_0_even_if_they_are_moved_during_editing = -1,

  // Section names. Order implies z with earlier sections drawn behind latter sections.
  STBackgroundCellsSection,
  STDataCellSection,
  STDayHeaderSection,
  STColumnHeaderSection,

  // The number of sections.
  STSectionCount,
};

В моем подклассе UICollectionViewLayout, когда запрашиваются атрибуты макета, я устанавливаю свойство z, чтобы выполнить упорядочение следующим образом:

-(UICollectionViewLayoutAttributes*) layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
  UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath: indexPath];
  const CGRect frame = …
  [attributes setFrame: frame];
  [attributes setZIndex: [indexPath section]];
  return attributes;
}

Для меня логика источника данных намного проще, если я даю обоим экземплярам UICollectionView все разделы, но контролировать, какой вид действительно получает их, делая разделы пустыми для другого.

Вот удобный метод, который я могу использовать, чтобы проверить, действительно ли данный UICollectionView имеет номер определенного раздела:

-(BOOL) collectionView:(UICollectionView *const)collectionView hasSection:(const NSUInteger)section
{
  const BOOL isForegroundView = collectionView == [self collectionView];
  const BOOL isBackgroundView = !isForegroundView;

  switch (section)
  {
    case STBackgroundCellsSection:
    case STDataCellSection:
    {
      return isBackgroundView;
    }

    case STColumnHeaderSection:
    case STDayHeaderSection:
    {
      return isForegroundView;
    }

    default:
    {
      return NO;
    }
  }
}

С этим на месте очень легко написать методы источника данных. Оба представления имеют один и тот же раздел, как я сказал, так:

-(NSInteger) numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
  return STSectionCount;
}

Однако они имеют разностные ячейки в разделах, но это легко вместить

-(NSInteger) collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
  if(![self collectionView: collectionView hasSection: section])
  {
    return 0;
  }

  switch(section)
  {
    case STDataCellSection:
    {
      return … // (actual logic not shown)
    }

    case STBackgroundCellsSection:
    {
      return …
    }

    … // similarly for other sections.

    default:
    {
      return 0;
    }
  }
}

Мой подкласс UICollectionViewLayout также имеет некоторые зависящие от вида методы, которые он делегирует подклассу UICollectionViewController, но они легко обрабатываются с использованием шаблона выше:

-(NSArray*) collectionViewRowRanges:(UICollectionView *)collectionView inSection:(NSInteger)section
{
  if(![self collectionView: collectionView hasSection: section])
  {
    return [NSArray array];
  }

  switch(section)
  {
    case STDataCellSection:
    {
      return … // (actual logic omitted)
      }
    }

    case STBackgroundCellsSection:
    {
      return …
    }

    … // etc for other sections

    default:
    {
      return [NSArray array];
    }
  }
}

Как проверка работоспособности, я гарантирую, что представления коллекции запрашивают только ячейки из разделов, которые они должны отображать:

-(UICollectionViewCell*) collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
  assert([self collectionView: collectionView hasSection: [indexPath section]] && "Check views are only asking for the sections they own.");

  switch([indexPath section])
  {
    case STBackgroundCellsSection:
    … // You get the idea.

Наконец, стоит отметить, что, как показано в другом ответе SA, математика для липких разделов проще, чем я предполагал, что им будет предоставлено, что вы думаете обо всем (в том числе экран устройства) как находящийся в пространстве содержимого представления коллекции.

Код для UICollectionReusableView Категория повторного использования

@interface UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view;
+(id) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath;
@end

Реализация:

@implementation UICollectionReusableView (Reuse)
+(void) registerForReuseInView: (UICollectionView*) view
{
  [view registerClass: self forCellWithReuseIdentifier: NSStringFromClass(self)];
}

+(instancetype) dequeueFromView: (UICollectionView*) view withIndexPath: (NSIndexPath *) indexPath
{
  return [view dequeueReusableCellWithReuseIdentifier:NSStringFromClass(self) forIndexPath: indexPath];
}
@end

Ответ 4

Я выполнил именно то, что вы пытаетесь сделать, установив флаг, который подскажет мне, почему именно prepareLayout вызывается, а затем только пересчитывает положение липких ячеек.

- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
    _invalidatedBecauseOfBoundsChange = YES;
    return YES;
}

Тогда в prepareLayout я делаю:

if (!_invalidatedBecauseOfBoundsChange)
{
    [self calculateStickyCellsPositions];
}
_invalidateBecauseOfBoundsChange = NO;

Ответ 5

Я работаю над пользовательским подклассом UICollectionViewLayout. Я попытался использовать UICollectionViewLayoutInvalidationContext. Когда я обновляю layout/view, который не нужен, весь UICollectionViewLayout пересчитывает все его атрибуты, я использую подкласс UICollectionViewLayoutInvalidationContext в invalidateLayoutWithContext: и в prepareLayout, я пересчитываю только атрибуты, указанные в моем UICollectionViewLayoutInvalidationContext свойства подкласса вместо пересчета всех атрибутов.