I have attached the full reproducible example below.
I am using a list version of UICollectionViewCompositionalLayout in a SwiftUI app.
When scrolling, it crashes the app with an error: “Fatal error: <UICollectionView 0x600000ca2c70> is stuck in a recursive layout loop. This can happen when self-sizing views do not return consistent sizes, or the collection view’s frame/bounds/contentOffset is being constantly adjusted.”
I am not entirely sure on how to debug this.
Once I remove, “collectionView.contentInsetAdjustmentBehavior = .never”, the collection view list compositional layout scrolls smoothly. Replacing UIHostingConfiguration with UIKit cells also fixes the problem.
struct ContentView: View {
var body: some View {
GeometryReader { geo in
ViewRepresentable()
}
.ignoresSafeArea(edges: .top)
}
}
struct ViewRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
ViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}
struct Item: Hashable {
var id = UUID()
let name: String
}
class ViewController: UIViewController {
enum Section: CaseIterable {
case main
}
enum ListItem: Hashable {
case header
case item(Item)
}
let testItems: [Item] = (1...100).map { Item(name: "Item ($0)") }
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, ListItem>!
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: .zero, collectionViewLayout: createLayout())
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 60, right: 0)
collectionView.contentInsetAdjustmentBehavior = .never //Completely breaks scrolling for UIHostingConfiguration
collectionView.delaysContentTouches = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
configureDataSource()
applySnapshot()
}
func createLayout() -> UICollectionViewLayout {
var listConfiguration = UICollectionLayoutListConfiguration(appearance: .plain)
listConfiguration.headerMode = .firstItemInSection
listConfiguration.headerTopPadding = 0
let layout = UICollectionViewCompositionalLayout.list(using: listConfiguration)
return layout
}
func configureDataSource() {
let headerCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> { (header, indexPath, item) in
let view = Color.blue.frame(width: 300, height: 300)
let controller = UIHostingController(rootView: view)
controller.safeAreaRegions = SafeAreaRegions()
header.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
controller.view.centerXAnchor.constraint(equalTo: header.centerXAnchor),
controller.view.topAnchor.constraint(equalTo: header.topAnchor),
controller.view.bottomAnchor.constraint(equalTo: header.bottomAnchor),
])
}
let itemCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
[unowned self] (cell, indexPath, item) in
let indexPathEdited = IndexPath(row: indexPath.row - 1, section: 0)
let item = testItems[indexPathEdited.row]
cell.contentConfiguration = UIHostingConfiguration {
Text(item.name)
.frame(height: 50)
}
}
dataSource = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: collectionView) { collectionView, indexPath, listItem in
switch listItem {
case .header:
return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: listItem)
case .item:
return collectionView.dequeueConfiguredReusableCell(using: itemCellRegistration, for: indexPath, item: listItem)
}
}
}
func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, ListItem>()
snapshot.appendSections([.main])
snapshot.appendItems([ListItem.header])
var listItems: [ListItem] = []
for item in testItems {
listItems.append(ListItem.item(item))
}
snapshot.appendItems(listItems)
dataSource.apply(snapshot, animatingDifferences: false)
}
}