如何将数据从子视图传递到父视图到 SwiftUI 中的另一个子视图?

Posted

技术标签:

【中文标题】如何将数据从子视图传递到父视图到 SwiftUI 中的另一个子视图?【英文标题】:How do I pass data from a child view to a parent view to another child view in SwiftUI? 【发布时间】:2019-07-22 06:51:33 【问题描述】:

正如标题所示,我正在尝试通过父视图 (P) 将数据从一个子视图 (A) 传递到另一个子视图 (B)。

父视图如下所示:

@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 中返回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()
        
    

【问题讨论】:

你是如何设置框架的?它只能通过 a) PreferenceKey 或 b) DispatchQueue.main.async ... 设置,否则更改将不会被传播。 @arsenius PreferenceKey 也许检查你的实现?我一直在使用this 很多就好了。 .background(GeometryBinder(rect: self.$childFrame)). 另外,这看起来有点可疑:childViewB(rectFrame: $rectFrame.value)。为什么不只是self.rectFrame 【参考方案1】:

你可以使用EnvironmentObject 来处理这样的事情......

EnvironmentObject 的好处在于,无论何时何地您更改其中的一个变量,它都将始终确保在使用该变量的每个地方都得到更新。

注意:要更新每个变量,您必须确保通过每个视图上的 BindableObject 类/EnvironmentObject 进行更新...

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()

• 然后在同一个类中,'SceneDelegate' 转到我指定根视图并通过我的 EnviromentObject 的位置

window.rootViewController = UIHostingController(
                rootView: ContentView()
                    .environmentObject(sourceRectBindings)
            )

• 然后在最后一步,我将它传递给可能的父视图

struct ContentView : View 

    @EnvironmentObject var sourceRectBindings: SourceRectBindings

    var body: some View 
        ParentView()
            .environmentObject(sourceRectBindings)
    

如果您像这样运行代码,您会看到预览会引发错误。那是因为它没有通过环境变量。 为此,只需启动一个类似于您的环境的虚拟实例:

#if DEBUG
struct ContentView_Previews : PreviewProvider 
    static var previews: some View 
        ContentView()
            .environmentObject(SourceRectBindings())
    

#endif

现在您可以随心所欲地使用该变量,如果您想传递除 rect 以外的任何其他内容...您只需将变量添加到 EnvironmentObject 类,您就可以访问和更新它强>

【讨论】:

【参考方案2】:

你没有提到 ChildViewB 如何获得它的矩形。也就是说,它是否使用 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 = $0
        

    


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)")
    

【讨论】:

如果您需要获取视图的大小和位置,请查看我发布的链接。例子很多。 我实际上是使用你的博客(所以偏好回答你的问题)来获取我在父视图中成功获取的 childA 中的 CGRect 信息。不幸的是,如果视图在列表中,Preferences 似乎没有得到正确的框架? @cyril 不看代码,我无法判断。也许这是一个坐标空间问题?还是对齐问题?如果您更新问题,我会看看。 另请注意,使用 EnvironmentObject (如在其他答案中)是一种可能的解决方案。我没有走那条路,因为我认为您打算将其封装起来。但也有可能采用更全球化的方法。 可能是坐标空间问题 - 如果我记得您博客文章的第 2 部分使用了另一种不依赖坐标空间的方法。我最终可能会尝试一下【参考方案3】:

PhilipJacob 的回答很棒,但是这个 API 有一个更新,我想我会提到它。

因此,如果您想使用已重命名/更新为 ObservableObjectBindableObject 并且 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) 

【讨论】:

【参考方案4】:

我希望我正确理解了您的问题,即在两个孩子之间共享信息。我采用的方法是在共享相同状态的两个子级中使用@Binding 变量,即使用 $rectFrame 实例化两者。这样,两者将始终具有相同的值,而不是实例化时的值。

在我的例子中,它是一个在一个子节点中更新并在另一个子节点中显示在列表中的数组,因此 SwiftUI 负责对更改做出反应。也许你的情况没那么简单。

【讨论】:

以上是关于如何将数据从子视图传递到父视图到 SwiftUI 中的另一个子视图?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 SwiftUI 视图动态添加到父视图?

将点击从子视图转发到父视图?

将图像从子视图绘制到父视图

如何使用双向数据绑定将数据从子组件传递到父组件?

如何将数据从子组件传递到父组件[Angular]

通过 uinavigationcontroller 将变量从父视图传递到子视图