UICollectionView или танцы с волками

в 19:13, , рубрики: ios development, mobile development, objective-c, uicollectionview, боль, разработка под iOS, метки: , , ,

The dream

UICollectionView — класс UIKit, появившийся в iOS 6. Строго говоря, это класс, позволяющий показывать на экране коллекцию айтемов. Структура коллекции — абсолютно произвольная, но обычно UICollectionView используется для всяких сетко-подобных контролов с ячейками, хедерами и футерами. Понимая, насколько абстрактен данный класс, разработчики Apple создали мощный механизм для создания любых лейаутов. По большому счету, даже UITableView это конкретная реализация UICollectionView. Возможности данного класса, в каком-то смысле, фантастические. Но в данной статье речь пойдет не об этом.

Ахиллесова пята разработчиков Apple — постоянное стремление делать СДК, которое будет работать «автомагически». Просто сделайте то-то и то-то, и класс «will do the right thing». К сожалению это работает далеко не всегда. И UICollectionView — яркий пример. Начиная с релиза в iOS 6 и по сегодняшний день (iOS 7.0.4) класс содержит довольно большое количество багов, с которыми очень трудно и неприятно иметь дело. Приходится угадывать, что же происходит «под капотом», и методом тыка заставлять UICollectionView работать как надо. Количество приобретенных костылей уже достигло таких размеров, что я решил поделиться известными багами и найденными решениями.

Кому интересно — милости просим под кат.

Reality

Перед тем как начать описывать баги, я попробую описать «идеальный сценарий», в котором UICollectionView работает стабильно, без каких-либо костылей. Как правило это UICollectionView, в котором нет хедеров, и футеров, только ячейки. Когда контроллер загрузился в память(viewDidLoad:), вызывается метод UICollectionView -reloadData. Анимированных удалений, перемещений, и вставок не делается. Вот такой идеальный сценарий для UICollectionView без костылей. Как вы видите, он довольно сильно ограничен.

Сразу оговорюсь, что вы можете не встретить баги из списка, который я приведу ниже. Каждый баг находился в специфических условиях, которых у вас может не быть. И каждое решение тоже помогало в моем конкретном случае, нельзя гарантировать, что оно поможет вам, и что оно будет продолжать работать в следующих релизах iOS. Многие из костылей будут выглядеть уродливо, но какой костыль не уродлив? Однако если что-то поможет — буду счастлив =). Поехали!

iOS 6 + iOS 7

1. Нельзя вставлять первый UICollectionViewCell в секции. Также нельзя удалять последний оставшийся UICollectionViewCell. Попытка это сделать приводит к крашу в дебаг билдах, и непредсказуемому поведению в релиз билдах:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'request for index path for global index 1 when there are only 1 items in the collection view'

Комментарий. Самый известный баг с UICollectionView. Открыт радар на баг-трекере Apple. Самое популярное и стабильное решение — вызывать reloadData. Иногда может помочь вызвать метод insertItemsAtIndexPaths: в try-catch блоке(да, нога безжалостно отстрелена).

@try {
    [self.collectionView insertItemsAtIndexPaths:indexPaths];
}
@catch (NSException *exception) {}

Еще одно решение, которое пожалуй не стоит использовать, но стоит упоминания, это вызов reloadData внутри performBatchUpdates блока.

[self.collectionView performBatchUpdates:^{
        [self.collectionView reloadData];
    } completion:nil];

2. Попытка выполнить две операции анимации одновременно без performBatchUpdates блока приведет к падению приложения.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (1) must be equal to the number of sections contained in the collection view before the update (0), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'

Комментарий. Данный пункт может показаться незначительным, но на самом деле он очень важен. 80% моих личных проблем с UICollectionView возникало тогда, когда возникала необходимость сгруппировать несколько анимаций. В частности, это бывает связано с NSFetchedResultsController и обновлениями в базе данных CoreData. Имеется опенсорсное решение от Ash Furrow, позволяющее корректно работать с обновлениями в CoreData.

3. Нельзя обновлять секции и ячейки в одном performBatchUpdates блоке. Под обновлением подразумевается вставка или удаление.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the collection view after the update (1) must be equal to the number of sections contained in the collection view before the update (0), plus or minus the number of sections inserted or deleted (0 inserted, 0 deleted).'

Комментарий. Довольно странная проблема, помогают два последовательных performBatchUpdates блока, в первом блоке — обновляются секции, во втором — ячейки.

4. Нельзя вызывать performBatchUpdates сразу за вызовом reloadData.

attempt to delete section 0, but there are only 0 sections before the update

Комментарий. Такое ощущение, что reloadData в UICollectionView работает асинхронно, в отличие от UITableView.

5. При использовании UICollectionViewFlowLayout, если headerSize или footerSize отличается от нуля, метод collectionView:viewForSupplementaryElementOfKind:atIndexPath: не должен возвращать nil, иначе — краш.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'the view returned from -collectionView:viewForSupplementaryElementOfKind:atIndexPath (UICollectionElementKindSectionFooter,<NSIndexPath: 0x8e55df0> {length = 2, path = 0 — 0}) was not retrieved by calling -dequeueReusableSupplementaryViewOfKind:withReuseIdentifier:forIndexPath: or is nil ((null))'

Комментарий. Если у вас UICollectionView, в котором header или footer могут отсутствовать, необходимо определить делегатовский метод

 -(CGSize)collectionView:(UICollectionView *)collectionView
                 layout:(UICollectionViewFlowLayout *)collectionViewLayout
referenceSizeForHeaderInSection:(NSInteger)sectionNumber

И в случае если у вас в данной секции отсутствует header, возвращать CGSizeZero.

iOS 6

1. UICollectionViewFLowLayout. Если у вас имеется секция без ячеек, и вы не определили делегатовский метод collectionView:layout:referenceSizeForHeaderInSection:, вам не повезло, краш рантайм.

*** Assertion failure in -[UICollectionViewData indexPathForItemAtGlobalIndex:]
request for index path for global index 805306368 when there are only 0 items in the collection view

Комментарий. Необходимо определить метод collectionView:layout:referenceSizeForHeaderInSection:, и в случае если в секции нет ячеек, вернуть CGSizeZero.

2. Иногда после вызова reloadData старые ячейки остаются на месте, несмотря на то, что методы UICollectionViewDatasource четко возвращают, что их нет. Особенно часто проявляется, когда UICollectionView закрыт клавиатурой, или другим UIView. Решения к сожалению нет.

iOS 7

1. В случае, если ячейка последняя в секции, а также наличия футеров в UICollectionView, вызов метода moveItemAtIndexPath:toIndexPath: для данной ячейки в другую секцию приведет к падению

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'no UICollectionViewLayoutAttributes instance for -layoutAttributesForSupplementaryElementOfKind: UICollectionElementKindSectionFooter at path <NSIndexPath: 0xc054730> {length = 2, path = 0 — 0}'

Комментарий. Данная проблема очень напоминает вставку и удаление первого айтема в секции, поэтому и решение такое-же, вызываем reloadData в случае, если айтем последний.

2. Удаление секции в тот момент, когда UICollectionView не показывается на экране, приводит к падению.

*** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2903.23/UICollectionViewData.m:341
2014-01-10 17:34:55.198 SMS-Bank[47090:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView recieved layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0xc000000000000056> {length = 2, path = 1 — 0}'

Комментарий. В тот момент, когда UICollectionView будет исчезать, зануляем делегат и датасорс.

self.collectionView.delegate = nil;
self.collectionView.dataSource = nil;

The end

Список может быть далеко не полон, к сожалению UICollectionView довольно нестабилен. Надеюсь, когда-нибудь разработчики Apple доведут его до ума. А пока-что — могу посоветовать библиотеку, которая была создана для удобной работы с UICollectionView.

Собственно, именно в процессе написания данной библиотеки автор и нашел большую часть багов, описанных в статье. Целью данного фреймворка никогда не был фикс багов iOS SDK, однако на сегодняшний день костыли, добавленные для iOS 6 и iOS 7, стали одной из важных фич. Возможность забыть о кошмаре под названием UICollectionView + NSInternalInconsistencyException — бесценна. Если у вас есть альтернативные решения, либо информация о других багах UICollectionView — поделитесь ею в комментариях!

Спасибо за внимание!

Автор: DenHeadless

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js