swift - 如何使用与@resultbuilder 结合来构建动态collectionview 列表?
问题描述
我想在 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
. 它有效,但我需要点击导航按钮两次,才能看到列表中的项目。两个问题:
- 为什么会发生这种情况,我该如何解决?(所以我只需点击一次按钮即可查看列表中的更改)
- 以及如何改进我的代码?(目前我总是创建一个新快照而不是扩展已经创建的快照)
解决方案
让我通过回答您的第二个问题来回答这两个问题。
我怎样才能改进我的代码?(目前我总是创建一个新快照而不是扩展已经创建的快照)
我对您使用@resultBuilder
. 通常,人们会使用结果构建器来创建领域特定语言 (DSL)。在这种情况下,您可以创建一个用于构造的 DSL ListItems
,但这意味着您的意思是在编译时填充一个列表,这里的大部分代码似乎都专注于动态更新列表,运行时。所以使用结果生成器似乎过于复杂。
在这种情况下,您还使用了Publisher
您可能通过didSet
在控制器的属性上使用简单的地方来获得的地方。然而,作为控制器试图与其视图协调的更复杂模型的一部分,发布者将是一个非常好的主意。我有一个你的代码版本,我用它替换了发布者,didSet
但第二眼看去——想象更复杂的模型案例,我把发布者放回去了。
您已经将发布者的管道全部纠缠在结果构建器中 - 这很奇怪,因为再次,发布者是关于在运行时动态地对更改做出反应,而结果构建器是关于为编译时代码的语法糖化制作漂亮的 DSL。
所以我退出了 DSL,并建立了一个丰富的管道,可以很好地利用发布者。
此外,在使用组合发布者时,通常使用类型擦除来使发布者的实际性质更加匿名。所以在我的返工中,我使用eraseToAnyPublisher
so thatList
可以从任何人那里获取它的值,而不仅仅是一个@Published
字符串列表。
于是List
变成:
final class List: UICollectionView {
enum Section {
case main
}
private var subscriptions = Set<AnyCancellable>()
private var data: UICollectionViewDiffableDataSource<Section, ListItem>!
init(itemPublisher: AnyPublisher<[String], Never>,
style: UICollectionLayoutListConfiguration.Appearance = .insetGrouped) {
super.init(frame: .zero, collectionViewLayout: List.createLayout(style))
configureDataSource()
itemPublisher
.map{ items in items.map { ListItem($0) }}
.map{ listItems in
var newSnapshot = NSDiffableDataSourceSectionSnapshot<ListItem>()
newSnapshot.append(listItems)
return newSnapshot
}
.sink {
newSnapshot in
self.data?.apply(newSnapshot, to: .main, animatingDifferences: true)
}
.store(in: &subscriptions)
}
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
}
}
}
请注意为其设置的丰富处理管道,itemPublisher
它以AnyPublisher<[String], Never>
.
然后你的DeclarativeViewController
变成:
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(itemPublisher: $testItems.eraseToAnyPublisher())
collectionView.frame = view.bounds
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(collectionView)
}
@objc func addItem() {
testItems.append("Item \(testItems.count)")
}
}
模型的testItems
发布者被删除给任何发布者。
在我的代码ListItem
中保持不变,但所有相关的东西@resultBuiler
都不见了。如果您想创建一个函数来ListItems
为表中的初始项目集(或具有静态内容的表)构建一组函数,也许您可以使用它,但这里似乎没有必要。
推荐阅读
- php - laravel - 正确使用 if 语句
- sequelize.js - Sequelize:同步后如何将数据插入新创建的表中
- javascript - 如何使用 Vuejs、vuex 将字符串转换为整数?
- facebook - Xamarin.Auth Facebook 登录完成事件未触发
- python - 检查请求中的身份验证信息是否正确
- javascript - 你如何“堆叠”单独列中的弹性元素?
- docker - Docker for Windows Kubernetes pod 在创建新部署后获取 ImagePullBackOff
- c# - 如何在局部空间中找到对象底部的位置?
- google-sheets - 将 Big Query 中的数据自动导入 Google 表格?
- python - 在 pymongo 中搜索时忽略一个特定或任何特殊字符