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



我如何使用 SwiftUI 和 Combine 让最上面的视图的状态依赖于其包含的 SubView 的状态,由依赖于包含的 SubSubView 的其他条件确定?


场景

我有以下视图层次结构:V1 包含 V2,其中包含 V3。

  1. V1 是特定设置视图 V2 的通用(主要是装饰性("包装器",并带有"保存"按钮。按钮的禁用状态(类型Bool应取决于 V2 的可保存性状态。

  2. V2 是特定的设置视图。哪种类型的 V2,显示的特定设置,可能会因程序的其余部分而异。保证能够确定其保存能力。它包含一个切换和V3,一个音乐选择器。V2 的保存能力取决于处理 V3 的选择状态及其切换状态的条件。

  3. V3 是具有类型为Int?的选择状态的常规"音乐选取器"视图。它可以与任何父级一起使用,双向通信其选择状态。

通常应使用Binding在 2 个视图之间来回通信。因此,V1 和 V2 以及 V2 和 V3 之间可能存在绑定。但是,据我所知/理解,V2 不能/不应该对绑定的 V3 值更改做出反应并将其(以及其他条件(传达回 V1。我可以使用ObservableObjects 与 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

作为对托比亚斯的回答的回应,一种可能的替代方案是使用ObservableObjects。

/* 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 状态的最合适方法是什么?

在方法的前提下发布此答案,并在您的问题本身中添加ObservableObject

仔细看。一旦代码:

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

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

例如,如果父视图

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

(阅读更多内容(


如果您只是将视图层次结构(可能是最终父级(中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
. . .
}

注意:我在这里将项目上传到远程仓库

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

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

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

我已经找到了一种具有相同最终结果的工作方法,可能对其他人有用。但是,它不会按照我在问题中要求的方式传递数据,但 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)
}
}
}

我希望这对其他人有用。

最新更新