We are currently combining AutoLayout and StackView to structure our cells, and we’re using UICollectionViewCompositionalLayout to set up sections. Additionally, we’re utilizing UICollectionViewDiffableDataSource to manage the data source. On the home screen, we display cells in a multi-section layout.
In the main screen, we support infinite scrolling and use a paging method that fetches 10 items at a time from the API. As we fetch and render more data, the number of displayed cells increases. However, we’ve noticed that as the number of displayed cells grows, the UI freezes during the process of fetching and rendering more data.
We suspect that the issue lies in the configuration of UICollectionViewCompositionalLayout. When data is fetched through paging, the data is applied to the CollectionViewDiffableDataSource, and during the process of displaying it on the screen, the method that configures the UICollectionViewCompositionalLayout layout is called. The problem here is that when 10 cells are displayed on the screen and we fetch more data through paging and add 10 more cells, the layout calculation is redone for all 20 cells. In other words, the UICollectionViewCompositionalLayout layout configuration occurs a total of 20 times.
Because of this, it seems that the UI freezing issue occurs as the number of cells increases.
What steps can we take to resolve this problem? We have attached the relevant code for your reference.
private lazy var collectionView = UICollectionView(
frame: .zero,
collectionViewLayout: self.createCollectionViewLayout()
)
private func createCollectionViewLayout() -> UICollectionViewLayout {
let layout = UICollectionViewCompositionalLayout { [weak self] sectionIndex, _ in
guard let self, self.dataSource.snapshot().sectionIdentifiers.isEmpty == false
else { return LayoutProvider.emptySection() }
private func applyRefreshSnapshot(with sections: [PlateViewSection]) {
var snapshot = self.dataSource.snapshot()
snapshot.deleteAllItems()
snapshot.appendSections(sections)
sections.forEach { section in
snapshot.appendItems(section.items, toSection: section)
}
self.dataSource.apply(snapshot, animatingDifferences: false)
}
let sectionIdentifier = self.dataSource.snapshot().sectionIdentifiers[safe: sectionIndex]
let analyticsScreen = self.payload.analyticsScreen
switch sectionIdentifier?.module {
case let .tabOutlined(_, reactor):
if self.tabOutlinedView == nil {
let tabOutlinedView = self.dependency.tabOutlinedViewFactory.create(
payload: .init(
reactor: reactor,
collectionView: self.collectionView,
analyticsScreen: analyticsScreen,
currentDisplayDepthObserver: .init { event in
guard let depth = event.element else { return }
self.currentLayoutHeaderTabDepth = depth
},
selectedTabItemObserver: .init { [weak self] event in
guard let (tab, isPrimaryTabClick) = event.element else { return }
var queryParams: [String: String]?
if isPrimaryTabClick == false {
guard let currentState = self?.reactor?.currentState else { return }
let currentSelectedQueryParams = self?.dependency.userDefaults
.dictionary(forKey: Keys.PlateParamsKeys.brandQueryParams) as? [String: String]
queryParams = currentSelectedQueryParams ?? currentState.defaultQueryParams
}
self?.scrollCollectionViewToTop()
self?.collectionView.viewWithTag(
QueryToggleModuleCell.Metric.optionsMenuViewTag
)?.removeFromSuperview()
self?.reactor?.action.onNext(.refreshWithParams(
tabParams: tab?.params,
queryParams: queryParams
))
}
)
)
self.view.addSubview(tabOutlinedView)
tabOutlinedView.snp.makeConstraints { make in
make.top.leading.trailing.equalToSuperview()
}
self.view.layoutIfNeeded()
self.tabOutlinedView = tabOutlinedView
self.tabOutlinedView?.emitInitializeAction()
}
return LayoutProvider.emptySection()
case let .carouselOneRowBrand(module, _):
let section = self.dependency.layoutProvider.createLayoutSection(module: module)
section.visibleItemsInvalidationHandler = { [weak self] _, _, _ in
guard let self else { return }
self.emitFetchLikedAction(module: module, sectionIndex: sectionIndex)
}
return section
case let .queryToggle(module):
return self.dependency.layoutProvider.createLayoutSection(module: module)
case .loadingIndicator:
return LayoutProvider.loadingIndicatorSection()
case let .space(module):
return self.dependency.layoutProvider.createLayoutSection(module: module)
case let .noResult(module):
return self.dependency.layoutProvider.createLayoutSection(module: module)
case let .buttonViewAll(module):
return self.dependency.layoutProvider.createLayoutSection(module: module)
case .footer:
return LayoutProvider.footerSection()
default:
return LayoutProvider.emptySection()
}
}
return layout
}
private func applyAppendSnapshot(with sections: [PlateViewSection]) {
var snapshot = self.dataSource.snapshot()
// loadingIndicator section delete
let sectionsToDelete = snapshot.sectionIdentifiers.filter { section in
section.module == .loadingIndicator
}
snapshot.deleteSections(sectionsToDelete)
snapshot.appendSections(sections)
sections.forEach { section in
snapshot.appendItems(section.items, toSection: section)
}
self.dataSource.apply(snapshot, animatingDifferences: false)
}