How to apply a SwiftUI view modifier to a container's subview?

I'm trying to define a SwiftUI view as a reusable component in my app. Here is how it is defined:

struct CustomContainer: View {
  
  var body: some View {
    ZStack {
      RoundedRectangle(cornerRadius: 8, style: .circular)
        .stroke(Color.gray, lineWidth: 1.2)

      // foreground here ...
    }
  }

}

I would like for users of my code to be able to apply the view modifier stroke(_:lineWidth:) on CustomContainer, but I would like the CustomContainer to internally apply that modifier on the rounded rectangle inside its body. How would I do that?

This is my desired use of CustomContainer:

struct FormView: View {
  
  var body: some View {
    CustomContainer()
      .stroke(Color.orange, lineWidth: 2)
  }


}
Answered by MQRS in 743660022

I was able to do this using environment variables. I was initially confused because I found a StackOverflow answer that recommended the use of PreferenceKey, but the Apple Docs say that preference keys are for propagating values from a child view to its parent, while what I want to do is for a parent view to propagate a value down to a child, which is what environment values are for.

I added the following to my custom container:

struct CustomContainer: View {
  
  // ...

  // Define a new environment key.
  struct BorderStyle: EnvironmentKey {
    
    let color: Color
    let width: CGFloat
    
    static let defaultValue = BorderStyle(color: Color.gray, width: 1.2)
    
  }
  
  // Convenience view modifier for supplying values to the environment key.
  func border(_ color: Color, width: CGFloat = 1.2) -> some View {
    return environment(\.containerBorderStyle, BorderStyle(color: color, width: width))
  }

  // Capture the environment key in a property of the parent view.
  @Environment(\.containerBorderStyle) private var borderStyle: BorderStyle

  var body: some View {
    ZStack {
      RoundedRectangle(cornerRadius: 8, style: .circular)
        .stroke(borderStyle.color, lineWidth: borderStyle.width) // use the environment key here

      // foreground here ...
    }
  }
  
}

And then I declared the custom environment key to an EnvironmentValues extension:

extension EnvironmentValues {
  
  var containerBorderStyle: CustomContainer.BorderStyle {
    get { self[CustomContainer.BorderStyle.self] }
    set { self[CustomContainer.BorderStyle.self] = newValue }
  }
  
}
Accepted Answer

I was able to do this using environment variables. I was initially confused because I found a StackOverflow answer that recommended the use of PreferenceKey, but the Apple Docs say that preference keys are for propagating values from a child view to its parent, while what I want to do is for a parent view to propagate a value down to a child, which is what environment values are for.

I added the following to my custom container:

struct CustomContainer: View {
  
  // ...

  // Define a new environment key.
  struct BorderStyle: EnvironmentKey {
    
    let color: Color
    let width: CGFloat
    
    static let defaultValue = BorderStyle(color: Color.gray, width: 1.2)
    
  }
  
  // Convenience view modifier for supplying values to the environment key.
  func border(_ color: Color, width: CGFloat = 1.2) -> some View {
    return environment(\.containerBorderStyle, BorderStyle(color: color, width: width))
  }

  // Capture the environment key in a property of the parent view.
  @Environment(\.containerBorderStyle) private var borderStyle: BorderStyle

  var body: some View {
    ZStack {
      RoundedRectangle(cornerRadius: 8, style: .circular)
        .stroke(borderStyle.color, lineWidth: borderStyle.width) // use the environment key here

      // foreground here ...
    }
  }
  
}

And then I declared the custom environment key to an EnvironmentValues extension:

extension EnvironmentValues {
  
  var containerBorderStyle: CustomContainer.BorderStyle {
    get { self[CustomContainer.BorderStyle.self] }
    set { self[CustomContainer.BorderStyle.self] = newValue }
  }
  
}

that's a great idea for global theme.

what are your thoughts about a CustomContainer { @Binding var border: BorderStyle that you set via CustomContainer(border: myBorderStyle)?

or perhaps a custom ViewModifier and it's implementation pattern?

How to apply a SwiftUI view modifier to a container's subview?
 
 
Q