Search code examples
iosswiftswiftui

How to create custom component that takes in (and passes) down all possible TextField arguments?


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(...)
  }
}

Solution

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