How to assign default modifiers to a SwiftUI view?

We should always remember that SwiftUI Views are value types, therefore you cannot change them after the initialization like you would do on UIKit. One possible way would be to inject it on the initialization. But if you wanna make it adjustable after the initialization (e.g to change the color upon pressing some other button), use the binding mechanism:

Add a @Binding to your MyCustomTextField, and use its value at the foregroundColor modifier.

struct MyCustomTextField: View {

    @Binding var text: String
    @Binding var foregroundColor: Color

    var body: some View {
      HStack {
        Image(systemName: "envelope").foregroundColor(.gray)
        TextField("", text: $text)
          .foregroundColor(foregroundColor)
          .font(.system(.title))
      }
    }
}

and then in the client view you can do as following:

struct MyView: View {
    @State var myText: String = ""

    @State var hasSpecialColor: Bool = false
    
    var foregroundColor: Binding<Color> {
        return Binding<Color>(get: {
            return hasSpecialColor ? Color.red : Color.black
        }, set: { newValue in
            hasSpecialColor = newValue == Color.red
        })
    }
    
    var body: some View {
        VStack {
            Text("Some Header Text")
            
            MyCustomTextField(text: $myText, foregroundColor: foregroundColor)
            
            Button("click to toggle color") {
                hasSpecialColor.toggle()
            }
        }
    }
}

now, everytime you will click the button, it will toggle hasSpecialColor (which is @State), and therefore the view will render and the value for the @Binding foregroundColor will change accordingly.


You can find the Font set by the nearest font modifier in the environment, so you can read it and provide a default like this:

struct MyCustomTextField: View {
  @Binding var text: String

  @Environment(\.font)
  var envFont: Font?

  var body: some View {
    HStack {
      Image(systemName: "envelope").foregroundColor(.gray)
      TextField("", text: $text)
        .foregroundColor(.gray)
        .font(envFont ?? .system(.title))
    }
  }
}

Unfortunately, the foregroundColor modifier doesn't store its setting in a (public) environment property, so you have to go a different route. One way is to provide a modifier directly on your custom view, like this:

struct MyCustomTextField: View {
    @Binding var text: String

    var _myColor: Color = .gray

    @Environment(\.font)
    var envFont: Font?

    func myColor(_ color: Color) -> Self {
        var copy = self
        copy._myColor = color
        return copy
    }

    var body: some View {
        HStack {
            Image(systemName: "envelope").foregroundColor(.gray)
            TextField("", text: $text)
                .foregroundColor(_myColor)
                .font(envFont ?? .system(.title))
        }
    }
}

You can then use the myColor modifier directly on a MyCustomTextField like this:

PlaygroundPage.current.setLiveView(
    MyCustomTextField(text: .constant("hello"))
        .myColor(.red)
        .padding()
        .font(.body)
)

But you cannot use it on any enclosing view or after any non-MyCustomTextField-specific modifier. For example, this will not work:

PlaygroundPage.current.setLiveView(
    MyCustomTextField(text: .constant("hello"))
        .padding()
        .myColor(.red) // Error: Value of type 'some View' has no member 'myColor'
        .font(.body)
)

If you want that to work, then you need to store the custom color in the environment, like this:

struct MyColor: EnvironmentKey {
    static var defaultValue: Color? { nil }
}

extension EnvironmentValues {
    var myColor: Color? {
        get { self[MyColor.self] }
        set { self[MyColor.self] = newValue }
    }
}

extension View {
    func myColor(_ color: Color?) -> some View {
        return self.environment(\.myColor, color)
    }
}

struct MyCustomTextField: View {
    @Binding var text: String

    @Environment(\.myColor)
    var envColor: Color?

    @Environment(\.font)
    var envFont: Font?

    var body: some View {
        HStack {
            Image(systemName: "envelope").foregroundColor(.gray)
            TextField("", text: $text)
                .foregroundColor(envColor ?? .gray)
                .font(envFont ?? .system(.title))
        }
    }
}

And then you can use the myColor modifier on any view, and it will apply to all enclosed subviews:

PlaygroundPage.current.setLiveView(
    MyCustomTextField(text: .constant("hello"))
        .padding()
        .myColor(.red)
        .font(.body)
)