SwiftUI 包装 UICollectionView 并使用组合布局
Posted
技术标签:
【中文标题】SwiftUI 包装 UICollectionView 并使用组合布局【英文标题】:SwiftUI wrapping UICollectionView and use compositional layout 【发布时间】:2020-04-04 19:55:59 【问题描述】:我尝试通过 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()
【问题讨论】:
【参考方案1】:我已经将它改写成 UIViewControllerRepresentable 并且现在可以正常工作了
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)
【讨论】:
以上是关于SwiftUI 包装 UICollectionView 并使用组合布局的主要内容,如果未能解决你的问题,请参考以下文章
在 SwiftUI 中使用 @State 属性包装器未更新视图
SwiftUI - 使用自定义绘图包装 UIView 时出现 UIViewRepresentable 故障
SwiftUI - 在视图中包装 Button,以创建自定义按钮
SwiftUI CollectionView 包装器无法从 updateUIViewController 应用快照