I have collection view with UICollectionViewCompositionalLayout
. Inside my collection I have items with images. When I launch my app I see collection view. And when I start scrolling I have multiple freezes. But when I scrolled the all collection, the freezes disappear. I think that freezes are disappearing after full collection scroll because the images are already loaded. But how can I fix the problem so that after launching the app and scrolling the collection for the first time, it does not freeze?
Video to reproduce the problem:
https://drive.google.com/file/d/145Bl2Oc3UHgwpJQZjG38YNVOZyWz7euM/view?usp=share_link
There is a lot of code in the app, so I posted the project on GitHub:
https://github.com/user234567890354678/carousel
Main code:
collection view cell
class CVCell: UICollectionViewCell, SelfConfiguringCell {
func configure(with item: Item) {
title.text = item.title
textView.backgroundColor = UIColor(item.backgroundColor)
textView.layer.borderColor = UIColor(item.borderColor).cgColor
titleImageView.image = UIImage(named: item.titleImage)
imageView.image = UIImage(named: item.image)
}
}
collection controller
class CVController: UIViewController, UICollectionViewDelegate {
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>?
let sections = Bundle.main.decode([Section].self, from: "img1.json")
func createDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, item in
switch self.sections[indexPath.section].identifier {
case "carouselCell":
let cell = self.configure(CarouselCell.self, with: item, for: indexPath)
return cell
default: return self.configure(CarouselCell.self, with: item, for: indexPath)
}
}
}
func configure<T: SelfConfiguringCell>(_ cellType: T.Type, with item: Item, for indexPath: IndexPath) -> T {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellType.reuseIdentifier, for: indexPath) as? T else { fatalError("\(cellType)") }
cell.configure(with: item)
return cell
}
func reloadData() {
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(sections)
for section in sections { snapshot.appendItems(section.item, toSection: section) }
dataSource?.apply(snapshot)
}
func setupCollectionView() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createCompositionalLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.isScrollEnabled = false
collectionView.delegate = self
collectionView.contentInsetAdjustmentBehavior = .never
view.addSubview(collectionView)
collectionView.register(CarouselCell.self, forCellWithReuseIdentifier: CarouselCell.reuseIdentifier)
createDataSource()
reloadData()
}
func createCompositionalLayout() -> UICollectionViewLayout {
UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), //
heightDimension: .fractionalHeight(1)) //
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupWidth = (layoutEnvironment.container.contentSize.width * 1.05)/3
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), //
heightDimension: .absolute(groupWidth)) //
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = NSDirectionalEdgeInsets(
top: (layoutEnvironment.container.contentSize.height/2) - (groupWidth/2),
leading: 0,
bottom: 0,
trailing: 0)
section.interGroupSpacing = 64
section.orthogonalScrollingBehavior = .groupPagingCentered
section.contentInsetsReference = .none
section.visibleItemsInvalidationHandler = { (items, offset, environment) in
items.forEach { item in
let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0)
let minScale: CGFloat = 0.7
let maxScale: CGFloat = 1.1
let scale = max(maxScale - (distanceFromCenter / environment.container.contentSize.width), minScale)
item.transform = CGAffineTransform(scaleX: scale, y: scale)
}
}
return section
}
}
func reloadItem(indexPath: IndexPath) {
guard let needReloadItem = dataSource!.itemIdentifier(for: indexPath) else { return }
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections(sections)
for section in sections { snapshot.appendItems(section.item, toSection: section) }
dataSource?.apply(snapshot)
snapshot.reloadItems([needReloadItem])
dataSource?.apply(snapshot, animatingDifferences: false)
}
}
What did I try to do to solve this problem?
I have the same problem in a UITableView
with a lot of images. To fix this I use this code:
func loadImageAsync(imageName: String, completion: @escaping (UIImage) -> ()) {
DispatchQueue.global(qos: .userInteractive).async {
guard let image = UIImage(named: imageName) else {return}
DispatchQueue.main.async {
completion(image)
}
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! RecipesViewCell
loadImageAsync(imageName: imageName) { (image) in
guard cell.imageName == imageName else { return }
cell.cardImageView.image = image
}
}
And after running, after a few seconds the images are loaded into the cells and after that I can scroll the table without freezing. Then I tried to apply this code for my collectionView
in question like this:
class CVCell: UICollectionViewCell, SelfConfiguringCell {
func configure(with item: Item) {
loadImageAsync(imageName: item.image) { (image) in
self.imageView.image = image
}
}
func loadImageAsync(imageName: String, completion: @escaping (UIImage) -> ()) {
DispatchQueue.global(qos: .userInteractive).async {
guard let image = UIImage(named: imageName) else {return}
DispatchQueue.main.async {
completion(image)
}
}
}
}
But this didn't help me at all. What am I doing wrong?
For iOS 15 and above, you can use func prepareForDisplay(completionHandler: @escaping (UIImage?) -> Void) for decoding image asynchronously.
Since the cells are reused and images are set asynchronously, the images might display on wrong cells during scroll. To fix this you can set the image to nil on prepareForReuse in CarouselCell.
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
}
And to avoid loading cells which goes out of screen during scroll, you can use Combine to handle task cancellation in CarouselCell.
import Combine
var imageTask: AnyCancellable?
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
imageTask?.cancel()
}
func configure(with item: Item) {
...
imageTask = Future<UIImage?, Never>() { promise in
UIImage(named: item.image)?.prepareForDisplay(completionHandler: { loadedImage in
promise(Result.success(loadedImage))
})
}
.receive(on: DispatchQueue.main)
.sink { image in
self.imageView.image = image
}
}