我如何将 Combine 与 @resultbuilder 结合使用来构建动态 collectionview 列表?
How can i use Combine with @resultbuilder to build a dynamic collectionview list?
我想使用 @resultbuilder
和 Combine
在 UIKit 中创建我自己的反应式和声明式 UICollectionView 列表,类似于我们在 SwiftUI 中使用 List {}
得到的。
为此,我正在使用 resultbuilder 创建这样的快照:
@resultBuilder
struct SnapshotBuilder {
static func buildBlock(_ components: ListItemGroup...) -> [ListItem] {
return components.flatMap { [=10=].items }
}
// Support `for-in` loop
static func buildArray(_ components: [ListItemGroup]) -> [ListItem] {
return components.flatMap { [=10=].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
Class 看起来像这样:
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)")
}
}
如您所见,我使用 @Published var testItems
变量初始化 List
。在我的 init()
函数中,我设置了一个订阅者并将它们存储在 cancellables
中,这样我就可以对更改做出反应。
如果我将一个项目添加到 testItems
数组,将执行 sink
回调以创建新快照并将它们应用到 data
。它有效,但我需要点击导航按钮两次,才能看到列表中的项目。两个问题:
- 为什么会这样,我该如何解决? (所以我只需要点击一次按钮就可以看到列表中的变化)
- 以及如何改进我的代码? (目前我总是创建一个新快照而不是扩展已经创建的快照)
让我通过回答你的第二个问题来回答这两个问题。
How can i improve my code? (currently I always create a new snapshot
instead of extending the already created one)
我对您对 @resultBuilder
的使用感到有点困惑。通常,人们会使用结果生成器来创建领域特定语言 (DSL)。在这种情况下,您可以创建一个用于构造 ListItems
的 DSL,但这意味着您的意思是在编译时填充一个列表,这里的大部分代码似乎都集中在运行时动态更新列表。所以使用结果生成器似乎过于复杂。
在这种情况下,您还使用了 Publisher
,您可以通过在控制器的 属性 上使用简单的 didSet
来获得。但是,作为控制器试图与其视图协调的更复杂模型的一部分,发布者将是一个非常好的主意。我有一个你的代码版本,我用 didSet
替换了 Publisher,但再看一眼 - 想象更复杂的模型案例,我把发布者放回去了。
你的发布者的管道都在你的结果构建器中纠缠不清——这很奇怪,因为发布者再次对运行时的变化做出动态反应,而结果构建器是为编译的语法加糖制作好的 DSL时间码。
所以我退出了 DSL,并建立了一个丰富的管道,充分利用了发布者。
此外,在使用 Combine 发布者时,通常使用类型擦除来使发布者的实际性质更加匿名。所以在我的返工中,我使用 eraseToAnyPublisher
以便 List
可以从任何人那里获取它的值,而不仅仅是 @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([=10=]) }}
.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>
的形式进入 class。
然后你的 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
相关的所有内容都消失了。如果您想创建一个函数来为 table 中的初始项目集构建一组 ListItems
(或具有静态内容的 table),也许您可以使用它但是这里似乎没有必要。
我想使用 @resultbuilder
和 Combine
在 UIKit 中创建我自己的反应式和声明式 UICollectionView 列表,类似于我们在 SwiftUI 中使用 List {}
得到的。
为此,我正在使用 resultbuilder 创建这样的快照:
@resultBuilder
struct SnapshotBuilder {
static func buildBlock(_ components: ListItemGroup...) -> [ListItem] {
return components.flatMap { [=10=].items }
}
// Support `for-in` loop
static func buildArray(_ components: [ListItemGroup]) -> [ListItem] {
return components.flatMap { [=10=].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
Class 看起来像这样:
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)")
}
}
如您所见,我使用 @Published var testItems
变量初始化 List
。在我的 init()
函数中,我设置了一个订阅者并将它们存储在 cancellables
中,这样我就可以对更改做出反应。
如果我将一个项目添加到 testItems
数组,将执行 sink
回调以创建新快照并将它们应用到 data
。它有效,但我需要点击导航按钮两次,才能看到列表中的项目。两个问题:
- 为什么会这样,我该如何解决? (所以我只需要点击一次按钮就可以看到列表中的变化)
- 以及如何改进我的代码? (目前我总是创建一个新快照而不是扩展已经创建的快照)
让我通过回答你的第二个问题来回答这两个问题。
How can i improve my code? (currently I always create a new snapshot instead of extending the already created one)
我对您对 @resultBuilder
的使用感到有点困惑。通常,人们会使用结果生成器来创建领域特定语言 (DSL)。在这种情况下,您可以创建一个用于构造 ListItems
的 DSL,但这意味着您的意思是在编译时填充一个列表,这里的大部分代码似乎都集中在运行时动态更新列表。所以使用结果生成器似乎过于复杂。
在这种情况下,您还使用了 Publisher
,您可以通过在控制器的 属性 上使用简单的 didSet
来获得。但是,作为控制器试图与其视图协调的更复杂模型的一部分,发布者将是一个非常好的主意。我有一个你的代码版本,我用 didSet
替换了 Publisher,但再看一眼 - 想象更复杂的模型案例,我把发布者放回去了。
你的发布者的管道都在你的结果构建器中纠缠不清——这很奇怪,因为发布者再次对运行时的变化做出动态反应,而结果构建器是为编译的语法加糖制作好的 DSL时间码。
所以我退出了 DSL,并建立了一个丰富的管道,充分利用了发布者。
此外,在使用 Combine 发布者时,通常使用类型擦除来使发布者的实际性质更加匿名。所以在我的返工中,我使用 eraseToAnyPublisher
以便 List
可以从任何人那里获取它的值,而不仅仅是 @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([=10=]) }}
.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>
的形式进入 class。
然后你的 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
相关的所有内容都消失了。如果您想创建一个函数来为 table 中的初始项目集构建一组 ListItems
(或具有静态内容的 table),也许您可以使用它但是这里似乎没有必要。