Padding on an Stack whose child views can be empty view

1 week ago 13
ARTICLE AD BOX

Lets say we have an OptionalView that would render a view if a condition is met (we do not know what it is), and otherwise render nothing (resolves body to an empty view).

struct OptionalView: View { private var random: Int { (1...5).randomElement()! } var body: some View { if random == 1 { // Intentional, we cannot check in parent if this condition is true. Text("Hello There") } } }

If we wrap some OptionalView in an Stack View (like VStack or HStack):

VStack(spacing: 10) { OptionalView() OptionalView() OptionalView() } .border(.red)

The stack will not take additional space for the OptionalView whose bodies are resolved to empty view at runtime due to some condition.

So far, so good. But if we need to apply padding to the stack like:

VStack(spacing: 10) { OptionalView() OptionalView() OptionalView() } .padding(.horizontal, 20) .padding(.vertical, 24) .border(.red)

We will find that the padding will still be applied even when no OptionalView is rendered. The white space exists even if the stack is empty. Note: This is true for Stack that has no view at all:

VStack(spacing: 10){ } .padding(.horizontal, 20) .padding(.vertical, 24) .border(.red)

How can I make it so that the padding is only applied when there is at least one view rendered in the stack?

Using Group is no go, due to varying spacing between views in the stack, and the top and bottom padding will need to be consistently applied to first and last view in the stack, which is not possible with dynamically rendering views since any view can be first or last.

Since the conditional of the OptionalView is not predictable, we cannot conditionally apply padding to the stack.


One way I solved this is by using preference key:

struct Paddable: PreferenceKey { static var defaultValue: Bool { false } static func reduce(value: inout Bool, nextValue: () -> Bool) { value = value || nextValue() } } struct PadOnlyWhenPaddable: ViewModifier { @State private var paddable: Bool = false let edges: Edge.Set let padding: CGFloat? func body(content: Content) -> some View { content .onPreferenceChange(Paddable.self) { self.paddable = $0 } .padding(edges, paddable ? padding : 0) } } extension View { func paddingWhenPaddable(_ edges: Edge.Set = .all, _ padding: CGFloat? = nil) -> some View { modifier(PadOnlyWhenPaddable(edges: edges, padding: padding)) } func paddable() -> some View { preference(key: Paddable.self, value: true) } }

Then in the stack, I would apply the paddable modifier to the views that are conditionally rendered:

VStack(spacing: 10) { OptionalView() .paddable() OptionalView() .paddable() OptionalView() .paddable() OptionalView() .paddable() } .paddingWhenPaddable(.horizontal, 20) .paddingWhenPaddable(.vertical, 24) .border(.red)

Since, the preference is ignored if the view is resolved as empty view, the padding will only be applied when there is at least one view rendered in the stack.


Is there any other better way to achieve this without using preference key? Targeting: iOS 15+

Read Entire Article