iOS 18 Subviews and Environment values

Hello 👋

I played with the iOS 18 Group(subviews:) APIs these days and I guess I'm missing a point. Environment values seems to not being passed to subviews when set within the Group(subviews:) API.

See the following code:

Is it intended to be that way ? How to propagate different values to different subviews in this case ?

I heard of ContainerValues API but it seems to be a way to provide value at root level to access it during subview iteration. What I'm looking for is "insert"/"propagate" values to each subview during subview iteration.

PS: This works but I have lost subview context this way (I'm out of the group).

Thanks in advance for anyone answering this!

Ok my bad this is intended.

"Subviews are proxies to the resolved view they represent, meaning that modifiers applied to the original view will be applied before modifiers applied to the subview, and the view is resolved using the environment of its container, not the environment of the its subview proxy."

from the documentation: https://developer.apple.com/documentation/swiftui/subview

But this question remains true to be answered: "What I'm looking for is "insert"/"propagate" values to each subview during subview iteration"

I guess this is one way to do it.

TLDR;

  1. Create a resolver view to wrap your content views.
  2. Use an environment closure to pullback values from it.
  3. Re-distribute these values using the containerValue API to propagate value to subviews.
  4. Use .onChange(of: subview.containerValues.foo)API on subview to be notified of changes.

Let's say we want to design an API like that:

// MARK: Client

Stepper {
    StepView {
        Text("Becoming spartan")
    } content: {
        FooView()
    }

    StepView {
        Text("Dying in glory")
    } content: {
        BarView()
    }
}

where FooView, BarView could access to a complete closure through environment to notify it finishes (same way as @Environment(\.dismiss)):

struct FooView: View {
    @Environment(\.isComplete) private var isComplete

    var body: some View {
        VStack {
            Text("This is the part where you become a spartan")
            HStack(alignment: .bottom) {
                Rectangle()
                    .fill(Color.blue.gradient)
                    .frame(width: 50, height: 25)
                Rectangle()
                    .fill(Color.cyan.gradient)
                    .frame(width: 50, height: 10)
                Rectangle()
                    .fill(Color.pink.gradient)
                    .frame(width: 50, height: 30)
            }
            Button {
                isComplete()
            } label: {
                Text("This is Sparta")
            }
        }
    }
}

Client could add as many step as he wants adding more and more views to the Stepper

// MARK: Library

struct Stepper<Content: View>: View {
    @State private var steps = [StepState()]
    @ViewBuilder let content: Content

    var body: some View {
        VStack(spacing: 30) {
            Group(subviews: content) { subviews in
                ForEach(Array(zip(steps.indices, subviews)), id: \.0) { index, subview in
                    subview
                        .onChange(of: subview.containerValues.isComplete) { oldValue, newValue in
                            $steps[index].wrappedValue.isComplete = newValue
                        }
                }
                .onAppear {
                    self.steps = Array(repeating: StepState(), count: subviews.count)
                }
            }
        }
    }
}

struct StepView<Title: View, Info: View>: View {
    @ViewBuilder var titleContent: Title
    @ViewBuilder var content: () -> Info

    @State
    private var isComplete = false
    @State
    private var isExpanded = false

    var body: some View {
        VStack {
            Button {
                withAnimation {
                    isExpanded.toggle()
                }
            } label: {
                HStack {
                    titleContent
                    Spacer()
                    if isComplete {
                        Image(systemName: "checkmark.circle.fill")
                            .resizable()
                            .frame(width: 24, height: 24)
                            .foregroundStyle(.green)
                    }
                    Image(systemName: "chevron.right")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 24, height: 24)
                        .rotationEffect(isExpanded && !isComplete ? .degrees(-90) : .degrees(90))
                }
            }
            .disabled(isComplete)
            if isExpanded, !isComplete {
                content()
                    .environment(\.isComplete, {
                        withAnimation {
                            isComplete = true
                        }
                    })
                    .transition(
                        .asymmetric(
                            insertion: .opacity.animation(.linear.delay(0.33)),
                            removal: .identity
                        )
                    )
            }
        }
        .padding()
        .background(
            RoundedRectangle(cornerRadius: 12, style: .continuous)
                .fill(.white)
                .shadow(radius: 5)
        )
        .containerValue(\.isComplete, isComplete)
    }
}

struct StepState: Equatable {
    var isComplete = false
}

extension EnvironmentValues {
    @Entry var isComplete: () -> Void = { print("Hello World!") }
}

extension ContainerValues {
    @Entry var isComplete: Bool = false
}

Creating custom container views sample code and WWDC24 session 10146: Demystify SwiftUI containers are great to learn about SwiftUI container views if you haven't already reviewed those.

iOS 18 Subviews and Environment values
 
 
Q