这是有关SwiftUI和架构的一般性问题,我将举一个简单但存在问题的例子。
初始项目:
我有一个首个View
,用于显示Item
列表。此列表由一个类(我在此称之为ListViewModel
)管理。在第二个视图中,我可以修改其中一个Item
,并使用“保存”按钮保存这些修改。在简化版本中,我可以使用@Binding
轻松完成此操作。感谢SwiftUI:
struct ListView: View {
@StateObject var vm = ListViewModel()
var body: some View {
NavigationView {
List(Array(vm.data.enumerated()), id: \.1.id) { index, item in
NavigationLink(destination: DetailView(item: $vm.data[index])) {
Text(item.name)
}
}
}
}
}
struct DetailView: View {
@Binding var initialItem: Item
@State private var item: Item
init(item: Binding<Item>) {
_item = State(initialValue: item.wrappedValue)
_initialItem = item
}
var body: some View {
VStack {
TextField("name", text: $item.name)
TextField("description", text: $item.description)
Button("save") {
initialItem = item
}
}
}
}
struct Item: Identifiable {
let id = UUID()
var name: String
var description: String
static var fakeItems: [Item] = [.init(name: "My item", description: "Very good"), .init(name: "An other item", description: "not so bad")]
}
class ListViewModel: ObservableObject {
@Published var data: [Item] = Item.fakeItems
func fetch() {}
func save() {}
func sort() {}
}
问题:
当详细/编辑视图变得更加复杂时,情况会变得更加复杂。它的属性数量增加,我们必须设置代码,不涉及 View
(网络、存储等),可能是一个有限状态机(FSM),所以我们另外创建一个 class
来处理 DetailView
(在我的示例中: DetailViewModel
)。
现在两个视图之间的通讯变得更加复杂,使用 @Binding
进行简单的通讯已不再可行。在我们的示例中,这两个元素没有直接关联,因此我们需要设置双向绑定:
class ListViewModel: ObservableObject {
@Published var data: [Item] <-----------
func fetch() {} |
func save() {} |
func sort() {} |
} | /In Search Of Binding/
|
class DetailViewModel: ObservableObject { |
@Published var initialItem: Item <----------
@Published var item: Item
init(item: Item) {
self.initialItem = item
self.item = item
}
func fetch() {}
func save() {
self.initialItem = item
}
}
尝试
1. 在ListViewModel中使用DetailViewModel的数组+Combine
与其存储Item
的 Array
,我的 ListViewModel
可以存储一个 [DetailViewModel]
。因此在初始化期间,它可以订阅 DetailViewModel
的更改:
class ListViewModel: ObservableObject {
@Published var data: [DetailViewModel]
var bag: Set<AnyCancellable> = []
init(items: [Item] = Item.fakeItems) {
data = items.map(DetailViewModel.init(item:))
subscribeToItemsChanges()
}
func subscribeToItemsChanges() {
data.enumerated().publisher
.flatMap { (index, detailVM) in
detailVM.$initialItem
.map{ (index, $0 )}
}
.sink { [weak self] index, newValue in
self?.data[index].item = newValue
self?.objectWillChange.send()
}
.store(in: &bag)
}
}
结果: 好的,这样做可以工作,但它并不真正是双向绑定。 但是 ViewModel 包含其他 ViewModel 数组真的很重要吗? a)它有点奇怪。b)我们有一组引用(而没有数据类型)。c)最终在视图中得到的是:
List(Array(vm.data.enumerated()), id: \.1.item.id) { index, detailVM in
NavigationLink(destination: DetailView(vm: detailVM)) {
Text(detailVM.item.name)
}
}
2. 将ListViewModel的引用传递给DetailViewModel(委托样式)
由于DetailViewModel
不包含Item
数组,而且它处理的Item
不再具有@Binding
,因此我们可以将ListViewModel
(它包含该数组)传递给每个DetailViewModel
。
protocol UpdateManager {
func update(_ item: Item, at index: Int)
}
class ListViewModel: ObservableObject, UpdateManager {
@Published var data: [Item]
init(items: [Item] = Item.fakeItems) {
data = items
}
func update(_ item: Item, at index: Int) {
data[index] = item
}
}
class DetailViewModel: ObservableObject {
@Published var item: Item
private var updateManager: UpdateManager
private var index: Int
init(item: Item, index: Int, updateManager: UpdateManager) {
self.item = item
self.updateManager = updateManager
self.index = index
}
func fetch() {}
func save() {
updateManager.update(item, at: index)
}
}
结果: 它可以工作,但是:1)它看起来像一个旧的方式,不太符合SwiftUI的风格。2)我们必须将项目的索引传递给DetailViewModel。
3. 使用闭包
与其传递对整个ListViewModel
的引用,我们可以将一个闭包(onSave
)传递给DetailViewModel
。
class ListViewModel: ObservableObject {
@Published var data: [Item]
init(items: [Item] = Item.fakeItems) {
data = items
}
func update(_ item: Item, at index: Int) {
data[index] = item
}
}
class DetailViewModel: ObservableObject {
@Published var item: Item
var update: (Item) -> Void
init(item: Item, onSave update: @escaping (Item) -> Void) {
self.item = item
self.update = update
}
func fetch() {}
func save() {
update(item)
}
}
结果: 一方面它看起来仍然像是一种旧的方法,另一方面它似乎符合“一个视图-一个ViewModel”的方法。如果我们使用FSM,我们可以想象发送一个事件/输入。
变量:
我们可以使用Combine并传递一个PassthroughSubject
而不是闭包:
class ListViewModel: ObservableObject {
@Published var data: [Item]
var archivist = PassthroughSubject<(Int, Item), Never>()
var cancellable: AnyCancellable?
init(items: [Item] = Item.fakeItems) {
data = items
cancellable = archivist
.sink {[weak self ]index, item in
self?.update(item, at: index)
}
}
func update(_ item: Item, at index: Int) {
data[index] = item
}
}
class DetailViewModel: ObservableObject {
@Published var item: Item
var index: Int
var archivist: PassthroughSubject<(Int, Item), Never>
init(item: Item, saveWith archivist: PassthroughSubject<(Int, Item), Never>, at index: Int) {
self.item = item
self.archivist = archivist
self.index = index
}
func fetch() {}
func save() {
archivist.send((index, item))
}
}
问题:
我也可以在我的ObservableObject
中使用@Binding
,或者甚至将我的Item
数组包装在另一个ObservableObject
中(因此在一个OO中有另一个OO)。 但对我来说似乎更不相关。
无论如何,一旦我们离开了简单的Model-View架构,一切似乎都变得非常复杂:在那里,一个简单的@Binding
就足够了。
所以我请求你的帮助: 你对这种情况有什么建议? 你认为对SwiftUI最合适的是什么? 你能想到更好的方法吗?
@Published var data: [Item]
更改为@Published var data: [DetailViewModel]
并且将你的DetailView
改为使用@ObservedObject var item: Item
。 - lorem ipsumItem
是一个结构体。你的意思是第一次尝试没问题吗? - Adrien