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

标签 swift mvvm swiftui

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

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 中,我有以下代码片段:

    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。现在我的内容 View :

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)
        }
    }
}

现在,两个 View :

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)")
    }
}

最后,我的问题是,更改并未按预期得到反射(reflect)。当我在 ViewA 中更改某些内容并切换到 ViewB 时,我看不到更新后的用户名字。当我改回 ViewA 时,我的更改也会丢失。我在商店内使用了 didSet 以及类似的调试目的,并且绑定(bind)实际上似乎有效。更改已传播,但不知何故 View 并未更新。我还强制进行了一些人为的状态更改(添加状态 bool 变量并在 onAppear() 中切换它), View 重新渲染,但它仍然不采用更新的值,我只是不这样做不知道该怎么办。

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

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 代码片段中的属性。

最佳答案

在您的场景中,更合适的做法是让 User 作为 ObservableObject 并在存储之间通过引用传递它,以及在相应的 View 中显式使用 ObservedObject。

这是根据您的代码快照组合并应用了该想法的简化演示。

使用 Xcode 11.4/iOS 13.4 进行测试

enter image description here

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)")
    }
}

关于swift - 如何在以下 mvvm 架构中使用 @Binding Wrapper?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62655457/

相关文章:

xaml - MVVM-Light 未显示设计模式数据

c# - WPF MVVM : Postpone rendering of View until DataContext is set

SwiftUI:在编辑开始时清除 TextField

ios - 我不断收到错误... 'use of unresolved identifier' 和 'Type gameType has no member pause game'

ios - 将非常大尺寸的图像缩小为非常小的图像,而不会失真和质量损失

mvvm - 在 MVVM 应用程序中为 MEF 使用组合模型是否有好处?

ios - SwiftUI @State 枚举即使在赋值后仍然为 nil

image - 如何将图像转换为 UIImage?

xcode - xcode 6 beta 7 中大量的警告

ios - 如何在3D Touch下注册预览单个单元格?