skip to Main Content

I’m trying to make reusable custom views that themselves can be customized later. Ideally I’d be able to define default modifiers like Color, Font, etc that the Views would have without any customization, but allow these to be easily overwritten by additional modifiers used later on.

For example if I had the custom view:

struct MyCustomTextField: View {

  @Binding var text: String

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

The view would have default gray foreground color and the TextField would have title font. But now if I wanted to reuse this view and customize it for my specific use-case, I might want to override these modifiers like so:

struct ContentView: View {
  @State var text = "hello"

  var body: some View {
    MyCustomTextField(text: $text)
      .foregroundColor(.blue)
      .font(.system(.body))
  }
}

But these outside modifiers are not effective as the inner modifiers take precedent.

What’s the best way to make custom views so that I can define default modifiers for their contents but still be able to override those defaults later?

Thanks

2

Answers


  1. 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.

    Login or Signup to reply.
  2. The font modifier stores the Font in the environment for access by the modified views. 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)
    )
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search