如何使用 SwiftUI 和 Combine 将 child 视图的状态更改传播到 parent 视图?
How to use SwiftUI and Combine to propagate state changes of child View to parent View?
我如何使用 SwiftUI 和 Combine,让最上面的 View 的状态取决于其包含的 SubView 的状态,该状态由依赖于 its[= 的其他标准确定64=] 包含 SubSubView?
场景
我有以下视图层次结构:V1 包含 V2,V2 包含 V3。
V1 是一般的,主要是装饰性的,'wrapper' 特定设置视图 V2,并包含一个 "Save" 按钮。 Bool
类型按钮的禁用状态应取决于 V2 的 save-ability 状态。
V2是具体的设置视图。 哪个类型的V2,显示的具体设置,根据我程序的其余部分可能会有所不同。保证能够确定其save-ability。它包含一个 Toggle 和 V3,一个 MusicPicker。 V2 的 save-ability 依赖于标准处理 V3 的 selection-state 及其 Toggle-state.
V3 是具有 selection-state 类型 Int?
的通用 'MusicPicker' 视图。它可以与任何 parent 一起使用,双向通信其 selection-state.
A Binding
通常应该用于在两个视图之间来回通信。因此,V1 和 V2 以及 V2 和 V3 之间可能存在绑定。然而,就我 know/understand 而言,V2 cannot/should 不会对 V3 的绑定值更改做出反应并将此(连同其他标准)传回 V1。我可以使用 ObservableObject
s 与 V1 和 V2 共享一个 save-ability 并与 V2 和 V3 共享一个 selection-state,但我不清楚如何将 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 更改。
问题
(在这种情况下)什么是最合适的方法来响应 V3 中的变化,使用 SwiftUI 和 Combine 处理 V2 中的其他状态,并相应地改变 V1 的状态?
因为 SwiftUI 不支持在嵌套的 ObservableObject 中刷新视图,您需要手动执行此操作。我在这里发布了一个关于如何做到这一点的解决方案:
(例如 ObservedObject)
(例如使用 EnvironmentObject)
我找到了一种最终结果相同的工作方法,可能对其他人有用。然而,它并没有按照我在问题中要求的方式传递数据,但 SwiftUI 似乎无论如何都不适合这样做。
由于 V2,'middle' 视图,可以正确访问两个重要状态,即选择和保存能力,我意识到我可以使 V2 成为父视图并拥有 V1,最初是 'parent' 视图,改为接受 @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)
}
}
}
我希望这对其他人有用。
在使用 ObservableObject
方法的前提下发布此答案,该方法已添加到您的问题本身。
仔细看。只要代码:
.onReceive(pickerStore.objectWillChange){ selected in
print("Called for selected: \(selected ?? -1)")
self.settingsStore.saveable = self.someCriteriaProcess()
}
在 SpecificSettingsView
中运行,settingsStore
即将更改,这会触发父 SettingsView
刷新其关联的视图组件。这意味着 func getSpecificV2() -> AnyView
将 return SpecificSettingsView
对象依次实例化 PickerStore
。因为,
SwiftUI views, being value type (as they are struct), will not retain your objects within their view scope if the view is recreated by a parent view, for example. So it’s best to pass those observable objects by reference and have a sort of container view, or holder class, which will instantiate and reference those objects. If the view is the only owner of this object, and that view is recreated because its parent view is updated by SwiftUI, you’ll lose the current state of your 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
我如何使用 SwiftUI 和 Combine,让最上面的 View 的状态取决于其包含的 SubView 的状态,该状态由依赖于 its[= 的其他标准确定64=] 包含 SubSubView?
场景
我有以下视图层次结构:V1 包含 V2,V2 包含 V3。
V1 是一般的,主要是装饰性的,'wrapper' 特定设置视图 V2,并包含一个 "Save" 按钮。
Bool
类型按钮的禁用状态应取决于 V2 的 save-ability 状态。V2是具体的设置视图。 哪个类型的V2,显示的具体设置,根据我程序的其余部分可能会有所不同。保证能够确定其save-ability。它包含一个 Toggle 和 V3,一个 MusicPicker。 V2 的 save-ability 依赖于标准处理 V3 的 selection-state 及其 Toggle-state.
V3 是具有 selection-state 类型
Int?
的通用 'MusicPicker' 视图。它可以与任何 parent 一起使用,双向通信其 selection-state.
A Binding
通常应该用于在两个视图之间来回通信。因此,V1 和 V2 以及 V2 和 V3 之间可能存在绑定。然而,就我 know/understand 而言,V2 cannot/should 不会对 V3 的绑定值更改做出反应并将此(连同其他标准)传回 V1。我可以使用 ObservableObject
s 与 V1 和 V2 共享一个 save-ability 并与 V2 和 V3 共享一个 selection-state,但我不清楚如何将 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 更改。
问题
(在这种情况下)什么是最合适的方法来响应 V3 中的变化,使用 SwiftUI 和 Combine 处理 V2 中的其他状态,并相应地改变 V1 的状态?
因为 SwiftUI 不支持在嵌套的 ObservableObject 中刷新视图,您需要手动执行此操作。我在这里发布了一个关于如何做到这一点的解决方案:
我找到了一种最终结果相同的工作方法,可能对其他人有用。然而,它并没有按照我在问题中要求的方式传递数据,但 SwiftUI 似乎无论如何都不适合这样做。
由于 V2,'middle' 视图,可以正确访问两个重要状态,即选择和保存能力,我意识到我可以使 V2 成为父视图并拥有 V1,最初是 'parent' 视图,改为接受 @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)
}
}
}
我希望这对其他人有用。
在使用 ObservableObject
方法的前提下发布此答案,该方法已添加到您的问题本身。
仔细看。只要代码:
.onReceive(pickerStore.objectWillChange){ selected in
print("Called for selected: \(selected ?? -1)")
self.settingsStore.saveable = self.someCriteriaProcess()
}
在 SpecificSettingsView
中运行,settingsStore
即将更改,这会触发父 SettingsView
刷新其关联的视图组件。这意味着 func getSpecificV2() -> AnyView
将 return SpecificSettingsView
对象依次实例化 PickerStore
。因为,
SwiftUI views, being value type (as they are struct), will not retain your objects within their view scope if the view is recreated by a parent view, for example. So it’s best to pass those observable objects by reference and have a sort of container view, or holder class, which will instantiate and reference those objects. If the view is the only owner of this object, and that view is recreated because its parent view is updated by SwiftUI, you’ll lose the current state of your
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