Reputation: 624
I am using SwiftUI using UIHostingConfiguration to create cells for UICollectionViewCompositionalLayout. When I scroll very fast, incorrect thumbnails are shown in the cells. I feel that it is very likely a cell reuse issue but not sure how to fix this since my cells are in SwiftUI.
I am showing a list of AudioFile entities from core data and I use a async function inside the SwiftUI view to show the thumbnail from disk if available.
struct ListViewRow: View {
@ObservedObject var audioFile: AudioFile
@State private var thumbnail: UIImage?
var body: some View {
HStack {
if let thumbnail = thumbnail {
Image(uiImage: thumbnail)
.resizable()
.scaledToFit()
.cornerRadius(5)
.frame(width: 50, height: 50)
.padding(.trailing, 5)
} else {
//Placeholder image when no artwork found
Image(systemName: "music.note")
.resizable()
.foregroundColor(.gray)
.scaledToFit()
.padding()
.frame(width: 50, height: 50)
.background(.quaternary)
.cornerRadius(5)
.padding(.trailing, 5)
}
VStack(alignment: .leading) {
Text(audioFile.wrappedName)
.lineLimit(1)
Text(audioFile.wrappedArtist)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
}
Spacer()
}
.frame(height: 44)
.task {
if let artwork = audioFile.artwork {
thumbnail = await getArtwork(for: artwork)
} else {
thumbnail = nil
}
}
}
func getArtwork(for name: String) async -> UIImage? {
let url = FileManager.customURL.appendingPathComponent(name)
guard let data = try? Data(contentsOf: url) else { return nil }
let image = UIImage(data: data)
guard let image = image else { return nil }
let size = CGSize(width: 250, height: (250 * image.size.height) / image.size.width)
guard let thumbnail = await image.byPreparingThumbnail(ofSize: size) else { return nil }
return thumbnail
}
}
class ListViewController: UIViewController {
enum Section {
case main
}
enum ListItem: Hashable {
case header
case audioFile(AudioFile)
}
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Section, ListItem>!
let manager: Manager
init(manager: Manager) {
self.manager = manager
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
collectionView.delegate = self
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> {
[unowned self] (cell, indexPath, item) in
cell.contentConfiguration = UIHostingConfiguration {
HeaderView()
}
.margins(.all, 0)
}
let audioFileCellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
[unowned self] (cell, indexPath, item) in
let indexPathEdited = IndexPath(row: indexPath.row - 1, section: 0)
let audioFile = manager.fetchedResultsController.object(at: indexPathEdited)
cell.contentConfiguration = UIHostingConfiguration {
ListViewRow(audioFile: audioFile)
}
}
dataSource = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: collectionView) { collectionView, indexPath, listItem in
switch listItem {
case .header:
return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: listItem)
case .audioFile:
return collectionView.dequeueConfiguredReusableCell(using: audioFileCellRegistration, for: indexPath, item: listItem)
}
}
}
func applySnapshot() {
var snapshot = NSDiffableDataSourceSnapshot<Section, ListItem>()
snapshot.appendSections([.main])
snapshot.appendItems([ListItem.header])
guard let audioFiles = manager.fetchedResultsController.fetchedObjects else { return }
var audioFileItems: [ListItem] = []
for audioFile in audioFiles {
audioFileItems.append(ListItem.audioFile(audioFile))
}
snapshot.appendItems(audioFileItems)
dataSource.apply(snapshot, animatingDifferences: false)
}
}
Upvotes: 0
Views: 56