如何在 SwiftUI 中将数据从 child 视图传递到 parent 视图到另一个 child 视图?
How do I pass data from a child view to a parent view to another child view in SwiftUI?
如标题所示,我试图通过 parent 视图 (P) 将数据从一个 child 视图 (A) 传递到另一个 child 视图 (B) ).
parent 视图如下所示:
@State var rectFrame: CGRect = .zero
var body: some View {
childViewA(rectFrame: $rectFrame)
childViewB()
}
其中 childViewA
获得 childViewB
需要的 CGRect
。
childViewA
看起来像这样:
@Binding var rectFrame: CGRect
var body: some View {
// Very long code where rectFrame is obtained and printed to console correctly
}
如何将 rectFrame
传递给 childViewB
?尽管在 parentView
和 childViewA
.
中打印了正确的值,但我到目前为止在 childViewB
中尝试的所有内容 returns CGRect.zero
为了尝试将值传递给 childViewB
,我将 parentView
重写为:
@State var rectFrame: CGRect = .zero
var body: some View {
childViewA(rectFrame: $rectFrame)
childViewB(rectFrame: $rectFrame.value)
}
childViewB
具有以下结构:
var rectFrame: CGRect = CGRect.zero
var body: some View {
}
但是每次都会打印 CGRect.zero
。
我最近尝试了 @ObjectBinding
但我一直在努力,所以如果有人能帮助我解决这个具体的例子,我将不胜感激。
class SourceRectBindings: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var sourceRect: CGRect = .zero {
willSet {
willChange.send()
}
}
}
你没有提到ChildViewB是如何获得它的rect的。也就是说,它是否使用 GeometryReader 或任何其他来源。无论您使用什么方法,您都可以通过两种方式将信息向上传递到层次结构:通过绑定或通过首选项。如果涉及几何,甚至锚点偏好。
对于第一种情况,在没有看到你的实现的情况下,你可能在设置 rect 时漏掉了一个 DispatchQueue.main.async {}
。这是因为更改必须在 视图完成自身更新后 发生。但是,我认为这是一个肮脏的技巧,只在测试时使用它,但绝不会在生产代码中使用它。
第二种选择(使用首选项)是一种更可靠的方法(并且是首选 ;-)。我将包括这两种方法的代码,但您应该明确地了解更多关于偏好的信息。我最近写了一篇文章,你可以在这里找到:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
第一种方法:我不推荐在层次结构中向上传递几何值,这会影响其他视图的绘制方式。但如果你坚持,这就是你如何让它发挥作用的方法:
struct ContentView : View {
@State var rectFrame: CGRect = .zero
var body: some View {
VStack {
ChildViewA(rectFrame: $rectFrame)
ChildViewB(rectFrame: rectFrame)
}
}
}
struct ChildViewA: View {
@Binding var rectFrame: CGRect
var body: some View {
DispatchQueue.main.async {
self.rectFrame = CGRect(x: 10, y: 20, width: 30, height: 40)
}
return Text("CHILD VIEW A")
}
}
struct ChildViewB: View {
var rectFrame: CGRect = CGRect.zero
var body: some View {
Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
}
}
第二种方法:使用首选项
import SwiftUI
struct MyPrefKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct ContentView : View {
@State var rectFrame: CGRect = .zero
var body: some View {
VStack {
ChildViewA()
ChildViewB(rectFrame: rectFrame)
}
.onPreferenceChange(MyPrefKey.self) {
self.rectFrame = [=11=]
}
}
}
struct ChildViewA: View {
var body: some View {
return Text("CHILD VIEW A")
.preference(key: MyPrefKey.self, value: CGRect(x: 10, y: 20, width: 30, height: 40))
}
}
struct ChildViewB: View {
var rectFrame: CGRect = CGRect.zero
var body: some View {
Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
}
}
你可以使用 EnvironmentObject 来处理这样的事情...
EnvironmentObject 的好处在于,无论何时何地更改其中一个变量,它始终会确保在使用该变量的每个地方进行更新。
Note: To get every variable to update, you have to make sure that you pass through the BindableObject class / EnvironmentObject on everyView you wish to have it updated in...
SourceRectBindings:
class SourceRectBindings: BindableObject {
/* PROTOCOL PROPERTIES */
var willChange = PassthroughSubject<Application, Never>()
/* Seems Like you missed the 'self' (send(self)) */
var sourceRect: CGRect = .zero { willSet { willChange.send(self) }
}
父级:
struct ParentView: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
childViewA()
.environmentObject(sourceRectBindings)
childViewB()
.environmentObject(sourceRectBindings)
}
}
ChildViewA:
struct ChildViewA: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
// Update your frame and the environment...
// ...will update the variable everywhere it was used
self.sourceRectBindings.sourceRect = CGRect(x: 0, y: 0, width: 300, height: 400)
return Text("From ChildViewA : \(self.sourceRectBindings.sourceRect)")
}
}
ChildViewB:
struct ChildViewB: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
// This variable will always be up to date
return Text("From ChildViewB : \(self.sourceRectBindings.sourceRect)")
}
}
让它发挥作用的最后步骤
• 进入你希望变量更新的最高视图,这是你的父视图,但我通常更喜欢我的 SceneDelegate ...所以我通常这样做:
let sourceRectBindings = SourceRectBindings()
• 然后在同一个 class 'SceneDelegate' 中转到我指定根视图的位置并通过我的 EnviromentObject
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(sourceRectBindings)
)
• 然后在最后一步,我将它传递给可能的父视图
struct ContentView : View {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
ParentView()
.environmentObject(sourceRectBindings)
}
}
If you run the code just like this, you'll see that your preview throws errors. Thats because it didn't get passed the environment variable.
To do so just start a dummy like instance of your environment:
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(SourceRectBindings())
}
}
#endif
现在你可以随心所欲地使用那个变量,如果你想传递 rect 以外的任何其他东西......你可以只将变量添加到 EnvironmentObject class 和您可以访问和更新它
我希望我已经正确理解了您的问题,即在两个 children 之间共享信息。我采用的方法是在两个共享相同状态的 children 中使用 @Binding 变量,即用 $rectFrame 实例化两者。这样两者将始终具有相同的值而不是实例化时的值。
在我的例子中,它是一个数组,在一个 child 中更新并显示在另一个列表中,因此 SwiftUI 负责对更改做出反应。也许你的情况没那么简单。
PhilipJacob 的回答很好,但是这个 API 有更新,我想我会提到它。
所以如果你想使用 BindableObject
已经 renamed/updated 到 ObservableObject
而且 var willChange
变成 var objectWillChange
所以他的答案变成了:
import Combine
class SourceRectBindings: ObservableObject {
/* PROTOCOL PROPERTIES */
var objectWillChange = PassthroughSubject<Void, Never>()
/* Seems Like you missed the 'self' (send(self)) */
var sourceRect: CGRect = .zero { willSet { objectWillChange.send(self) }
}
如标题所示,我试图通过 parent 视图 (P) 将数据从一个 child 视图 (A) 传递到另一个 child 视图 (B) ).
parent 视图如下所示:
@State var rectFrame: CGRect = .zero
var body: some View {
childViewA(rectFrame: $rectFrame)
childViewB()
}
其中 childViewA
获得 childViewB
需要的 CGRect
。
childViewA
看起来像这样:
@Binding var rectFrame: CGRect
var body: some View {
// Very long code where rectFrame is obtained and printed to console correctly
}
如何将 rectFrame
传递给 childViewB
?尽管在 parentView
和 childViewA
.
childViewB
中尝试的所有内容 returns CGRect.zero
为了尝试将值传递给 childViewB
,我将 parentView
重写为:
@State var rectFrame: CGRect = .zero
var body: some View {
childViewA(rectFrame: $rectFrame)
childViewB(rectFrame: $rectFrame.value)
}
childViewB
具有以下结构:
var rectFrame: CGRect = CGRect.zero
var body: some View {
}
但是每次都会打印 CGRect.zero
。
我最近尝试了 @ObjectBinding
但我一直在努力,所以如果有人能帮助我解决这个具体的例子,我将不胜感激。
class SourceRectBindings: BindableObject {
let willChange = PassthroughSubject<Void, Never>()
var sourceRect: CGRect = .zero {
willSet {
willChange.send()
}
}
}
你没有提到ChildViewB是如何获得它的rect的。也就是说,它是否使用 GeometryReader 或任何其他来源。无论您使用什么方法,您都可以通过两种方式将信息向上传递到层次结构:通过绑定或通过首选项。如果涉及几何,甚至锚点偏好。
对于第一种情况,在没有看到你的实现的情况下,你可能在设置 rect 时漏掉了一个 DispatchQueue.main.async {}
。这是因为更改必须在 视图完成自身更新后 发生。但是,我认为这是一个肮脏的技巧,只在测试时使用它,但绝不会在生产代码中使用它。
第二种选择(使用首选项)是一种更可靠的方法(并且是首选 ;-)。我将包括这两种方法的代码,但您应该明确地了解更多关于偏好的信息。我最近写了一篇文章,你可以在这里找到:https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
第一种方法:我不推荐在层次结构中向上传递几何值,这会影响其他视图的绘制方式。但如果你坚持,这就是你如何让它发挥作用的方法:
struct ContentView : View {
@State var rectFrame: CGRect = .zero
var body: some View {
VStack {
ChildViewA(rectFrame: $rectFrame)
ChildViewB(rectFrame: rectFrame)
}
}
}
struct ChildViewA: View {
@Binding var rectFrame: CGRect
var body: some View {
DispatchQueue.main.async {
self.rectFrame = CGRect(x: 10, y: 20, width: 30, height: 40)
}
return Text("CHILD VIEW A")
}
}
struct ChildViewB: View {
var rectFrame: CGRect = CGRect.zero
var body: some View {
Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
}
}
第二种方法:使用首选项
import SwiftUI
struct MyPrefKey: PreferenceKey {
typealias Value = CGRect
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct ContentView : View {
@State var rectFrame: CGRect = .zero
var body: some View {
VStack {
ChildViewA()
ChildViewB(rectFrame: rectFrame)
}
.onPreferenceChange(MyPrefKey.self) {
self.rectFrame = [=11=]
}
}
}
struct ChildViewA: View {
var body: some View {
return Text("CHILD VIEW A")
.preference(key: MyPrefKey.self, value: CGRect(x: 10, y: 20, width: 30, height: 40))
}
}
struct ChildViewB: View {
var rectFrame: CGRect = CGRect.zero
var body: some View {
Text("CHILD VIEW B: RECT WIDTH = \(rectFrame.size.width)")
}
}
你可以使用 EnvironmentObject 来处理这样的事情...
EnvironmentObject 的好处在于,无论何时何地更改其中一个变量,它始终会确保在使用该变量的每个地方进行更新。
Note: To get every variable to update, you have to make sure that you pass through the BindableObject class / EnvironmentObject on everyView you wish to have it updated in...
SourceRectBindings:
class SourceRectBindings: BindableObject {
/* PROTOCOL PROPERTIES */
var willChange = PassthroughSubject<Application, Never>()
/* Seems Like you missed the 'self' (send(self)) */
var sourceRect: CGRect = .zero { willSet { willChange.send(self) }
}
父级:
struct ParentView: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
childViewA()
.environmentObject(sourceRectBindings)
childViewB()
.environmentObject(sourceRectBindings)
}
}
ChildViewA:
struct ChildViewA: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
// Update your frame and the environment...
// ...will update the variable everywhere it was used
self.sourceRectBindings.sourceRect = CGRect(x: 0, y: 0, width: 300, height: 400)
return Text("From ChildViewA : \(self.sourceRectBindings.sourceRect)")
}
}
ChildViewB:
struct ChildViewB: view {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
// This variable will always be up to date
return Text("From ChildViewB : \(self.sourceRectBindings.sourceRect)")
}
}
让它发挥作用的最后步骤
• 进入你希望变量更新的最高视图,这是你的父视图,但我通常更喜欢我的 SceneDelegate ...所以我通常这样做:
let sourceRectBindings = SourceRectBindings()
• 然后在同一个 class 'SceneDelegate' 中转到我指定根视图的位置并通过我的 EnviromentObject
window.rootViewController = UIHostingController(
rootView: ContentView()
.environmentObject(sourceRectBindings)
)
• 然后在最后一步,我将它传递给可能的父视图
struct ContentView : View {
@EnvironmentObject var sourceRectBindings: SourceRectBindings
var body: some View {
ParentView()
.environmentObject(sourceRectBindings)
}
}
If you run the code just like this, you'll see that your preview throws errors. Thats because it didn't get passed the environment variable. To do so just start a dummy like instance of your environment:
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(SourceRectBindings())
}
}
#endif
现在你可以随心所欲地使用那个变量,如果你想传递 rect 以外的任何其他东西......你可以只将变量添加到 EnvironmentObject class 和您可以访问和更新它
我希望我已经正确理解了您的问题,即在两个 children 之间共享信息。我采用的方法是在两个共享相同状态的 children 中使用 @Binding 变量,即用 $rectFrame 实例化两者。这样两者将始终具有相同的值而不是实例化时的值。
在我的例子中,它是一个数组,在一个 child 中更新并显示在另一个列表中,因此 SwiftUI 负责对更改做出反应。也许你的情况没那么简单。
PhilipJacob 的回答很好,但是这个 API 有更新,我想我会提到它。
所以如果你想使用 BindableObject
已经 renamed/updated 到 ObservableObject
而且 var willChange
变成 var objectWillChange
所以他的答案变成了:
import Combine
class SourceRectBindings: ObservableObject {
/* PROTOCOL PROPERTIES */
var objectWillChange = PassthroughSubject<Void, Never>()
/* Seems Like you missed the 'self' (send(self)) */
var sourceRect: CGRect = .zero { willSet { objectWillChange.send(self) }
}