2

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)?

Extragorey
  • 1,654
  • 16
  • 30

2 Answers2

3

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
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • yes, but what to do, if RoundedBorderTextFieldStyle() define more than just rounded rectangle around? Every time Apple change it, you have to rewrite your code. It is better use custom modifiers (different naming) for this and without TextFieldStyle parameter! – user3441734 Mar 06 '20 at 07:03
  • Just curious, why do you write the `func textFieldStyle` as an extension? As far as I can tell it would be functionally identical including it in the original struct. – Extragorey Mar 08 '20 at 23:52
  • 1
    @Extragorey, it is just a interface design styling, best-practices if you want, to separate different functional areas by extensions, Apple uses it widely, and, if I'm not mistaken, recommended at some WWDC session. If technically, then yes, you're right. – Asperi Mar 09 '20 at 04:30
  • 1
    This answer worked for me, but it's important that `.textFieldStyle` be the first modifier after the `BetterTextField()` declaration in the view, otherwise Apple's implementation is used instead of this one (I also changed the return type from `some View` to `BetterTextField` to allow chaining of other custom modifiers). – Extragorey Mar 10 '20 at 01:19
  • @Extragorey To not have to rely on the order of your custom modifiers you could make use of `EnvironmentValues` which can be access via the context (`UIViewRepresentable.Context.environment`). That way you can add View extension methods that use the `.environment()` modifier with your custom environment keys (`EnvironmentKey`). Whenever that environment value changes `updateUIView` is called. – bcause Jun 11 '20 at 04:56
0
@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())
user3441734
  • 16,722
  • 2
  • 40
  • 59