SwiftUI包装UICollectionView并使用合成布局

SwiftUI包装UICollectionView并使用合成布局,swift,uicollectionview,swiftui,Swift,Uicollectionview,Swiftui,我试图通过UIViewRepresentable在SwiftUI中实现CollectionView,并将这些SwiftUI视图用作可重用的单元格,但单元格根本没有出现。在SwiftUI中,似乎不可能包装集合视图并使用合成布局,或者可能我遗漏了一些东西 我知道我可以在UIKit中完全实现此CollectionView,只需包装所有视图控制器,但如果我想使用SwiftUI视图作为UICollectionView的单元格,该怎么办 使用UICollectionViewFlowLayout()时,视图显

我试图通过UIViewRepresentable在SwiftUI中实现CollectionView,并将这些SwiftUI视图用作可重用的单元格,但单元格根本没有出现。在SwiftUI中,似乎不可能包装集合视图并使用合成布局,或者可能我遗漏了一些东西

我知道我可以在UIKit中完全实现此CollectionView,只需包装所有视图控制器,但如果我想使用SwiftUI视图作为UICollectionView的单元格,该怎么办

使用UICollectionViewFlowLayout()时,视图显示正确

import SwiftUI
import UIKit

struct CollectionView<Section: Hashable & CaseIterable, Item: Hashable>: UIViewRepresentable {

    // MARK: - Properties
    let layout: UICollectionViewLayout
    let sections: [Section]
    let items: [Section: [Item]]

    // MARK: - Actions
    let snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)?
    let content: (_ indexPath: IndexPath, _ item: Item) -> AnyView

    // MARK: - Init
    init(layout: UICollectionViewLayout,
         sections: [Section],
         items: [Section: [Item]],
         snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)? = nil,
         @ViewBuilder content: @escaping (_ indexPath: IndexPath, _ item: Item) -> AnyView) {
        self.layout = layout

        self.sections = sections
        self.items = items

        self.snapshot = snapshot
        self.content = content
    }


    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func generateLayout() -> UICollectionViewLayout {

        let itemHeightDimension = NSCollectionLayoutDimension.absolute(44)
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension)
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: itemHeightDimension)
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 1)

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

        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }

    func makeUIView(context: Context) -> UICollectionView {

        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .clear

        collectionView.delegate = context.coordinator

        collectionView.register(HostingControllerCollectionViewCell<AnyView>.self, forCellWithReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier)

        context.coordinator.dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: context.coordinator.cellProvider)


        return collectionView
    }

    func updateUIView(_ uiView: UICollectionView, context: Context) {

        context.coordinator.dataSource.apply( (snapshot ?? reloadSnaphot)() , animatingDifferences: true)
    }

    func reloadSnaphot() -> NSDiffableDataSourceSnapshot<Section, Item> {

        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(Array(items.keys))

        items.forEach { (section, items) in
            snapshot.appendItems(items, toSection: section)
        }

        return snapshot
    }

    class Coordinator: NSObject, UICollectionViewDelegate {

        var dataSource: UICollectionViewDiffableDataSource<Section, Item>! = nil

        // MARK: - Properties
        let parent: CollectionView

        // MARK: - Init
        init(_ parent: CollectionView) {
            self.parent = parent
        }

        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

            print("Did select item at \(indexPath)")
        }


        // MARK: - Cell Provider
        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(parent.content(indexPath, item))
            //cell.host(rootView: parent.content(indexPath, item))
            return cell
        }
    }
}

class HostingControllerCollectionViewCell<Content: View> : 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.layoutMarginsGuide.leadingAnchor),
                controller.view.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
                controller.view.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
                controller.view.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor)
            ])

            if let parent = parent {
                controller.didMove(toParent: parent)
            }
            controller.view.layoutIfNeeded()
        }
    }

}
导入快捷界面
导入UIKit
结构集合视图:UIViewRepresentable{
//标记:-属性
let布局:UICollectionViewLayout
让我们来看看以下章节:[章节]
出租项目:[部分:[项目]]
//马克:行动
让快照:(()->NSDiffableDataSourceSnapshot)?
让内容:(\uIndeXPath:indexPath,\uItem:item)->AnyView
//MARK:-Init
初始化(布局:UICollectionViewLayout,
章节:[章节],
项目:[部分:[项目]],
快照:(()->NSDiffableDataSourceSnapshot)?=nil,
@ViewBuilder内容:@escaping(indexPath:indexPath,item:item)->AnyView){
self.layout=布局
self.sections=节
self.items=项目
self.snapshot=快照
self.content=内容
}
func makeCoordinator()->Coordinator{
协调员(自我)
}
func generateLayout()->UICollectionViewLayout{
设itemHeightDimension=NSCollectionLayoutDimension.absolute(44)
让itemSize=NSCollectionLayoutSize(宽度维度:。分数宽度(1.0),高度维度:itemHeightDimension)
let item=NSCollectionLayoutItem(layoutSize:itemSize)
让groupSize=NSCollectionLayoutSize(宽度维度:.SectilWidth(1.0),高度维度:itemHeightDimension)
let group=NSCollectionLayoutGroup.horizontal(layoutSize:groupSize,子项:项,计数:1)
let section=NSCollectionLayoutSection(组:组)
section.contentInsets=NSDirectionalEdgeInsets(顶部:0,前导:0,底部:0,尾随:0)
let layout=UICollectionViewCompositionLayout(截面:截面)
返回布局
}
func makeUIView(上下文:context)->UICollectionView{
让collectionView=UICollectionView(帧:.0,collectionViewLayout:UICollectionViewFlowLayout())
collectionView.autoresizingMask=[.flexibleHeight、.flexibleWidth]
collectionView.backgroundColor=.clear
collectionView.delegate=context.coordinator
collectionView.register(HostingControllerCollectionViewCell.self,forCellWithReuseIdentifier:HostingControllerCollectionViewCell.reuseIdentifier)
context.coordinator.dataSource=UICollectionViewDiffableDataSource(collectionView:collectionView,cellProvider:context.coordinator.cellProvider)
返回集合视图
}
func updateUIView(uiView:UICollectionView,context:context){
context.coordinator.dataSource.apply((快照??重新加载snapshot)(),animatingDifferences:true)
}
func reloadSnaphot()->NSDiffableDataSourceSnapshot{
var snapshot=NSDiffableDataSourceSnapshot()
快照.appendSections(数组(items.key))
items.forEach{(节,项)在
snapshot.appendItems(items,toSection:section)
}
返回快照
}
类协调器:NSObject,UICollectionViewDelegate{
变量数据源:UICollectionViewDiffableDataSource!=nil
//标记:-属性
让父对象:CollectionView
//MARK:-Init
init(uu父:CollectionView){
self.parent=parent
}
func collectionView(collectionView:UICollectionView,didSelectItemAt indexPath:indexPath){
打印(“在\(indexPath)处选择了项目”)
}
//标记:-单元提供程序
func cellProvider(collectionView:UICollectionView,indexPath:indexPath,item:item)->UICollectionViewCell{
打印(“为\(indexPath)提供单元格”)
guard let cell=collectionView.dequeueReusableCell(带reuseIdentifier:HostingControllerCollectionViewCell.reuseIdentifier,for:indexPath)作为?HostingControllerCollectionViewCell else{
fatalError(“不能使用称重传感器!”)
}
cell.host(parent.content(indexPath,item))
//host(rootView:parent.content(indexPath,item))
返回单元
}
}
}
类HostingControllerCollectionViewCell:UICollectionViewCell{
弱var控制器:UIHostingController?
func主机(u视图:内容,父级:UIViewController?=nil){
如果让控制器=控制器{
controller.rootView=view
controller.view.layoutIfNeeded()
}否则{
let controller=UIHostingController(rootView:view)
self.controller=控制器
controller.view.backgroundColor=.clear
layoutIfNeeded()
父级?.addChild(控制器)
contentView.addSubview(controller.view)
controller.view.translatesAutoResizezingMaskintoConstraints=false
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(等式:self.contentView.layoutMarginsGuide.leadingAnchor),
controller.view.trailingAnchor.constraint(等式:self.contentView.layoutMarginsGuide.trailingAnchor),
controller.view.topAnchor.constraint(等式:self.contentView.layoutMarginsGuide.topAnchor),
controller.view.bottomAnchor.constraint(等式:self.contentView.layoutMarginsGuide.bottomAnchor)
])
如果let parent=parent{
controller.didMove(toParent:parent)
}
controller.view.layoutIfNeeded()
}
}
}

我已将其重写到UIViewControllerRepresentab中
struct CollectionView<Section: Hashable & CaseIterable, Item: Hashable>: UIViewControllerRepresentable {

    // MARK: - Properties
    let layout: UICollectionViewLayout
    let sections: [Section]
    let items: [Section: [Item]]

    // MARK: - Actions
    let snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)?
    let content: (_ indexPath: IndexPath, _ item: Item) -> AnyView

    // MARK: - Init
    init(layout: UICollectionViewLayout,
         sections: [Section],
         items: [Section: [Item]],
         snapshot: (() -> NSDiffableDataSourceSnapshot<Section, Item>)? = nil,
         @ViewBuilder content: @escaping (_ indexPath: IndexPath, _ item: Item) -> AnyView) {
        self.layout = layout

        self.sections = sections
        self.items = items

        self.snapshot = snapshot
        self.content = content
    }


    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIViewController(context: Context) -> CollectionViewController<Section, Item> {

        let controller = CollectionViewController<Section, Item>()

        controller.layout = self.layout

        controller.snapshotForCurrentState = {

            if let snapshot = self.snapshot {
                return snapshot()
            }

            var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()

            snapshot.appendSections(self.sections)

            self.sections.forEach { section in
                snapshot.appendItems(self.items[section]!, toSection: section)
            }

            return snapshot
        }

        controller.content = content

        controller.collectionView.delegate = context.coordinator

        return controller
    }

    func updateUIViewController(_ uiViewController: CollectionViewController<Section, Item>, context: Context) {

        uiViewController.updateUI()
    }


    class Coordinator: NSObject, UICollectionViewDelegate {

        // MARK: - Properties
        let parent: CollectionView

        // MARK: - Init
        init(_ parent: CollectionView) {
            self.parent = parent
        }

        func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

            print("Did select item at \(indexPath)")
        }
    }
}

struct CollectionView_Previews: PreviewProvider {

    enum Section: CaseIterable {
        case features
        case categories
    }

    enum Item: Hashable {
        case feature(feature: Feature)
        case category(category: Category)
    }

    class Feature: Hashable{
        let id: String
        let title: String

        init(id: String, title: String) {
            self.id = id
            self.title = title
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(self.id)
        }

        static func ==(lhs: Feature, rhs: Feature) -> Bool {
            lhs.id == rhs.id
        }
    }

    class Category: Hashable {
        let id: String
        let title: String

        init(id: String, title: String) {
            self.id = id
            self.title = title
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(self.id)
        }

        static func ==(lhs: Category, rhs: Category) -> Bool {
            lhs.id == rhs.id
        }
    }

    static let items: [Section: [Item]] = {
        return [
            .features : [
                .feature(feature: Feature(id: "1", title: "Feature 1")),
                .feature(feature: Feature(id: "2", title: "Feature 2")),
                .feature(feature: Feature(id: "3", title: "Feature 3"))
            ],
            .categories : [
                .category(category: Category(id: "1", title: "Category 1")),
                .category(category: Category(id: "2", title: "Category 2")),
                .category(category: Category(id: "3", title: "Category 3"))
            ]
        ]
    }()

    static var previews: some View {

        func generateLayout() -> UICollectionViewLayout {

            let itemHeightDimension = NSCollectionLayoutDimension.absolute(44)
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: itemHeightDimension)
            let item = NSCollectionLayoutItem(layoutSize: itemSize)

            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: itemHeightDimension)
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 2)

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

            let layout = UICollectionViewCompositionalLayout(section: section)
            return layout
        }

        return CollectionView(layout: generateLayout(), sections: [.features], items: items, snapshot: {

            var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
            snapshot.appendSections(Section.allCases)

            items.forEach { (section, items) in
                snapshot.appendItems(items, toSection: section)
            }

            return snapshot

        }) { (indexPath, item) -> AnyView in

            switch item {
            case .feature(let item):
                return AnyView(Text("Feature \(item.title)"))
            case .category(let item):
                return AnyView(Text("Category \(item.title)"))
            }
        }

    }
}

class CollectionViewController<Section, Item>: UIViewController
    where Section : Hashable & CaseIterable, Item : Hashable {

    var layout: UICollectionViewLayout! = nil
    var snapshotForCurrentState: (() -> NSDiffableDataSourceSnapshot<Section, Item>)! = nil
    var content: ((_ indexPath: IndexPath, _ item: Item) -> AnyView)! = nil

    lazy var dataSource: UICollectionViewDiffableDataSource<Section, Item> = {
        let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: cellProvider)
        return dataSource
    }()

    lazy var collectionView: UICollectionView = {
        let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        collectionView.backgroundColor = .clear
        return collectionView
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        configureCollectionView()
        configureDataSource()
    }
}

extension CollectionViewController {

    private func configureCollectionView() {
        view.addSubview(collectionView)

        collectionView.register(HostingControllerCollectionViewCell<AnyView>.self, forCellWithReuseIdentifier: HostingControllerCollectionViewCell<AnyView>.reuseIdentifier)
    }


    private func configureDataSource() {

        // load initial data
        let snapshot : NSDiffableDataSourceSnapshot<Section, Item> = snapshotForCurrentState()
        dataSource.apply(snapshot, animatingDifferences: false)
    }

    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
    }
}

extension CollectionViewController {

    func updateUI() {
        let snapshot : NSDiffableDataSourceSnapshot<Section, Item> = snapshotForCurrentState()
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}