SwiftUI UICollectionView or UITableView wrapper and reusable cells is it efficient to replace cell content with new SwitUI views

Viewed 428

As we know when using UITableView or UICollectionView we register reusable cell classes and then we dequeue this reusable cells to make list/collection working more efficiently with large dataset.

Is it possible to use such UITableViews or UICollectionViews with SwiftUI Content in any approach in such way to not deteriorate efficiency of this lists/collections?

I consider such examples:

  1. UICollectionView cells

    private func cellProvider(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? {
    
        print("Providing cell for \(indexPath)")
    
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier, for: indexPath) as? HostingControllerCollectionViewCell<AnyView> else {
            fatalError("Coult not load cell!")
        }
    
        cell.host(content(indexPath, item))
    
        return cell
    }
    

    class HostingControllerCollectionViewCell : UICollectionViewCell {

    weak var controller: UIHostingController<Content>?
    
    func host(_ view: Content, parent: UIViewController? = nil) {
    
        if let controller = controller {
            controller.rootView = view
            controller.view.layoutIfNeeded()
        } else {
            let controller = UIHostingController(rootView: view)
            self.controller = controller
            controller.view.backgroundColor = .clear
    
            layoutIfNeeded()
    
            parent?.addChild(controller)
            contentView.addSubview(controller.view)
            controller.view.translatesAutoresizingMaskIntoConstraints = false
    
            NSLayoutConstraint.activate([
                controller.view.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
                controller.view.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
                controller.view.topAnchor.constraint(equalTo: self.contentView.topAnchor),
                controller.view.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
            ])
    
            if let parent = parent {
                controller.didMove(toParent: parent)
            }
            controller.view.layoutIfNeeded()
        }
    }
    

    }

  2. UITableViewCell

    class HostingTableViewControllerCell: UITableViewCell {

    private weak var controller: UIHostingController<Content>?
    
    func host(_ view: Content, parent: UIViewController) {
        if let controller = controller {
            controller.rootView = view
            controller.view.layoutIfNeeded()
        } else {
            let swiftUICellViewController = UIHostingController(rootView: view)
            controller = swiftUICellViewController
            swiftUICellViewController.view.backgroundColor = .clear
    
            layoutIfNeeded()
    
            parent.addChild(swiftUICellViewController)
            contentView.addSubview(swiftUICellViewController.view)
            swiftUICellViewController.view.translatesAutoresizingMaskIntoConstraints = false
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.leading, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.leading, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.trailing, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.trailing, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.top, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.top, multiplier: 1.0, constant: 0.0))
            contentView.addConstraint(NSLayoutConstraint(item: swiftUICellViewController.view!, attribute: NSLayoutConstraint.Attribute.bottom, relatedBy: NSLayoutConstraint.Relation.equal, toItem: contentView, attribute: NSLayoutConstraint.Attribute.bottom, multiplier: 1.0, constant: 0.0))
    
            swiftUICellViewController.didMove(toParent: parent)
            swiftUICellViewController.view.layoutIfNeeded()
        }
    }
    

    }

or I have other solutions just with replacing subviews of contentView

class HostingTableViewCell: UITableViewCell {

    func host<Content: View>(rootView: Content) {

        self.contentView.subviews.forEach {  $0.removeFromSuperview() }

        let view = UIHostingController(rootView: rootView).view
        view?.backgroundColor = .clear

        self.contentView.addSubview(view!)

        view!.preservesSuperviewLayoutMargins = true
        view!.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            view!.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
            view!.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
            view!.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
            view!.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor)
        ])

    }
}

Maybe should I register multiple HostingCells with specific view type and instead of replacing entire view try to somehow setTitle() setImage() on this SwiftUI content views.

 private func cellProvider(collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? {

        print("Providing cell for \(indexPath)")
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HostingControllerCollectionViewCell<CustomView>.reuseIdentifier, for: indexPath) as? HostingControllerCollectionViewCell<CustomView> else {
            fatalError("Could not load cell!")
        }

        if cell.hostingController == nil {
            cell.host(Text("LOL"))
        } else { 
           cell.hostingController.rootView 
           cell.controller?.rootView?.setTitle("Text") 
        } 

        return cell
}

struct CustomView: View { 

   @State var text: String 

   func setTitle(_ text: String) { self.text = text } 
}

But here question. How I could register multiple SwiftUI content views like CustomView1, CustomView2, CustomView3?

maybe at constraction time of add

CollectionView(reusableCells: [CustomView1.self, CustomView2.self],...) 

and then in configureCollectionView() add

collectionView.register(HostingControllerCollectionViewCell.self, forCellWithReuseIdentifier: CustomViewX.reuseIdentifier)

but is this last suggested approach better and how I could pass View struct types and store then off different view type without not erasing type to AnyView

UPDATE

If I added to current soluton 100k items then app works very very slows, views are not layout properly and it seems to crash very frequently. So it seems that cells aren't reused properly.

static func generateCategories() -> [Item] {

        var items = [Item]()

        for i in 0..<100000 {
            items.append(.category(category: Category(id: "\(i)", title: "Category \(i)")) )
        }

        return items
    }
0 Answers
Related