0

I’ve created a view that acts as a container view, that has multiple properties whose values I wish to pass down to its sibling views.

The below example container view only stores padding values and isn’t a practical use-case, but the idea is transferrable to one that is (that I’ll explain at the end).

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

In practise, this looks like:

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

And it works fine. But what I’d like to achieve is:

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

My reference in approach is the GeometryReader that provides access to a GeometryProxy for its sibling views to depend on.

As mentioned at the top, although I’m unlikely to find benefit in passing down the padding values, the immediate use-case I’m focused on is the creation of an Expandable container view that stores properties such as expanded: Bool and methods like toggleExpanded() attached to its tap gesture handler that changes the state of expanded.


UPDATE:

pawello2222’s suggestion is technically satisfactory for the example code I shared. However because of the static nature of the values, it turns out it’s not immediately transferrable to my actual use case as it involves a value that can be changed based on user interaction. Lesson learnt! Here is the Expandable view:

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

In other words, I need the children views to have access to a @Binding expanded property, so it’s not only able to read the current expanded value but also has the possibility of toggling its state back up to its parent.

The problem with the above code is it tells me:

Variable 'self.content' used before being initialized
Barrrdi
  • 902
  • 13
  • 33

1 Answers1

1

I'm not sure if that's what you want, but you can try the following:

public init(padding: Padding...,
            @ViewBuilder content: ([Padding]) -> Content)
{
    self.content = content(padding)
    for pad in padding {
        // ...
    }
}

and use it like:

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

EDIT

What you want to achieve in your updated question can be done by creating a custom 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)
        }
    }
}

Note: you can follow the same approach and use the .environment(\.padding in the Container as well (instead of Container(padding: padding)).

pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • Thanks so much! That’s *almost* what I was looking for (technically fine for the code I provided but not the actual final use case). I’ve clarified developments with new code in my original post. Hope you can guide further. Apologies for not sharing the actual intended usage the first time around – I thought it would be transferrable but apparently not unfortunately. – Barrrdi Mar 11 '21 at 11:50
  • @Barrrdi Your updated question is different from your original one - it's no longer about *passing down* `@State` values. But I updated my answer accordingly. – pawello2222 Mar 11 '21 at 18:41
  • Thanks so much for the follow-up, I really appreciate the time and patience you’ve taken. This approach is new to me, and I’m sure I’ll greatly benefit from it. However, trying your exact example code, I’m getting this error on ChildView(): "Missing argument for parameter 'padding' in call" suggesting "Insert 'padding: <#Environment#>'" as the fix, but I’m not entirely sure what the values that need to be inserted instead of the placeholder suggestion is (and whether there’s another better/correct way). – Barrrdi Mar 11 '21 at 19:09
  • Yep, I realise that, and actually I was just about to fork the updated question to be a separate question, but then I saw you answered it nevertheless. Generous people like you are a privilege to interact with. – Barrrdi Mar 11 '21 at 19:10
  • FWIW if I change Binding to just Padding, and ChildView’s padding property to: @Environment(\.padding) var padding, it does compile. But I’m assuming any changes to padding by a ChildView doesn’t move upstream to its parent? – Barrrdi Mar 11 '21 at 19:36
  • @Barrrdi Yes, I just fixed a typo. And added an example for changing the padding variable. – pawello2222 Mar 11 '21 at 19:40
  • Thanks so much! One last general question if you don’t mind @pawello2222 – can there be as many environment values of the key active simultaneously? I’m asking because in my actual use case where I’m passing down the expanded state of a card to children views, there will be multiple cards visible at a time i.e. each will have their own state, as opposed to a single value shared across all. – Barrrdi Mar 11 '21 at 19:45
  • @Barrrdi You can create an array of Bindings as an EnvironmentKey - see this answer https://stackoverflow.com/a/65646180/8697793 – pawello2222 Mar 11 '21 at 19:50