我想在 UIKit 中使用@resultbuilder
并Combine
创建自己的反应式和声明式 UICollectionView 列表,类似于我们List {}
在 SwiftUI 中得到的。
为此,我正在使用结果构建器来创建这样的快照:
@resultBuilder
struct SnapshotBuilder {
static func buildBlock(_ components: ListItemGroup...) -> [ListItem] {
return components.flatMap { $0.items }
}
// Support `for-in` loop
static func buildArray(_ components: [ListItemGroup]) -> [ListItem] {
return components.flatMap { $0.items }
}
static func buildFinalResult(_ component: [ListItem]) -> NSDiffableDataSourceSectionSnapshot<ListItem> {
var sectionSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
sectionSnapshot.append(component)
return sectionSnapshot
}
}
我还需要使用以下扩展传递ListItemGroup
给 SnapshotBuilder 并获取[ListItem]
struct ListItem: Hashable {
let title: String
let image: UIImage?
var children: [ListItem]
init(_ title: String, children: [ListItem] = []) {
self.title = title
self.image = UIImage(systemName: title)
self.children = children
}
}
protocol ListItemGroup {
var items: [ListItem] { get }
}
extension Array: ListItemGroup where Element == ListItem {
var items: [ListItem] { self }
}
extension ListItem: ListItemGroup {
var items: [ListItem] { [self] }
}
我的List
班级看起来像这样:
final class List: UICollectionView {
enum Section {
case main
}
var data: UICollectionViewDiffableDataSource<Section, ListItem>!
private var cancellables = Set<AnyCancellable>()
init(_ items: Published<[String]>.Publisher, style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped, @SnapshotBuilder snapshot: @escaping () -> NSDiffableDataSourceSectionSnapshot<ListItem>) {
super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
configureDataSource()
data.apply(snapshot(), to: .main)
items
.sink { newValue in
let newSnapshot = snapshot()
self.data.apply(newSnapshot, to: .main, animatingDifferences: true)
}
.store(in: &cancellables)
}
required init(coder: NSCoder) {
super.init(coder: coder)!
}
private static func createLayout(_ appearance: UICollectionLayoutListConfiguration.Appearance) -> UICollectionViewLayout {
let layoutConfig = UICollectionLayoutListConfiguration(appearance: appearance)
return UICollectionViewCompositionalLayout.list(using: layoutConfig)
}
private func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<UICollectionViewListCell, ListItem> {
(cell, indexPath, item) in
var content = cell.defaultContentConfiguration()
content.image = item.image
content.text = item.title
cell.contentConfiguration = content
}
data = UICollectionViewDiffableDataSource<Section, ListItem>(collectionView: self) {
(collectionView: UICollectionView, indexPath: IndexPath, identifier: ListItem) -> UICollectionViewCell? in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: identifier)
cell.accessories = [.disclosureIndicator()]
return cell
}
}
}
我在我的 ViewControllers 中使用它,如下所示:
class DeclarativeViewController: UIViewController {
@Published var testItems: [String] = []
var collectionView: List!
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.navigationBar.sizeToFit()
title = "Settings"
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addItem))
view.backgroundColor = .systemBackground
collectionView = List($testItems) {
for item in self.testItems {
ListItem(item)
}
}
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
@objc func addItem() {
testItems.append("Item \(testItems.count)")
}
}
如您所见,我List
使用@Published var testItems
变量初始化 my 。在我的init()
func 中,我设置了一个订阅者并将它们存储在 中cancellables
,这样我就可以对更改做出反应。
如果我将一个项目添加到testItems
数组,则执行sink
回调以创建一个新快照并将它们应用于data
. 它有效,但我需要点击导航按钮两次,才能看到列表中的项目。两个问题:
- 为什么会发生这种情况,我该如何解决?(所以我只需点击一次按钮即可查看列表中的更改)
- 以及如何改进我的代码?(目前我总是创建一个新快照而不是扩展已经创建的快照)