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
-
Вы должны обрабатывать случай, когда размер представления коллекции изменяется. Если вы измените ориентацию или ограничения, будет запущен метод viewWillLayoutSubviews.
-
Вы должны сделать недействительным текущее макет представления коллекции. После того, как макет признан недействительным с помощью метода 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
}
}