User95797654974
User95797654974

Reputation: 624

Incorrect thumbnails in SwiftUI cells for UICollectionView

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

Answers (0)

Related Questions