如何在以下 mvvm 架构中使用 @Binding Wrapper?

Posted

技术标签:

【中文标题】如何在以下 mvvm 架构中使用 @Binding Wrapper?【英文标题】:How can I use the @Binding Wrapper in the following mvvm architecture? 【发布时间】:2020-06-30 10:32:41 【问题描述】:

我已经建立了一个 mvvm 架构。我有一个模型,一堆视图,每个视图都有一个商店。为了说明我的问题,请考虑以下几点: 在我的模型中,存在一个用户对象user 和两个视图(A 和 B)以及两个都使用用户对象的商店(商店 A、商店 B)。视图 A 和视图 B 不相互依赖(两者都有不共享用户对象的不同存储),但都能够编辑 user 对象的状态。显然,您需要以某种方式将更改从一个商店传播到另一个商店。为了做到这一点,我建立了一个存储层次结构,其中一个根存储维护整个“应用程序状态”(共享对象的所有状态,如user)。现在,Store A 和 B 只维护对根存储对象的引用,而不是维护对象本身。我现在预计,如果我更改视图 A 中的对象,存储 A 会将更改传播到根存储,根存储会再次将更改传播到存储 B。当我切换到视图 B 时,我应该能够现在看看我的变化。我使用 Store A 和 B 中的 Bindings 来引用根存储对象。但这不能正常工作,我只是不理解 Swift 绑定的行为。这是我作为简约版本的具体设置:

public class RootStore: ObservableObject 
    @Published var storeA: StoreA?
    @Published var storeB: StoreB?
    
    @Published var user: User
    
    init(user: User) 
        self.user = user
    


extension ObservableObject 
    func binding<T>(for keyPath: ReferenceWritableKeyPath<Self, T>) -> Binding<T> 
        Binding(get:  [unowned self] in self[keyPath: keyPath] ,
                set:  [unowned self] in self[keyPath: keyPath] = $0 )
    


public class StoreA: ObservableObject 
    @Binding var user: User

    init(user: Binding<User>) 
        _user = user
    


public class StoreB: ObservableObject 
    @Binding var user: User

    init(user: Binding<User>) 
        _user = user
    

在我的 SceneDelegate.swift 中,我有以下 sn-p:

    user = User()
    let rootStore = RootStore(user: user)
    let storeA = StoreA(user: rootStore.binding(for: \.user))
    let storeB = StoreB(user: rootStore.binding(for: \.user))
    
    rootStore.storeA = storeA
    rootStore.storeB = storeB
    
    let contentView = ContentView()
        .environmentObject(appState) // this is used for a tabView. You can safely ignore this for this question
        .environmentObject(rootStore)

然后,contentView 作为 rootView 传递给 UIHostingController。现在我的内容视图:

struct ContentView: View 
    @EnvironmentObject var appState: AppState
    @EnvironmentObject var rootStore: RootStore
    
    var body: some View 
        TabView(selection: $appState.selectedTab) 
            ViewA().environmentObject(rootStore.storeA!).tabItem 
                Image(systemName: "location.circle.fill")
                Text("ViewA")
            .tag(Tab.viewA)

            ViewB().environmentObject(rootStore.storeB!).tabItem 
                Image(systemName: "waveform.path.ecg")
                Text("ViewB")
            .tag(Tab.viewB)
        
    

现在,两个视图:

struct ViewA: View 
    // The profileStore manages user related data
    @EnvironmentObject var storeA: StoreA
    
    var body: some View 
        Section(header: HStack 
            Text("Personal Information")
            Spacer()
            Image(systemName: "info.circle")
        ) 
            TextField("First name", text: $storeA.user.firstname)
        
    


struct ViewB: View 
    @EnvironmentObject var storeB: StoreB
    
    var body: some View 
        Text("\(storeB.user.firstname)")
    

最后,我的问题是,更改并没有像应有的那样反映出来。当我在 ViewA 中更改某些内容并切换到 ViewB 时,我看不到更新后的用户名。当我改回 ViewA 时,我的更改也丢失了。我在商店内使用didSet 和类似的用于调试目的,并且绑定实际上似乎工作。更改被传播但不知何故视图只是不更新​​。我还强制进行了一些人工状态更改(添加一个状态布尔变量并在onAppear() 中切换它),视图重新呈现,但它仍然没有采用更新的值,我只是不知道该怎么做。

编辑:这是我的用户对象的最小版本

public struct User 
    public var id: UUID?
    public var firstname: String
    public var birthday: Date

    public init(id: UUID? = nil,
                firstname: String,
                birthday: Date? = nil) 
        self.id = id
        self.firstname = firstname
        self.birthday = birthday ?? Date()
    

为简单起见,上面的 SceneDelegate.swift sn-p 中的属性我没有传递。

【问题讨论】:

这里的绑定是错误的工具。你会显示什么是用户? @Asperi 现在就在那儿 ;) 在您的场景中,将 User 作为 ObservableObject 并在商店之间通过引用传递它以及在相应视图中显式用作 ObservedObject 更为合适。 @Asperi 你能用一个小例子详细说明吗? 【参考方案1】:

在您的场景中,将 User 作为 ObservableObject 并在存储之间通过引用传递它以及在相应视图中显式用作 ObservedObject 更为合适。

这里是结合你的代码快照并应用了这个想法的简化演示。

使用 Xcode 11.4 / ios 13.4 测试

struct ContentView_Previews: PreviewProvider 
    static var previews: some View 
        let user = User(id: UUID(), firstname: "John")
        let rootStore = RootStore(user: user)
        let storeA = StoreA(user: user)
        let storeB = StoreB(user: user)
        rootStore.storeA = storeA
        rootStore.storeB = storeB

        return ContentView().environmentObject(rootStore)
    


public class User: ObservableObject 
    public var id: UUID?
    @Published public var firstname: String
    @Published public var birthday: Date

    public init(id: UUID? = nil,
                firstname: String,
                birthday: Date? = nil) 
        self.id = id
        self.firstname = firstname
        self.birthday = birthday ?? Date()
    


public class RootStore: ObservableObject 
    @Published var storeA: StoreA?
    @Published var storeB: StoreB?

    @Published var user: User

    init(user: User) 
        self.user = user
    


public class StoreA: ObservableObject 
    @Published var user: User

    init(user: User) 
        self.user = user
    


public class StoreB: ObservableObject 
    @Published var user: User

    init(user: User) 
        self.user = user
    


struct ContentView: View 
    @EnvironmentObject var rootStore: RootStore

    var body: some View 
        TabView 
            ViewA(user: rootStore.user).environmentObject(rootStore.storeA!).tabItem 
                Image(systemName: "location.circle.fill")
                Text("ViewA")
            .tag(1)

            ViewB(user: rootStore.user).environmentObject(rootStore.storeB!).tabItem 
                Image(systemName: "waveform.path.ecg")
                Text("ViewB")
            .tag(2)
        
    


struct ViewA: View 
    @EnvironmentObject var storeA: StoreA    // keep only if it is needed in real view

    @ObservedObject var user: User
    init(user: User) 
        self.user = user
    

    var body: some View 
        VStack 
            HStack 
                Text("Personal Information")
                Image(systemName: "info.circle")
            
            TextField("First name", text: $user.firstname)
        
    


struct ViewB: View 
    @EnvironmentObject var storeB: StoreB

    @ObservedObject var user: User
    init(user: User) 
        self.user = user
    

    var body: some View 
        Text("\(user.firstname)")
    

【讨论】:

【参考方案2】:

在此处提供一个替代答案,并对您的设计进行一些更改作为比较。

    这里的共享状态是用户对象。把它放在@EnvironmentObject 中,根据定义,它是层次结构中视图共享的外部状态对象。这样你就不需要通知 StoreA 通知 RootStore 然后通知 StoreB。

    那么StoreA、StoreB可以是本地的@State,不需要RootStore。存储 A、B 可以是值类型,因为没有什么可观察的。

    由于@EnvironmentObject 定义为一个 ObservableObject,我们不需要User 来 符合 ObservableObject,因此可以使User 成为值类型。

     final class EOState: ObservableObject 
    
         @Published var user = User()
    
     
    
     struct ViewA: View 
         @EnvironmentObject eos: EOState
         @State storeA = StoreA()
         // ... TextField("First name", text: $eos.user.firstname)
     
    
     struct ViewB: View 
         @EnvironmentObject eos: EOState
         @State storeB = StoreB()
         // ... Text("\(eos.user.firstname)")
     
    

其余的应该是直截了当的。

这个比较的要点是什么?

    应避免对象相互观察,或较长的发布链。它令人困惑、难以跟踪且不可扩展。

    MVVM 没有告诉您管理状态。当你学会了如何分配和管理你的状态时,SwiftUI 是最强大的。然而,MVVM 严重依赖 @ObservedObject 进行绑定,因为 iOS 没有绑定。对于初学者来说这是危险的,因为它需要是引用类型。在这种情况下,结果可能是过度使用引用类型,从而破坏了围绕值类型构建的 SDK 的全部目的。

    它还删除了大部分样板初始化代码,并且可以专注于 1 个共享状态对象而不是 4 个。

    如果您认为 SwiftUI 的创建者是白痴,那么 SwiftUI 是不可扩展的,并且需要在其之上使用 MVVM,IMO 你大错特错了。

【讨论】:

以上是关于如何在以下 mvvm 架构中使用 @Binding Wrapper?的主要内容,如果未能解决你的问题,请参考以下文章

绑定:未找到属性。 MVVM

MVVM 一种新型架构框架

Data Binding Guide——google官方文档翻译(上)

WPF MVVM Binding x:Name 不触发方法

WPF MVVM,Prism,Command Binding

PasswordBox和MVVM