skip to Main Content

I currently have following custom component that wraps and styles a default TextField from swift ui. I came across an issue where I now need to re-use this where @Binding is no longer just String, but rather, can be a number like int or double, thus underlying TextField needs to change and take in TextField(value: ..., formatter: ...) instead of just text.

I can turn all of the modifiers into a custom modifier and apply it to relevant text fields to apply the styling, but am wondering if there is a solution where I can keep this as a custom component and instead allow TextInputSmall to take in and pass down all possible permutations of TextField arguments, for example

TextInputSmall("", text: $someString) and TextInputSmall("", value: $someNumber, formatter: .number)

struct TextInputSmall: View {
  // Public Variables
  var accentColor = Color._orange
  @Binding var text: String
  
  // Private Variables
  @FocusState private var focused: Bool
  
  // Body
  var body: some View {
    TextField(
      "",
      text: $text
    )
    .font(...)
    .foregroundStyle(...)
    .accentColor(...)
    .focused($focused)
    .submitLabel(.done)
    .frame(...)
    .padding(...)
    .background(...)
  }
}

2

Answers


  1. You can basically "steal" the declarations of TextField and its initialisers and put them into your own code. Add a TextField property in your wrapper, and in each initialiser, initialise that property by creating a TextField using the corresponding initialiser.

    Here is an example for init(_:text:) and init(_:value:format):

    struct CustomTextField<Label: View>: View {
        let textField: TextField<Label>
        
        init(_ title: LocalizedStringKey, text: Binding<String>) where Label == Text {
            textField = TextField(title, text: text)
        }
        
        init<F>(_ title: LocalizedStringKey, value: Binding<F.FormatInput>, format: F)
            where F: ParseableFormatStyle, F.FormatOutput == String, Label == Text {
            textField = TextField(title, value: value, format: format)
        }
        
        var body: some View {
            textField
                .padding(10)
                // ...add your own styling...
        }
    }
    

    That said, this is quite tedious if you want to have all the combinations of TextField.init. IMO, using a ViewModifier like you suggested is the more idiomatic and correct solution.

    Login or Signup to reply.
  2. As already mentioned, the better solution would be to use a ViewModifier.

    But, you need to be clear what the modifier is responsible for and for what not.

    You could think to implement it like:

    struct MyModifier<L: View>: ViewModifier {
        
        let accentColor: Color
        
        func body(content: Content) -> some View {
            content
                .font(.body)
                .foregroundStyle(.blue)
                .accentColor(.red)
                // .focused($focused)
                // .submitLabel(.done)
                // .frame(...)
                // .padding()
                .background(.green)
        }
    }
    

    I commented out those properties, which should not go into this modifier. This has a clear reason. When you look closer, the modifier only mutates "style" properties, but no "layout" properties.

    It also does not set the focus, which is completely orthogonal what a style modifier should do.

    So, when designing a modifier, try to separate the concepts. Don’t mix up style with layout, and don’t handle state properties (like focus) in the modifier.

    Regarding the focus, you should provide a FocusState in a parent view where it has several views having a focus. Putting it into a custom view, which wraps a single TextField makes no sense.

    Once you have the modifier, you don’t need the custom view, just apply the modifier to any view.

    struct ContentView: View {
        @State private var text: String = "Hello, world!"
        
        var body: some View {
            VStack {
                TextField("", text: $text)
                    .modifier(MyModifier(accentColor: .red))
            }
            .padding()
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search