将 @State 属性的 @Binding 值从容器视图传递到其子视图

Posted

技术标签:

【中文标题】将 @State 属性的 @Binding 值从容器视图传递到其子视图【英文标题】:Passing down @Binding values of @State properties from a container view to its children views 【发布时间】:2021-03-11 10:41:28 【问题描述】:

我创建了一个充当容器视图的视图,它具有多个我希望将其值传递给其兄弟视图的属性。

下面的示例容器视图仅存储填充值,不是一个实际的用例,但这个想法可以转移到那个(我将在最后解释)。

public struct Container<Content: View>: View 

let content: Content

// MARK: Padding
@State var bottom: CGFloat = .zero
@State var leading: CGFloat = .zero
@State var trailing: CGFloat = .zero
@State var top: CGFloat = .zero

// MARK: -
public init(padding: Padding...,
            @ViewBuilder content: () -> Content) 
    for pad in padding 
        switch pad 
            case .bottom(let padding):      bottom = padding
            case .leading(let padding):     leading = padding
            case .trailing(let padding):    trailing = padding
            case .top(let padding):         top = padding
            case .horizontal(let padding):  leading = padding
                                            trailing = padding
            case .vertical(let padding):    bottom = padding
                                            top = padding
        
    
    self.content = content()


// MARK: -
public var body: some View 
    content
        .padding(.bottom, bottom)
        .padding(.leading, leading)
        .padding(.top, top)
        .padding(.trailing, trailing)



extension Container 
    public enum Padding 
        case vertical(_:CGFloat)
        case horizontal(_:CGFloat)
        case leading(_:CGFloat)
        case trailing(_:CGFloat)
        case top(_:CGFloat)
        case bottom(_:CGFloat)
    

实际上,这看起来像:

Container(padding:.vertical(50)) 
    /// children have no idea about Container’s set property values

而且效果很好。但我想要实现的是:

Container(padding:.vertical(50))  padding in
    /// children have access to padding.bottom,top,leading,trailing

我在方法中的参考是 GeometryReader,它提供对 GeometryProxy 的访问以供其兄弟视图依赖。

如顶部所述,虽然我不太可能从传递填充值中找到好处,但我关注的直接用例是创建一个可扩展容器视图,该视图存储诸如 expanded: Bool 之类的属性以及诸如toggleExpanded() 之类的方法附加到其轻击手势处理程序,以更改expanded 的状态。


更新:

pawello2222 的建议对于我分享的示例代码在技术上是令人满意的。然而,由于值的静态性质,事实证明它不能立即转移到我的实际用例中,因为它涉及可以根据用户交互更改的值。学习到教训了!这是展开式视图:

public struct Expandable<Content: View>: View 
    
    let content: Content
    @State public var expanded: Bool = false

    public init(@ViewBuilder content: ((Binding<Bool>)) -> Content) 
        self.content = content(($expanded))
    
    
    // MARK: -
    public var body: some View 
        content
           .onTapGesture(perform: toggle)
    
    
    // MARK: - Methods
    
    func toggle() 
        expanded.toggle()
    

换句话说,我需要子视图能够访问@Binding expanded 属性,因此它不仅能够读取当前的expanded 值,而且还可以将其状态切换回其父级。

上面代码的问题是它告诉我:

Variable 'self.content' used before being initialized

【问题讨论】:

【参考方案1】:

我不确定这是否是您想要的,但您可以尝试以下方法:

public init(padding: Padding...,
            @ViewBuilder content: ([Padding]) -> Content)

    self.content = content(padding)
    for pad in padding 
        // ...
    

并像这样使用它:

Container(padding:.vertical(50))  padding in
    // ...

编辑

您希望在更新后的问题中实现的目标可以通过创建自定义EnvironmentKey 来完成:

extension EnvironmentValues 
    private struct PaddingKey: EnvironmentKey 
        static let defaultValue: Binding<Padding> = .constant(.vertical(0)) // or any other default value
    
    
    var padding: Binding<Padding> 
        get  self[PaddingKey] 
        set  self[PaddingKey] = newValue 
    

struct ContentView: View 
    @State var padding: Padding = .vertical(50)

    var body: some View 
        Container(padding: padding) 
            ChildView()
            // ...
        
        .environment(\.padding, $padding) // inject the binding into the environment
    

struct ChildView: View 
    @Environment(\.padding) var padding  // inject the binding into the environment

    var body: some View 
        if case let .bottom(value) = padding.wrappedValue 
            Text("Bottom")
                .padding(.bottom, value)
        
        // ...
        Button("Change") 
            padding.wrappedValue = .leading(50)
        
    

注意:您可以按照相同的方法在容器中使用.environment(\.padding(而不是Container(padding: padding))。

【讨论】:

非常感谢!这就是我所寻找的几乎(我提供的代码在技术上很好,但不是实际的最终用例)。我已经在我的原始帖子中阐明了新代码的发展。希望大家多多指导。很抱歉第一次没有分享实际的预期用途——我认为它是可以转移的,但不幸的是显然不是。 @Barrrdi 您更新后的问题与原来的问题不同 - 它不再是关于传递 @State 值。但我相应地更新了我的答案。 非常感谢您的跟进,非常感谢您花费的时间和耐心。这种方法对我来说是新的,我相信我会从中受益匪浅。但是,尝试您的确切示例代码,我在 ChildView() 上收到此错误:“Missing argument for parameter 'padding' in call”建议“插入'填充:#>'”作为修复,但我不完全确定需要插入哪些值而不是占位符建议(以及是否有另一种更好/正确的方法)。 是的,我意识到这一点,实际上我正准备将更新的问题分叉为一个单独的问题,但后来我看到你仍然回答了它。像您这样慷慨的人有幸与之互动。 FWIW 如果我将 Binding 更改为 Padding,并且 ChildView 的 padding 属性更改为:@Environment(\.padding) var padding,它确实可以编译。但是我假设 ChildView 对填充的任何更改都不会向上游移动到其父级?

以上是关于将 @State 属性的 @Binding 值从容器视图传递到其子视图的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI:属性装饰器的理解@State,@Binding,@ObservedObject,@Published,@Environment,@EnvironmentObject

SwiftUI:属性装饰器的理解@State,@Binding,@ObservedObject,@Published,@Environment,@EnvironmentObject

@State 的限制

将观察对象的投影值的属性传递给@Binding

如何使 Binding 尊重 DependencyProperty 值强制?

WPF 绑定Binding ElementName Path