如何使用SwiftUI和Combine将子视图的状态更改传播到父视图?

时间:2019-12-18 13:58:25

标签: swift swiftui combine

我如何使用SwiftUI和Combine来使最高视图的状态取决于其包含的SubView的状态,而该状态取决于其他条件,而该标准取决于所包含的SubSubView?


场景

我具有以下View层次结构:V1包含V2,V2包含V3。

  1. V1是特定设置视图V2的常规“包装”,通常是带有装饰性的“包装”,并带有“保存”按钮。类型为Bool的按钮的禁用状态应取决于V2的可保存性状态。

  2. V2是特定的设置视图。 V2的哪种类型(显示的具体设置)可能会有所不同,具体取决于程序的其余部分。保证能够确定其保存能力。它包含一个Toggle和一个V3,一个MusicPicker。 V2的可保存性取决于处理V3的选择状态及其切换状态的条件。

  3. V3是一般的“ MusicPicker”视图,其选择状态为Int?。它可以与任何父级一起使用,可以双向传递其选择状态。

通常应使用Binding在2个视图之间来回通信。这样,V1和V2以及V2和V3之间可能存在绑定。但是,据我所知/理解,V2无法/不应对V3的绑定的值更改作出反应,并将其(连同其他条件)传达回V1。我可以使用ObservableObject与V1和V2共享保存能力,并与V2和V3共享选择状态,但是我不清楚如何将V3的ObservableObject更改与其他条件集成以设置V1的ObservableObject

示例

使用@State@Binding

/* V1 */
struct SettingsView: View {
    @State var saveable = false

    var body: some View {
        VStack {
            Button(action: saveAction){
                Text("Save")
            }.disabled(!saveable)
            getSpecificV2(saveable: $saveable)
        }
    }

    func getSpecificV2(saveable: Binding<Bool>) -> AnyView {
        // [Determining logic...]
        return AnyView(SpecificSettingsView(saveable: saveable))
    }

    func saveAction(){
        // More code...
    }
}

/* V2 */
struct SpecificSettingsView: View {
    @Binding var saveable: Bool

    @State var toggled = false
    @State var selectedValue: Int?

    var body: some View {
        Form {
            Toggle("Toggle me", isOn: $toggled)
            CustomPicker(selected: $selectedValue)
        }
    }

    func someCriteriaProcess() -> Bool {
        if let selected = selectedValue {
            return (selected == 5)
        } else {
            return toggled
        }
    }
}

/* V3 */
struct CustomPicker: View {
    @Binding var selected: Int?

    var body: some View {
        List {
            Text("None")
                .onTapGesture {
                    self.selected = nil
            }.foregroundColor(selected == nil ? .blue : .primary)
            Text("One")
                .onTapGesture {
                    self.selected = 1
            }.foregroundColor(selected == 1 ? .blue : .primary)
            Text("Two")
                .onTapGesture {
                    self.selected = 2
            }.foregroundColor(selected == 2 ? .blue : .primary)
        }
    }
}

在此示例代码中,我实际上需要使saveable依赖于someCriteriaProcess()

使用ObservableObject

为响应Tobias的回答,可能的替代方法是使用ObservableObject s。

/* V1 */
class SettingsStore: ObservableObject {
  @Published var saveable = false
}

struct SettingsView: View {
    @ObservedObject var store = SettingsStore()

    var body: some View {
        VStack {
            Button(action: saveAction){
                Text("Save")
            }.disabled(!store.saveable)
            getSpecificV2()
        }.environmentObject(store)
    }

    func getSpecificV2() -> AnyView {
        // [Determining logic...]
        return AnyView(SpecificSettingsView())
    }

    func saveAction(){
        // More code...
    }
}

/* V2 */
struct SpecificSettingsView: View {
    @EnvironmentObject var settingsStore: SettingsStore
    @ObservedObject var pickerStore = PickerStore()

    @State var toggled = false
    @State var selectedValue: Int?

    var body: some View {
        Form {
            Toggle("Toggle me", isOn: $toggled)
            CustomPicker(store: pickerStore)
        }.onReceive(pickerStore.objectWillChange){ selected in
            print("Called for selected: \(selected ?? -1)")
            self.settingsStore.saveable = self.someCriteriaProcess()
        }
    }

    func someCriteriaProcess() -> Bool {
        if let selected = selectedValue {
            return (selected == 5)
        } else {
            return toggled
        }
    }
}

/* V3 */

class PickerStore: ObservableObject {
    public let objectWillChange = PassthroughSubject<Int?, Never>()
    var selected: Int? {
        willSet {
            objectWillChange.send(newValue)
        }
    }
}

struct CustomPicker: View {
    @ObservedObject var store: PickerStore

    var body: some View {
        List {
            Text("None")
                .onTapGesture {
                    self.store.selected = nil
            }.foregroundColor(store.selected == nil ? .blue : .primary)
            Text("One")
                .onTapGesture {
                    self.store.selected = 1
            }.foregroundColor(store.selected == 1 ? .blue : .primary)
            Text("Two")
                .onTapGesture {
                    self.store.selected = 2
            }.foregroundColor(store.selected == 2 ? .blue : .primary)
        }
    }
}

使用onReceive()附件,我尝试对PickerStore的任何更改做出反应。尽管该操作会触发并且调试可以正确打印,但不会显示UI更改。


问题

在这种情况下,最合适的方法是使用SwiftUI和Combine对V3的变化做出反应,用V2中的其他状态进行处理以及相应地更改V1的状态?

3 个答案:

答案 0 :(得分:0)

由于SwiftUI不支持刷新嵌套的ObservableObject内的更改视图,因此您需要手动执行此操作。我在此处发布了有关如何执行此操作的解决方案:

https://stackoverflow.com/a/58996712/12378791(例如,使用ObservedObject)

https://stackoverflow.com/a/58878219/12378791(例如,使用EnvironmentObject)

答案 1 :(得分:0)

我想出了一种具有相同最终结果的可行方法,这可能对其他人有用。但是,它不能按照我在问题中所要求的方式传递数据,但是SwiftUI在任何情况下似乎都不适合这样做。

由于V2是“中间”视图,可以正确访问选择和保存能力这两个重要状态,所以我意识到我可以使V2成为父视图,并让V1最初是“父”视图成为子视图改为接受@ViewBuilder内容。此示例不适用于所有情况,但适用于我的情况。 一个有效的示例如下。

/* V2 */
struct SpecificSettingsView: View {
    @State var toggled = false
    @State var selected: Int?

    var saveable: Bool {
        return someCriteriaProcess()
    }

    var body: some View {
        SettingsView(isSaveable: self.saveable, onSave: saveAction){
            Form {
                Toggle("Toggle me", isOn: self.$toggled)
                CustomPicker(selected: self.$selected)
            }
        }
    }

    func someCriteriaProcess() -> Bool {
        if let selected = selected {
            return (selected == 2)
        } else {
            return toggled
        }
    }

    func saveAction(){
        guard saveable else { return }
        // More code...
    }
}

/* V1 */
struct SettingsView<Content>: View where Content: View {
    var content: () -> Content
    var saveAction: () -> Void
    var saveable: Bool

    init(isSaveable saveable: Bool, onSave saveAction: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content){
        self.saveable = saveable
        self.saveAction = saveAction
        self.content = content
    }

    var body: some View {
        VStack {
            // More decoration
            Button(action: saveAction){
                Text("Save")
            }.disabled(!saveable)
            content()
        }
    }
}


/* V3 */
struct CustomPicker: View {
    @Binding var selected: Int?

    var body: some View {
        List {
            Text("None")
                .onTapGesture {
                    self.selected = nil
            }.foregroundColor(selected == nil ? .blue : .primary)
            Text("One")
                .onTapGesture {
                    self.selected = 1
            }.foregroundColor(selected == 1 ? .blue : .primary)
            Text("Two")
                .onTapGesture {
                    self.selected = 2
            }.foregroundColor(selected == 2 ? .blue : .primary)
        }
    }
}

我希望这对其他人有帮助。

答案 2 :(得分:0)

在问题本身带有方法ObservableObject的前提下发布此答案。

仔细看。尽快的代码:

.onReceive(pickerStore.objectWillChange){ selected in
    print("Called for selected: \(selected ?? -1)")
    self.settingsStore.saveable = self.someCriteriaProcess()
}

SpecificSettingsView中运行,settingsStore将要更改,从而触发父SettingsView刷新其关联的视图组件。这意味着func getSpecificV2() -> AnyView将返回SpecificSettingsView对象,该对象又将再次实例化PickerStore。因为,

  作为值类型的

SwiftUI视图(因为它们是结构),例如,如果视图是由父视图重新创建的,则不会将对象保留在其视图范围内。因此,最好通过引用传递这些可观察对象,并具有某种容器视图或Holder类,该容器视图或容器类将实例化并引用这些对象。如果该视图是该对象的唯一所有者,并且由于其父视图由SwiftUI更新而重新创建了该视图,则您将丢失ObservedObject的当前状态。

(上面的Read More


如果仅在视图层次结构(可能是最终父级)中将PickerStore的实例化推到更高位置,您将获得预期的行为。

struct SettingsView: View {
    @ObservedObject var store = SettingsStore()
    @ObservedObject var pickerStore = PickerStore()

    . . .

    func getSpecificV2() -> AnyView {
        // [Determining logic...]
        return AnyView(SpecificSettingsView(pickerStore: pickerStore))
    }

    . . .

}

struct SpecificSettingsView: View {
    @EnvironmentObject var settingsStore: SettingsStore
    @ObservedObject var pickerStore: PickerStore

    . . .

}

注意:我将项目上传到了远程存储库here