0

enter code hereI'm creating an application that requires a UICollectionView inside a UICollectionViewCell. I've successfully added the UICollectionView and all of its delegate methods within the cell and, on initial load, everything loads correctly. However, as soon as the user attempts to scroll further down in the main UICollectionView, the cells which held the inner UICollectionView, essentially, loose their view. An interesting detail to note is that the issue only rears its head when the user scrolls at a non-snail rate. If the scroll slow enough, the cells will load correctly. Otherwise, the cells will become malformed.

Unfortunately, I've been dealing with this problem for quite a while, as in close to/more than a month at this point. I've tried just about every solution proposed on StackOverflow, Medium articles, YouTube tutorials, etc. but have yet to find the working solution.

This behavior is not exhibited without the conditional rendering if I always add the UICollectionView as a subview.

An important detail to note is that my cells are vertically self-sizing, and every cell is not guaranteed to contain an inner UICollectionView. I suspect part of my issue is born from my handling of this conditional rendering (i.e. if the cell's datasource has images, add the UICollectionView as a subview. Otherwise, don't add it at all).

Additionally, I'm using imported assets in my Xcode project as images. I'm not making any async calls that otherwise may attribute to the images not being fetched and the datasource having 0 images when it should have > 0. My logs in my parseDatasource(withDatasource datasource: (String, [UIImage]) function are showing that datasource.1 contains an array of images in the cells that should have images, as expected. I'm beyond stumped at this point and a bit more than slightly frustrated.

My main UIViewController controlling the cells:

class ExperimentalViewController: UIViewController {

    private let cellReuseId = "cellReuseId"
    fileprivate var data = [(String, [UIImage])]()
    fileprivate let strings = [<a large array of strings>]

    private let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.backgroundColor = .clear
        collection.alwaysBounceVertical = true
        collection.contentInsetAdjustmentBehavior = .always

        return collection
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white

        setupDummyData()
        setupCollectionView()
    }

    private func setupDummyData() {
        for (i, str) in strings.enumerated() {
            var images = [UIImage]()
            if i % 2 == 0 {
                images = [UIImage(named: "boxed-water-is-better-1464052-unsplash")!, UIImage(named: "boxed-water-is-better-1464052-unsplash")!, UIImage(named: "boxed-water-is-better-1464052-unsplash")!]
            }
            data.append((str, images))
        }
    }

    private func setupCollectionView() {
        if #available(iOS 13.0, *) {
            let size = NSCollectionLayoutSize(
                widthDimension: NSCollectionLayoutDimension.fractionalWidth(1),
                heightDimension: NSCollectionLayoutDimension.estimated(440)
            )

            let item = NSCollectionLayoutItem(layoutSize: size)

            let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item])

            let section = NSCollectionLayoutSection(group: group)
            section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 8, bottom: 0, trailing: 8)
            section.interGroupSpacing = 5

            let layout = UICollectionViewCompositionalLayout(section: section)
            collectionView.collectionViewLayout = layout
        } else {
            let layout = ExperimentalFlowLayout()
            collectionView.collectionViewLayout = layout
        }

        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(ExperimentalCell.self, forCellWithReuseIdentifier: cellReuseId)

        view.addSubview(collectionView)
        collectionView.anchor(top: view.topAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
    }

}

extension ExperimentalViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return data.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellReuseId, for: indexPath) as! ExperimentalCell
        cell.index = indexPath
        cell.backgroundColor = .yellow
        cell.setupViews(withDatasource: data[indexPath.item])

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 20
    }

}

My UICollectionViewCell subclass

class ExperimentalCell: UICollectionViewCell {

    // https://stackoverflow.com/questions/37782659/swift-ios-uicollectionview-images-mixed-up-after-fast-scroll/37784212
    // http://www.thomashanning.com/the-most-common-mistake-in-using-uitableview/
    private let imageCellReuseId = "imageCellReuseId"
    var index: IndexPath?
    var images = [UIImage]()

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0

        return label
    }()

    private let label: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0

        return label
    }()

    let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.backgroundColor = .clear

        return collection
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        print("RYANLOG \(index?.row) INIT")
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepareForReuse() {
        super.prepareForReuse()

        images = []
        imageView.image = nil
        collectionView.delegate = nil
        collectionView.dataSource = nil
        titleLabel.text = nil
        label.text = nil

        setupViews(withDatasource: nil)
    }

    func setupViews(withDatasource datasource: (String, [UIImage])?) {
        if datasource != nil {
            parseDatasource(datasource!)
        }

        contentView.addSubview(titleLabel)
        titleLabel.anchor(top: contentView.topAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)

        contentView.addSubview(label)
        if images.count > 0 {
            titleLabel.text = "Images"
            label.text = String(images.count)
            label.anchor(top: titleLabel.bottomAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)

            setupCollectionView()
            contentView.addSubview(collectionView)
            collectionView.anchor(top: label.bottomAnchor, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 200)
        } else {
            titleLabel.text = "No Images"
            label.anchor(top: titleLabel.bottomAnchor, leading: contentView.leadingAnchor, bottom: contentView.bottomAnchor, trailing: contentView.trailingAnchor, paddingTop: 0, paddingLeft: 0, paddingBottom: 0, paddingRight: 0, width: 0, height: 0)
        }

    }

    private func parseDatasource(_ datasource: (String, [UIImage])) {
        label.text = datasource.0
        images = datasource.1

        print("RYANLOG \(index?.row) Images:", images.count)
    }

    // MARK: - Used for self-sizing on <= iOS 12

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        layoutIfNeeded()
        let layoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        layoutAttributes.bounds.size = systemLayoutSizeFitting(UIView.layoutFittingCompressedSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .defaultLow)
        return layoutAttributes
    }

}

extension ExperimentalCell: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    fileprivate func setupCollectionView() {
        collectionView.delegate = self
        collectionView.dataSource = self
        collectionView.register(ScrollableImageView.self, forCellWithReuseIdentifier: imageCellReuseId)
    }

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return images.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: imageCellReuseId, for: indexPath) as! ScrollableImageView
        cell.imageView.image = images[indexPath.item]
        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize(width: 200, height: 200)
    }

}

Please excuse the use of print("RYANLOG ...)s. I've been using them for debugging and figured they might be helpful in displaying the areas I believe might be part of the issue.

ScrollableImageView is a subclass of UIView. If you think seeing the code contained within this class would be helpful, please let me know!

Any help is greatly appreciated!

A gif demonstrating the flawed behavior: https://i.stack.imgur.com/gYVuH.jpg

RElliott
  • 93
  • 1
  • 8

1 Answers1

0

Update: I finally solved this using two different reuse identifiers: "cellRuseId" and "imageCellReuseId". I'm still not sure whether this is the correct solution, but, now my collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) looks like so:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let reuseId = data[indexPath.item].1.count > 0 ? imageCellReuseId : cellReuseId
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseId, for: indexPath) as! ExperimentalCell
    cell.index = indexPath
    cell.backgroundColor = .yellow
    cell.setupViews(withDatasource: data[indexPath.item])
    return cell
}

My collection view scrolls correctly and is properly reusing the cells. I won't accept this answer as it's not the most elegant (especially if you have multiple variations of the same cell) so if someone has a better answer, please give a response!

RElliott
  • 93
  • 1
  • 8