emmics
emmics

Reputation: 1063

Is it possible to add kerning to a TextField in SwiftUI?

To match a Styleguide I have to add kerning to a textfield, both placeholder and value itself.

With UIKit I was able to do so with:

    class MyTextField: UITextField {
    override func awakeFromNib() {
        super.awakeFromNib()

        // ...

        self.attributedPlaceholder = NSAttributedString(string: self.placeholder ?? "", attributes: [
            //...
            NSAttributedString.Key.kern: 0.3
        ])

        self.attributedText = NSAttributedString(string: "", attributes: [
            // ...
            NSAttributedString.Key.kern: 0.3
        ])
    }
}

In SwiftUI, I could figure out that I could apply a kerning effect to a Text element like so:

    Text("My label with kerning")
       .kerning(0.7)

Unfortunately, I could not find a way to apply a kerning style to neither a TextField's value nor placeholder. Any ideas on this one? Thanks in advance

Upvotes: 6

Views: 3320

Answers (1)

Andrew
Andrew

Reputation: 28539

There is a simple tutorial on HackingwithSwift that shows how to implement a UITextView. It can easily be adapted for UITextField.

Here is a quick example showing how to use UIViewRepresentable for you UITextField. Setting the kerning on both the text and the placeholder.

struct ContentView: View {

    @State var text = ""

    var body: some View {
        MyTextField(text: $text, placeholder: "Placeholder")
    }

}

struct MyTextField: UIViewRepresentable {
    @Binding var text: String
    var placeholder: String

    func makeUIView(context: Context) -> UITextField {
        return UITextField()
    }

    func updateUIView(_ uiView: UITextField, context: Context) {
        uiView.attributedPlaceholder = NSAttributedString(string: self.placeholder, attributes: [
            NSAttributedString.Key.kern: 0.3
        ])
        uiView.attributedText = NSAttributedString(string: self.text, attributes: [
            NSAttributedString.Key.kern: 0.3
        ])
    }
}

Update

The above doesn't work for setting the kerning on the attributedText. Borrowing from the fantastic work done by Costantino Pistagna in his medium article we need to do a little more work.

Firstly we need to create a wrapped version of the UITextField that allows us access to the delegate methods.

class WrappableTextField: UITextField, UITextFieldDelegate {
    var textFieldChangedHandler: ((String)->Void)?
    var onCommitHandler: (()->Void)?

    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        if let nextField = textField.superview?.superview?.viewWithTag(textField.tag + 1) as? UITextField {
            nextField.becomeFirstResponder()
        } else {
            textField.resignFirstResponder()
        }
        return false
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let currentValue = textField.text as NSString? {
            let proposedValue = currentValue.replacingCharacters(in: range, with: string)
            print(proposedValue)
            self.attributedText = NSAttributedString(string: currentValue as String, attributes: [
                NSAttributedString.Key.kern: 10
            ])
            textFieldChangedHandler?(proposedValue as String)
        }
        return true
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        onCommitHandler?()
    }
}

As the shouldChangeCharactersIn delegate method will get called every time the text changes, we should use that to update the attributedText value. I tried using first the proposedVale but it would double up the characters, it works as expected if we use the currentValue

Now we can use the WrappedTextField in the UIViewRepresentable.

struct SATextField: UIViewRepresentable {
    private let tmpView = WrappableTextField()

    //var exposed to SwiftUI object init
    var tag:Int = 0
    var placeholder:String?
    var changeHandler:((String)->Void)?
    var onCommitHandler:(()->Void)?

    func makeUIView(context: UIViewRepresentableContext<SATextField>) -> WrappableTextField {
        tmpView.tag = tag
        tmpView.delegate = tmpView
        tmpView.placeholder = placeholder
        tmpView.attributedPlaceholder = NSAttributedString(string: self.placeholder ?? "", attributes: [
            NSAttributedString.Key.kern: 10
        ])
        tmpView.onCommitHandler = onCommitHandler
        tmpView.textFieldChangedHandler = changeHandler
        return tmpView
    }

    func updateUIView(_ uiView: WrappableTextField, context: UIViewRepresentableContext<SATextField>) {
        uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
    }
}

We set the attributed text for the placeholder in the makeUIView. The placeholder text is not being updated so we don't need to worry about changing that.

And here is how we use it:

struct ContentView: View {

    @State var text = ""

    var body: some View {
        SATextField(tag: 0, placeholder: "Placeholder", changeHandler: { (newText) in
            self.text = newText
        }) {
            // do something when the editing of this textfield ends
        }
    }
}

Upvotes: 5

Related Questions