如何在 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?尽管在 parentViewchildViewA.

中打印了正确的值,但我到目前为止在 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) }
}