如何在以下mvvm体系结构中使用@Binding包装器?

时间:2020-06-30 10:32:41

标签: swift mvvm swiftui

我已经建立了一个mvvm体系结构。我有一个模型,有很多视图,每个视图有一个商店。为了说明我的问题,请考虑以下事项:
在我的模型中,存在一个用户对象user和两个带有两个商店(商店A,商店B)的视图(A和B),这两个商店都使用该用户对象。视图A和视图B彼此不依赖(两者都具有不共享用户对象的不同存储),但是都能够编辑user对象的状态。显然,您需要以某种方式将更改从一个存储传播到另一个存储。为此,我建立了一个存储层次结构,其中一个根存储维护整个“应用程序状态”(共享对象的所有状态,例如user)。现在,存储A和B仅维护对根存储对象的引用,而不维护对象本身。我现在期望,如果我在视图A中更改对象,则存储A将把更改传播到根存储,而根存储又将更改再次传播到存储B。当我切换到视图B时,我应该能够现在来看我的变化。我在商店A和B中使用绑定来指代根商店对象。但这不能正常工作,我只是不了解Swift的Binding的行为。这是我作为最低版本设置的具体设置:

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。现在我的ContentView:

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,并且类似地用于调试目的,并且绑定实际上似乎起作用。更改已传播,但不知何故视图不会更新。我还强迫进行了一些人为的状态更改(添加状态bool变量并仅在onAppear()中进行了切换),该视图会重新渲染,但仍然不采用更新后的值,我只是不知道该怎么做。

编辑:这是我的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代码段中传递属性。

2 个答案:

答案 0 :(得分:2)

在您的方案中,更合适的是将User as- a ObservableObject设置为在商店之间通过引用传递,以及在相应的视图中显式用作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)")
    }
}

答案 1 :(得分:0)

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

  1. 此处的共享状态是用户对象。将其放在@EnvironmentObject中,根据定义,它是由层次结构中的视图共享的外部状态对象。这样,您无需通知StoreA,后者先通知RootStore,然后再通知StoreB。

  2. 然后,StoreA,StoreB可以是本地@State,并且不需要RootStore。商店A,B可以是值类型,因为没有什么可观察的。

  3. 由于@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)")
     }
    

其余应该简单明了。

此比较的重点是什么?

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

  2. MVVM告诉您有关状态管理的任何信息。当您学习了如何分配和管理状态时,SwiftUI最为强大。但是,MVVM在很大程度上依赖@ObservedObject进行绑定,因为iOS没有绑定。对于初学者来说这很危险,因为它必须是引用类型。在这种情况下,结果可能是过度使用了引用类型,从而破坏了围绕值类型构建的SDK的全部目的。

  3. 它也删除了大多数样板初始化代码,并且一个代码可以专注于1个共享状态对象,而不是4个。

  4. 如果您认为SwiftUI的创建者是白痴,则SwiftUI不可扩展,并且需要MVVM,IMO,这是您的错。