Making a custom SwiftUI View adapt to built-in modifiers

Viewed 2224

I am endeavouring to write my own BetterTextField view for SwiftUI, since the built-in TextField is lacking in several areas. Namely, I want to support delayed binding (updating the bound value only on locusing focus, instead of forcing redraws after every keypress), programmatic focusing/responder control, and a few other features of UIKit's UITextField that SwiftUI lacks.

So I've created a custom UIViewRepresentable with a coordinator as the UITextFieldDelegate and that's working fine. However, for parity with other views, I'd really like to have my custom text field adapt to certain existing SwiftUI modifiers.

For example:

// Here's my content view
struct ContentView: View {
    var body: some View {
        BetterTextField("Username", text: $username)
            // I want to adapt the view to this modifier
            .textFieldStyle(RoundedBorderTextFieldStyle())
    }
}

// Here's my (simplified) custom text field view
struct BetterTextField: UIViewRepresentable {
    var title: String
    @Binding var text: String

    init(_ title: String, text: Binding<String>) {
        self.title = title
        self._text = text
    }

    func makeUIView(context: Context) -> UITextField {
        let textField = UITextField()
        textField.placeholder = title
        return textField
    }

    func updateUIView(_ view: UITextField, context: Context) {
        view.text = text

        // How can I check for the .textFieldStyle() modifier here and set the corresponding UIKit style accordingly?
        view.borderStyle = .roundedRect
    }
}

As the comment says, how can I adapt the borderStyle property of my UITextField to match the View modifier?

And more generally, how does one check for the presence of modifiers and return the appropriately-styled custom view (such as .bold() translating to attributed text, perhaps)?

2 Answers

View modifiers are just functions that just return again some View, so you can implement support for any modifier, conforming to any protocol you decide appropriate to your custom type. How your control would behave on each implemented modifier is up to you.

Below is a simple demo support for textFieldStyle modifier that makes your ContentView render BetterTextField as intended depending on added round rect style modifier or removed.

struct BetterTextField: UIViewRepresentable {
    var title: String
    @Binding var text: String

    private let textField = UITextField()

    init(_ title: String, text: Binding<String>) {
        self.title = title
        self._text = text
    }

    func makeUIView(context: Context) -> UITextField {
        textField.placeholder = title
        return textField
    }

    func updateUIView(_ view: UITextField, context: Context) {
        view.text = text
    }
}

extension BetterTextField {
    func textFieldStyle<S>(_ style: S) -> some View where S : TextFieldStyle {
        if style is RoundedBorderTextFieldStyle {
            self.textField.borderStyle = .roundedRect
        }
        return self
    }
}
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {

    /// Sets the style for `TextField` within the environment of `self`.
    public func textFieldStyle<S>(_ style: S) -> some View where S : TextFieldStyle

}

see the note

Sets the style for TextField within the environment of self

UIViewRepresentable inherits from View but doesn't have any TextField within 'self'

.bold, .italic ... are modifiers for Font, not for generic View. Let say

Image("image001").italic()

doesn't work as well.

For debouncing see Debounced Property Wrapper

For 'delayed' binding see

/// Creates an instance with a `Text` label generated from a localized title
    /// string.
    ///
    /// - Parameters:
    ///     - titleKey: The key for the localized title of `self`, describing
    ///       its purpose.
    ///     - text: The text to be displayed and edited.
    ///     - onEditingChanged: An `Action` that will be called when the user
    ///     begins editing `text` and after the user finishes editing `text`,
    ///     passing a `Bool` indicating whether `self` is currently being edited
    ///     or not.
    ///     - onCommit: The action to perform when the user performs an action
    ///     (usually the return key) while the `TextField` has focus.
    public init(_ titleKey: LocalizedStringKey, text: Binding<String>, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {})

EXAMPLE of 'delayed' binding

import SwiftUI
struct MyTextField<S>: View  where S: StringProtocol {
    let label: S
    @State private var __text = ""
    @Binding var text: String
    var body: some View {
        TextField(label, text: $__text, onEditingChanged: { (e) in

        }) {
            self.text = self.__text
        }
    }
}

struct ContentView: View {
    @State var text = " "
    var body: some View {
        VStack {
            MyTextField(label: "label", text: $text).textFieldStyle(RoundedBorderTextFieldStyle())
            Text(text)
        }.padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

enter image description here

If you need different font and .bold, use

MyTextField(label: "label", text: $text).textFieldStyle(RoundedBorderTextFieldStyle()).font(Font.title.bold())

or

MyTextField(label: "label", text: $text).font(Font.title.bold()).textFieldStyle(RoundedBorderTextFieldStyle())
Related