Поведение UICollectionViewFlowLayout не определено, поскольку ширина ячейки больше ширины коллекцииView

2015-08-18 16: 07: 51.523 Пример [16070: 269647] поведение UICollectionViewFlowLayout не определен, потому что: 2015-08-18 16: 07: 51,523
Пример [16070: 269647] ширина элемента должна быть меньше ширина UICollectionView за вычетом секций вставки и правильные значения, минус вставки содержимого влево и вправо.
2015-08-18 16: 07: 51.524 Пример [16070: 269647] Соответствующий Экземпляр UICollectionViewFlowLayout, и он привязан к; анимация = { положение =; bounds.origin =; bounds.size =; }; layer =; contentOffset: {0, 0}; contentSize: {1024, 770} > макет коллекции:. 2015-08-18 16: 07: 51.524 Пример [16070: 269647] Сделать символический точка останова в UICollectionViewFlowLayoutBreakForInvalidSizes для catch это в отладчике.

Это то, что я получаю, что я делаю, это

func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
        return CGSizeMake(self.collectionView!.frame.size.width - 20, 66)
    }

когда я вращаюсь от пейзажа к портрету, консоль показывает это сообщение об ошибке только в iOS 9, кто-нибудь знает, что это происходит, и если есть исправление для этого?

Снимок экрана

Ответ 1

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

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

collectionView (collectionView: UICollectionView, макет collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath)

Обновить, чтобы включить Swift 4

@objc переопределить func collectionView (_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) → UICollectionViewCell     {...}

Это потому, что эта функция называется not или, по крайней мере, не сразу.

Что происходит, так это то, что подкласс класса представления представления коллекции не отменяет функцию shouldInvalidateLayoutForBoundsChange, которая по умолчанию returns false.

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

Это означает 2 вещи:

1 - предупреждение само по себе не вредно

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

Ответ 2

Это происходит потому, что ширина ячейки представления коллекции больше, чем ширина просмотра коллекции после вращения. Предположим, что у вас есть экран 1024x768, и ваш вид коллекции заполняет экран. Когда ваше устройство является ландшафтом, ваша ширина ячейки будет self.collectionView. frame.size.width - 20 = 1004, а его ширина больше, чем ширина коллекции в портрете = 768. Отладчик говорит, что "ширина элемента должна быть меньше ширины UICollectionView за вычетом значений вставки слева и справа, минус вставки содержимого слева и справа".

Ответ 3

Я использую подкласс UICollectionViewFlowLayout и использую его свойство itemSize чтобы указать размер ячейки (вместо collectionView:sizeForItemAtIndexPath: метод делегата). Каждый раз, когда я поворачиваю экран на более короткую ширину (например, пейзаж → портрет), я получаю это огромное предупреждение.

Я смог исправить это, выполнив 2 шага.

Шаг 1: В UICollectionViewFlowLayout подкласса prepareLayout() функции, переместить super.prepareLayout(), чтобы после того, как, где self.itemSize устанавливается. Я думаю, что это заставляет суперкласс использовать правильное значение itemSize.

import UIKit

extension UICollectionViewFlowLayout {

  var collectionViewWidthWithoutInsets: CGFloat {
    get {
      guard let collectionView = self.collectionView else { return 0 }
      let collectionViewSize = collectionView.bounds.size
      let widthWithoutInsets = collectionViewSize.width
        - self.sectionInset.left - self.sectionInset.right
        - collectionView.contentInset.left - collectionView.contentInset.right
      return widthWithoutInsets
    }
  }

}

class StickyHeaderCollectionViewLayout: UICollectionViewFlowLayout {

  // MARK: - Variables
  let cellAspectRatio: CGFloat = 3/1

  // MARK: - Layout
  override func prepareLayout() {
    self.scrollDirection = .Vertical
    self.minimumInteritemSpacing = 1
    self.minimumLineSpacing = 1
    self.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: self.minimumLineSpacing, right: 0)
    let collectionViewWidth = self.collectionView?.bounds.size.width ?? 0
    self.headerReferenceSize = CGSize(width: collectionViewWidth, height: 40)

    // cell size
    let itemWidth = collectionViewWidthWithoutInsets
    self.itemSize = CGSize(width: itemWidth, height: itemWidth/cellAspectRatio)

    // Note: call super last if we set itemSize
    super.prepareLayout()
  }

  // ...

}

Обратите внимание, что вышеуказанное изменение каким-то образом изменит размер макета, когда вращение экрана перестанет работать. Это где шаг 2 входит.

Шаг 2: Поместите это в контроллер представления, который содержит представление коллекции.

  override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator)
    collectionView.collectionViewLayout.invalidateLayout()
  }

Теперь предупреждение прошло :)


Некоторые заметки:

  1. Убедитесь, что вы добавляете ограничения в collectionView и не используете collectionView.autoresizingMask = [.flexibleWidth,.flexibleHeight]

  2. Убедитесь, что вы не вызываете invalidateLayout в viewWillTransitionToSize() потому что ширина ячейки от края до края в альбомной viewWillTransitionToSize() больше ширины кадра коллекций представлений в портретной ориентации. Смотрите ниже ссылки.

Рекомендации

Ответ 4

Я решил эту проблему, используя safeAreaLayoutGuide.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    return CGSize(width: (view.safeAreaLayoutGuide.layoutFrame.width), height: 80);
}

и вы также должны переопределить эту функцию для правильной поддержки портретного и ландшафтного режима.

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

Ответ 5

Вы можете проверить отладчик, если collectionView.contentOffset изменен на отрицательный, в моем случае он изменяется от (0,0) до (0,-40). Вы можете решить эту проблему, используя этот метод

if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) {
   self.automaticallyAdjustsScrollViewInsets = NO;
}

Ответ 6

После некоторых экспериментов это, похоже, также связано с тем, как вы компонуете свой CollectionView.

Tl; dr: Используйте AutoLayout, а не autoresizingMask.

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

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

    coordinator.animate(alongsideTransition: { (context) in
        self.collectionView.collectionViewLayout.invalidateLayout()
    }, completion: nil)
}

Однако есть ситуации, когда вы можете получить предупреждение о размере элемента. Для меня, если я нахожусь в ландшафте на одной вкладке, переключитесь на другую вкладку, поверните на портрет и вернитесь к предыдущей вкладке. Я попытался сделать недействительным макет в willAppear, willLayout, всех обычных подозреваемых, но не повезло. Фактически, даже если вы вызываете invalidateLayout до super.willAppear(), вы все равно получите предупреждение.

И в конечном итоге эта проблема привязана к размеру обновления границ коллекции, прежде чем делегат попросит элемент itemize.

Итак, имея в виду, я попытался использовать AutoLayout вместо collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight], и с этим проблема решена! (Я использую SnapKit, чтобы делать AutoLayout, чтобы я не вытаскивал свои волосы постоянно). Вам также просто нужно invalidateLayout в viewWillTransition (без coordinator.animate, так же, как в вашем примере), а также invalidateLayout внизу viewWillAppear(:). Это похоже на все ситуации.

Я не знаю, пользуетесь ли вы авторезистированием или нет - было бы интересно узнать, работает ли моя теория/решение для всех.

Ответ 7

Это работает для меня:

Недействительно макет при вращении:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    collectionView.collectionViewLayout.invalidateLayout()
}

Есть отдельная функция для расчета доступной ширины:

Примечание: принимая во внимание как вставку раздела, так и вставку содержимого.

// MARK: - Helpers

func calculateCellWidth(for collectionView: UICollectionView, section: Int) -> CGFloat {
    var width = collectionView.frame.width
    let contentInset = collectionView.contentInset
    width = width - contentInset.left - contentInset.right

    // Uncomment the following two lines if you're adjusting section insets
    // let sectionInset = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, insetForSectionAt: section)
    // width = width - sectionInset.left - sectionInset.right

    // Uncomment if you are using the sectionInset property on flow layouts
    // let sectionInset = (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset ?? UIEdgeInsets.zero
    // width = width - sectionInset.left - sectionInset.right

    return width
}

И, наконец, наконец, возвращаем размер предмета:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CGSize(width: calculateCellWidth(for: collectionView, section: indexPath.section), height: 60)
}

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

Вуаля!

Ответ 8

Я также видел, как это происходит, когда мы устанавливаем для параметра valuesItemSize значение AutomaticSize, а вычисленный размер ячеек составляет менее 50x50 (Примечание. Этот ответ был действителен во времена iOS 12. Возможно, в более поздних версиях его исправили).

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

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

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

Один из обходных путей исправления заключается в замене константы размером элемента 1x1:

flowLayout.estimatedItemSize = CGSize(width: 1, height: 1)

Это не влияет на самое-проклейках поддержки, потому что, как документация estimatedItemSize упоминает:

Установка его в любое другое значение заставляет представление коллекции запрашивать каждую ячейку для ее фактического размера, используя ячейку предпочитаемый элементayLayoutAttributesFitting (_ :).

Тем не менее, это может/не может иметь последствия для производительности.

Ответ 9

У меня была аналогичная проблема.

В моем случае у меня появилось представление коллекции, и когда вы постучали по одной из ячеек, для редактирования элемента открылось popover с UITextField. После этого popover исчезло, self.collectionView.contentInset.bottom было установлено в 55 (изначально 0).

Чтобы исправить мою проблему, после исчезновения представления popover, я вручную настраиваю содержимоеInset на UIEdgeInsetsZero.

contextual prediction bar

Оригинальная проблема, похоже, связана с панелью контекстного прогнозирования, которая отображается вверху клавиатуры. Когда клавиатура скрыта, панель исчезает, но значение contentInset.bottom не возвращается к исходному значению.

Поскольку ваша проблема, похоже, связана с шириной, а не с высотой ячейки, проверьте, не соответствуют ли какие-либо из значений contentInset или layout.sectionInset тем, которые были установлены вами.

Ответ 10

У меня была такая же проблема. Я исправил это, очистив мой просмотр коллекции его ограничений и сбросив их в Storyboard.

Ответ 11

Я обнаружил, что это хорошо работает для UICollectionViewController и правильно анимируется:

- (void)viewWillTransitionToSize:(CGSize)size
       withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    // Ensure the layout is within the allowed size before animating below, or 
    //  'UICollectionViewFlowLayoutBreakForInvalidSizes' breakpoint will trigger
    //  and the logging will occur.

    if ([self.collectionView.collectionViewLayout isKindOfClass:[YourLayoutSubclass class]]) {
        [(YourLayoutSubclass *)self.collectionView.collectionViewLayout updateForWidth:size.width];
    }

    // Then, animate alongside the transition, as you would:

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

    [super viewWillTransitionToSize:size
          withTransitionCoordinator:coordinator];
}

///////////////

@implementation YourLayoutSubclass

- (void)prepareLayout {
    [super prepareLayout];

    [self updateForWidth:self.collectionView.bounds.size.width];
}

- (void)updateForWidth:(CGFloat)width {
    // Update layout as you wish...
}

@end

... См. Комментарии встроенного кода выше для объяснения. 👍🏻

Ответ 12

Это то, что вам нужно:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    //Get frame width
    let width = self.view.frame.width
    //I want a width of 418 and height of 274 (Aspect ratio 209:137) with a margin of 24 on each side of the cell (See insetForSectionAt 24 + 24 = 48). So, check if a have that much screen real estate.
    if width > (418 + 48) {
        //If I do return the size I want
        return CGSize(width: 418, height: 274)
    }else{
        //Get new width. Frame width minus the margins I want to maintain
        let newWidth = (width - 48)
        //If not calculate the new height that maintains the aspect ratio I want. NewHeight = (originalHeight / originalWidth) * newWidth
        let height = (274 / 418) * newWidth
        //Return the new size that is Aspect ratio 209:137
        return CGSize(width: newWidth, height: height)
    }
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24)
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return 33
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
    return 33
}