UICollectionView недействительный макет при изменении границ

В настоящее время у меня есть следующий фрагмент для расчета размеров UICollectionViewCells:

- (CGSize)collectionView:(UICollectionView *)mainCollectionView
                  layout:(UICollectionViewLayout *)collectionViewLayout
  sizeForItemAtIndexPath:(NSIndexPath *)atIndexPath
{
    CGSize bounds = mainCollectionView.bounds.size;
    bounds.height /= 4;
    bounds.width /= 4;
    return bounds;
}

Это работает. Тем не менее, теперь я добавляю наблюдателя на клавиатуре в viewDidLoad (который запускает методы делегата и источника данных для UICollectionView перед тем, как он появится и изменится сам из раскадровки). Следовательно, границы являются неправильными. Я также хотел бы поддержать ротацию. Какой хороший способ обработки этих двух краевых случаев и пересчет размеров, если UICollectionView изменяет размер?

Ответы

Ответ 1

Решение о недействительности вашего макета, когда границы вида коллекции меняются, заключается в переопределении shouldInvalidateLayoutForBoundsChange: и возврате YES. Это также указано в документации: https://developer.apple.com/documentation/uikit/uicollectionviewlayout/1617781-shouldinvalidatelayoutforboundsc

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

Это также должно охватывать поддержку вращения. Если это не так, выполните viewWillTransitionToSize:withTransitionCoordinator:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
    [super viewWillTransitionToSize:size
          withTransitionCoordinator:coordinator];

    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
         [self.collectionView.collectionViewLayout invalidateLayout];
     }
                                 completion:^(id<UIViewControllerTransitionCoordinatorContext> context)
     {
     }];
}

Ответ 2

  1. Вы должны обрабатывать случай, когда размер представления коллекции изменяется. Если вы измените ориентацию или ограничения, будет запущен метод viewWillLayoutSubviews.

  2. Вы должны сделать недействительным текущее макет представления коллекции. После того, как макет признан недействительным с помощью метода invalidateLayout, будут запущены методы UICollectionViewDelegateFlowLayout.

Вот пример кода:

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    [mainCollectionView.collectionViewLayout invalidateLayout];
}

Ответ 3

Этот подход позволяет вам делать это без создания подкласса макета, вместо этого вы добавляете его в свой, по-видимому, уже существующий подкласс UICollectionViewController, и избегаете возможности рекурсивного вызова viewWillLayoutSubviews - это вариант принятого решения, слегка упрощенный в том viewWillLayoutSubviews что он не использует transitionCoordinator. В Свифте:

override func viewWillTransition(
    to size: CGSize,
    with coordinator: UIViewControllerTransitionCoordinator
) {
    super.viewWillTransition(to: size, with: coordinator)
    collectionViewLayout.invalidateLayout()
}

Ответ 4

Хотя ответ @tubtub действителен, некоторые из вас могут столкнуться со следующей ошибкой: The behaviour of the UICollectionViewFlowLayout is not defined.

Прежде всего, не забудьте переопределить shouldInvalidateLayout в вашем классе CustomLayout. (пример ниже)

Затем вы должны рассмотреть, изменились ли все элементы вашего представления в соответствии с новым макетом (см. Дополнительные шаги в примере кода).

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

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

    super.viewWillTransition(to: size, with: coordinator)

    /// (Optional) Additional step 1. Depending on your layout, you may have to manually indicate that the content size of a visible cells has changed
    /// Use that step if you experience the 'the behavior of the UICollectionViewFlowLayout is not defined' errors.

    collectionView.visibleCells.forEach { cell in
        guard let cell = cell as? CustomCell else {
            print("'viewWillTransition' failed. Wrong cell type")
            return
        }

        cell.recalculateFrame(newSize: size)

    }

    /// (Optional) Additional step 2. Recalculate layout if you've explicitly set the estimatedCellSize and you'll notice that layout changes aren't automatically visible after the #3

    (collectionView.collectionViewLayout as? CustomLayout)?.recalculateLayout(size: size)


    /// Step 3 (or 1 if none of the above is applicable)

    coordinator.animate(alongsideTransition: { context in
        self.collectionView.collectionViewLayout.invalidateLayout()
    }) { _ in
        // code to execute when the transition finished.
    }

}

/// Example implementations of the 'recalculateFrame' and 'recalculateLayout' methods:

    /// Within the 'CustomCell' class:
    func recalculateFrame(newSize: CGSize) {
        self.frame = CGRect(x: self.bounds.origin.x,
                            y: self.bounds.origin.y,
                            width: newSize.width - 14.0,
                            height: self.frame.size.height)
    }

    /// Within the 'CustomLayout' class:
    func recalculateLayout(size: CGSize? = nil) {
        estimatedItemSize = CGSize(width: size.width - 14.0, height: 100)
    }

    /// IMPORTANT: Within the 'CustomLayout' class.
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {

        guard let collectionView = collectionView else {
            return super.shouldInvalidateLayout(forBoundsChange: newBounds)
        }

        if collectionView.bounds.width != newBounds.width || collectionView.bounds.height != newBounds.height {
            return true
        } else {
            return false
        }
    }