Search code examples
iosswiftuicollectionviewuicollectionviewcell

CollectionView with Cells made up of LPLinkView's and UIImageView's is Slow and Reloads Data While Scrolling


I have a UICollectionView with cells of two types; some that load images from a URL, and others that load metadata from a URL and display it with an LPLinkView.

Below is my code for the cell that displays data via LPLinkView:

import UIKit
import LinkPresentation

class LinkItemLinkPreviewCell: UICollectionViewCell {
    
    static let identifier = "kLinkPreviewCollectionViewCell"
    var linkView: LPLinkView?
    var urlMetadata: LPLinkMetadata?
    
    @IBOutlet var containerView: UIView? = UIView()
    @IBOutlet var titleLabel: UILabel? = UILabel()
    @IBOutlet var dateLabel: UILabel? = UILabel()
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.clipsToBounds = true
        self.autoresizesSubviews = true
        self.layer.cornerRadius = 20
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
    }
    
    var linkItem: LinkPostDataObject? {
        didSet {
            titleLabel?.text = linkItem?.postTitle ?? ""
            
            let df = DateFormatter()
            df.dateFormat = "yyyy-MM-dd hh:mm"
            dateLabel?.text = df.string(from: toDate((linkItem?.timeSent)!/1000)!)
            
            let provider = LPMetadataProvider()
            let url = URL(string: linkItem!.url)
            
            if url != nil {
                provider.startFetchingMetadata(for: URL(string: linkItem!.url)!) { (metadata, error) in
                    if let md = metadata {
                        DispatchQueue.main.async { [self] in
                            linkView = LPLinkView()
                            linkView?.metadata = md
                            linkView!.frame = containerView!.frame
                            containerView?.addSubview(linkView!)
                            linkView!.isUserInteractionEnabled = false
                            urlMetadata = metadata
                        }
                    }
                }
            }
        }
    }
 }

Below is the code for the UICollectionViewCell that displays an image from a URL:

import UIKit

class LinkItemImageCell: UICollectionViewCell {
    
    static let identifier = "kImageCollectionViewCell"
    
    @IBOutlet var imageView: UIImageView? = UIImageView()
    @IBOutlet var titleLabel: UILabel? = UILabel()
    @IBOutlet var dateLabel: UILabel? = UILabel()
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        self.clipsToBounds = true
        self.autoresizesSubviews = true
        self.layer.cornerRadius = 20
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
    }
    
    var linkItem: LinkPostDataObject? {
        didSet {
            titleLabel?.text = linkItem?.postTitle ?? ""
            
            let df = DateFormatter()
            df.dateFormat = "yyyy-MM-dd hh:mm"
            dateLabel?.text = df.string(from: toDate((linkItem?.timeSent)!/1000)!)
            
            if linkItem?.url.isImage() == true {
                let url = URL(string: linkItem!.url)
                url!.getImageData(from: url!) { (data, response, error) in
                    guard let data = data, error == nil else { return }
                    DispatchQueue.main.async() { [self] in
                        imageView?.image = UIImage(data: data)
                    }
                }
            }
        }
    }
}

And below is the code for my ViewController:

import UIKit

class LinkCollectionViewController : UIViewController, UICollectionViewDelegate,UICollectionViewDataSource {
    
    @IBOutlet weak var collectionView: UICollectionView!
    @IBOutlet weak var collectionNameLabel: UILabel!
    @IBOutlet weak var collectionSubTitleLabel: UILabel!
    @IBOutlet weak var collectionCreatorLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let backButton = UIBarButtonItem()
        backButton.title = "Inbox"
        self.navigationController?.navigationBar.topItem?.backBarButtonItem = backButton

        collectionNameLabel.text = airlockStore.selectedChannel.channelName
        collectionSubTitleLabel.text = airlockStore.selectedChannel.channelDescription
        collectionCreatorLabel.text = airlockStore.selectedChannel.creator
        
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.alwaysBounceVertical = true
        collectionView.register(UINib(nibName: "LinkImageItemCell", bundle: nil), forCellWithReuseIdentifier: "kImageCollectionViewCell")
        collectionView.register(UINib(nibName: "LinkItemLinkPreviewCell", bundle: nil), forCellWithReuseIdentifier: "kLinkPreviewCollectionViewCell")
        collectionView.reloadItems(at: collectionView.indexPathsForVisibleItems)
        collectionView.reloadData()

        NotificationCenter.default.addObserver(self, selector: #selector(self.updateLinkCollection), name: Notification.Name("Link_Collection_Updated"), object: nil)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        self.navigationController?.setNavigationBarHidden(false, animated: animated)
    }
    
    @objc func updateLinkCollection() {
        collectionView.reloadData()
        collectionView.reloadItems(at: collectionView.indexPathsForVisibleItems)
    }
    
    // MARK: - UICollectionViewDataSource Delegate

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return airlockStore.selectedChannel.linkPosts.count
    }
    
    func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return 1
    }
    
    // MARK: - UICollectionViewDelegate
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        //Image
        if airlockStore.selectedChannel.linkPosts.reversed()[indexPath.item].url.isImage() == true {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LinkItemImageCell.identifier, for: indexPath) as? LinkItemImageCell
                else { preconditionFailure("Failed to load collection view cell") }
            cell.linkItem = airlockStore.selectedChannel.linkPosts.reversed()[indexPath.item]
            return cell
        }
        
        //URL
        else {
            guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: LinkItemLinkPreviewCell.identifier, for: indexPath) as? LinkItemLinkPreviewCell
                else { preconditionFailure("Failed to load collection view cell") }
            cell.linkItem = airlockStore.selectedChannel.linkPosts.reversed()[indexPath.item]
            return cell
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.performSegue(withIdentifier: "GoToLinkBlockViewController", sender: self)
        airlockStore.selectedLinkBlock = airlockStore.selectedChannel.linkPosts.reversed()[indexPath.item]
        
        guard let cell: LinkItemLinkPreviewCell = collectionView.cellForItem(at: indexPath)! as? LinkItemLinkPreviewCell else {
            return
        }
        
        airlockStore.selectedLinkBlock.urlMetadata = cell.urlMetadata

    }
    
}

What I'm noticing is that the cells with the LPLinkView's are really slow to display data, and also that a LOT of refreshing happens when scrolling the the UICollectionView.

Any ideas or guidance on how I could improve performance here appreciated; really just trying to not reload the images/URL's of the cells as I scroll.


Solution

  • I don't have time to work through your code, so my answer is going to be general:

    When your table view/collection view displays data that has to be fetched over the network, load that data into your data model, not into cells. When the network load completes and that entry in the model is updated, tell the corresponding cell to reload.

    If you load data into your cells, as you scroll, the data you loaded before will be lost, and if you scroll back, you'll have to load it again. Ideally, save you loaded data to files in the caches directory so it will still be available if the user closes the view controller and then reopens it.

    If you install downloaded data into your model then when you scroll away and scroll back the cell will render correctly as soon as it's displayed.