GilroyKilroy
GilroyKilroy

Reputation: 894

SwiftUI passing ViewModifier as parameter

I am creating a custom TextField view that consists of multiple adornment views. I want to be able to set up the inner TextField with view modifiers such as keyboard, capitalization, etc. that apply just to that sub-view.

Rather than creating properties for each of these I figured the best way would be to pass in a single optional ViewModifier parameter and use it something like this:

struct MySuperTextField: View {
    var vm: ViewModifier?

    var body: some View {
         TextField(...)
           .modifier( vm ?? EmptyModifier() )
         // ... more views here
    }
}

This doesn't work due to the associatedType in ViewModifier. Alas there is no such thing as AnyViewModifier either (and I could't figure out how to make one that worked).

Anyone manage to do something like this? I couldn't find anything searching the web.

An example would be

struct LastNameModifier: ViewModifier {
   func body(content: Content) -> some View {
       content
          .autocapitalization(.words)
          .textContentType(.familyName)
          .backgroundColor(.green)
          // ... anything else specific to names
   }
}

struct EmailModifier: ViewModifier {
   func body(content: Content) -> some View {
       content
          .keyboardType(.emailAddress)
          .textContentType(.emailAddress)
          .backgroundColor(.yellow)
          // ... anything else specific to emails
   }
}

and then use them with my MySuperTextField like this:

VStack {
    MySuperTextField("Last Name", $lastName, vm: LastNameModifier())

    MySuperTextField("Email", $email, vm: EmailModifier())
}

Upvotes: 8

Views: 8091

Answers (3)

pawello2222
pawello2222

Reputation: 54621

If I understood correctly, you can make your MySuperTextField accept a generic parameter:

struct MySuperTextField<V>: View where V: ViewModifier {
    private let placeholder: String
    @Binding private var text: String
    private let vm: V
    
    init(_ placeholder: String, text: Binding<String>, vm: V) {
        self.placeholder = placeholder
        self._text = text
        self.vm = vm
    }

    var body: some View {
        TextField(placeholder, text: $text)
            .modifier(vm)
    }
}

Then, you can pass some ViewModifier as the parameter:

struct ContentView: View {
    @State private var text: String = "Test"

    var body: some View {
        MySuperTextField("Last Name", text: $text, vm: LastNameModifier())
    }
}

If you need a way to skip the vm parameter when creating MySuperTextField:

MySuperTextField("Last Name", text: $text)

you can create an extension:

extension MySuperTextField where V == EmptyModifier {
    init(_ placeholder: String, text: Binding<String>) {
        self.placeholder = placeholder
        self._text = text
        self.vm = EmptyModifier()
    }
}

Upvotes: 8

David
David

Reputation: 1172

since ViewModifier has an associated type that you cannot use as an instance variable but I propose you another way of use:

MySuperTextField does not have to maintain instances of the ViewModifier type.

struct MySuperTextField: View {
    @State var textValue = ""

    var body: some View {
        TextField("", text: $textValue)
    }
}

For each structure that conforms to the ViewModifier protocol you create an extension:

extension MySuperTextField {
    func lastNameModifier() -> some View {
        modifier(LastNameModifier())
    }

    func emailModifier() -> some View {
        modifier(EmailModifier())
    }
}

and you can use it this way:

struct ContentView : View {
    var body: some View {
        VStack {
            MySuperTextField(textValue: "Last Name")
                .lastNameModifier()
            MySuperTextField(textValue: "Email")
                .emailModifier()
        }
    }
}

Upvotes: 1

Yrb
Yrb

Reputation: 9755

Set the ViewModifier up as a struct and just call it on the view. You just have to be specific as to the type, such as this buttonStyle modifier I used:

struct PurchaseButtonStyle: ButtonStyle {
    let geometry: GeometryProxy
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .frame(minWidth: 0, idealWidth: 300, maxWidth: .infinity)
            .padding(.all, geometry.size.height / 30)
            .foregroundColor(.black)
            .background(.orange)
            .cornerRadius(30)

    }
}

The use is:

Button(...)
    .buttonStyle(PurchaseButtonStyle(geometry: Geometry))

You just have to write it to be the specific modifier. I just used this because it was handy in an app I was working on.

Upvotes: 0

Related Questions