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

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

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
}
}
}

Related

How to not add to textfield when the input character is already found in the text

How could you get a TextField to only append to the text variable when the input is not already present?
So if you had already typed "abc" and you tried to type 'a' again, it wouldn't do anything
Starting point:
TextField("list of characters", text: $characterSequence)
.onReceive{
}
You can check strings like this and input only a new char.
Note: This code is a case-sensitive input string.
TextField("list of characters", text: $characterSequence)
.onReceive(Just(characterSequence), perform: { char in
print(char)
let oldString = characterSequence.dropLast()
if let last = characterSequence.last, oldString.contains(last) {
characterSequence = String(characterSequence.dropLast())
}
})
For, without a case-sensitive input string.
TextField("list of characters", text: $characterSequence)
.onReceive(Just(characterSequence), perform: { char in
print(char)
let oldString = characterSequence.dropLast()
if let last = characterSequence.last, oldString.uppercased().contains(last.uppercased()) {
characterSequence = String(characterSequence.dropLast())
}
})
Edit
For the disabled characters in the middle, use UITextField with UIViewRepresentable. This one is also handled copy-past to the text field.
Here is solution
struct UniqueTextField: UIViewRepresentable {
private var placeholder: String?
#Binding private var text: String
init(_ placeholder: String? = nil, text: Binding<String>) {
_text = text
self.placeholder = placeholder
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.placeholder = placeholder
textField.text = text
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
/**
// case-sensitive
string.filter({(textField.text ?? "").contains($0)}).isEmpty
*/
//without case-sensitive
string.filter({(textField.text?.uppercased() ?? "").contains($0.uppercased())}).isEmpty
}
}
}
Usage:
UniqueTextField("list of characters", text: $characterSequence)

SwiftUI Clear Textfield FirstResponder

I'm looking to clear my FirstResponder TextField with an independent button that will save the field data and clear the input content so that we can write again. My code is based on the work of :
Matteo Pacini -> https://stackoverflow.com/a/56508132/15763454
The Clear variable should when it goes to True clear the field, it works when I click the button but I have to additionally enter a character in the text field for it to clear. This is logical since the "textFieldDidChangeSelection" function only runs when the textfield is changed
How can I make sure that as soon as my Clear variable changes to True the textfield is automatically deleted?
struct ContentView : View {
#State var text: String = ""
#State var Clear = false
var body: some View {
HStack{
Spacer()
Button(action: set,label: {
Text("Clic")
})
Spacer()
CustomTextField(text: $text,clear: $Clear,isFirstResponder: true)
.frame(width: 300, height: 50)
.background(Color.red)
}
}
func set(){
self.Clear=true
}
}
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
#Binding var clear: Bool
var didBecomeFirstResponder = false
init(text: Binding<String>,clear: Binding<Bool>) {
_text = text
_clear = clear
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
if clear == true{
textField.text?.removeAll()
clear = false
}
}
}
#Binding var text: String
#Binding var clear: Bool
var isFirstResponder = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(text: $text,clear: $clear)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
Just replace
func set() {
self.Clear=true
}
with
func set() {
self.text = ""
}
This immediately clears the text.

How to make text typed in TextField undeletable?

I am Fairly new to programming, after looking around I thought that id take my chances with asking here. I am basically needing for text typed in a TextField to be undeletable, although additional text can be added/typed.
A different approach would be to create a custom keybaord without a delete key, although I couldn't find a good starting place as in research and etc for doing so in SwiftUI.
I have a basic TextField setup with an empty Binding<String>
Looking for pointers of what I should research and or learn.
Thank you.
The idea is the create UITextField class and use UIViewRepresentable to bind with SwiftUI view. By this, you can use all delegate methods and detect backspace. Also, using this you can prevent from cut and delete from tap action.
UndeletableTextField custom class
class UndeletableTextField: UITextField {
// This for prevent to cut and delete
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(UIResponderStandardEditActions.delete(_:)) ||
action == #selector(UIResponderStandardEditActions.cut(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
UIViewRepresentable view
struct UndeletableTextFieldUI: UIViewRepresentable {
#Binding var text: String
var placeholder: String
func makeUIView(context: Context) -> UndeletableTextField {
let textField = UndeletableTextField(frame: .zero)
textField.delegate = context.coordinator
textField.placeholder = placeholder
return textField
}
func updateUIView(_ uiView: UndeletableTextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: UndeletableTextFieldUI
init(parent: UndeletableTextFieldUI) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Here we detect backspace and ignore it.
if let char = string.cString(using: String.Encoding.utf8) {
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Backspace was pressed")
return false
}
}
return true
}
}
}
ContentView
struct ContentView: View {
#State private var text: String = ""
var body: some View {
UndeletableTextFieldUI(text: $text, placeholder: "Type here")
}
}
You will probably want a custom binding for that String. The following is a super basic example -- you'll probably want to cover more edge cases. Note that I've chosen to include the logic in an ObservableObject, but you could do the same in a View struct by changing _textStore to be a #State variable. You'd also want to include logic for initial text, etc.
class ViewModel : ObservableObject {
var _textStore = ""
var textBinding : Binding<String> {
Binding<String>(get: {
return _textStore
}, set: { newValue in
//do something here to compare newValue to what existed before
//note that this solution will allow text to be both prepended and appended to the existing text
if _textStore.contains(newValue) { _textStore = newValue }
})
}
}
...
#ObservedObject var vm = ViewModel()
TextField("", vm.textBinding)

How to initialise #State variables depending on each other in Swiftui?

I need to assign my MultilineTextField view (a wrapped UITextView) to a variable textField in order to be able later to call its method updateTextStyle from a button in ContentView (the method takes the selected text and turns it into bold). The problem is that MultilineTextField rely on the #State var range, therefore not compiling. What are possible workaround for this?
struct ContentView: View {
#State private var range: NSRange?
#State var textField = MultilineTextField(rangeSelected: $range)
var body: some View {
VStack {
textField
Button(action: {
self.textField.updateTextStyle()
}) {
Text("Update text style")
}
}
}
}
In case relevant, MultilineTextField (I tried to remove the unnecessary - hope it's clear)
struct MultilineTextField: UIViewRepresentable {
let textView = UITextView()
#Binding var rangeSelected: NSRange?
#State var attributedNoteText = NSMutableAttributedString(string: "Lorem ipsum")
func makeUIView(context: Context) -> UITextView {
// ...
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = attributedNoteText
}
func updateTextStyle() {
if self.rangeSelected != nil {
// apply attributes (makes the selected text bold)
} else {
print("rangeSelected is nil")
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self, $attributedNoteText)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: MultilineTextField
var text: Binding<NSMutableAttributedString>
init(parent: MultilineTextField, _ text: Binding<NSMutableAttributedString>) {
self.parent = parent
self.text = text
}
func textViewDidChange(_ textView: UITextView) {
let attributedStringCopy = textView.attributedText?.mutableCopy() as! NSMutableAttributedString
parent.textView.attributedText = attributedStringCopy
self.text.wrappedValue = attributedStringCopy
}
func textViewDidChangeSelection(_ textView: UITextView) {
parent.rangeSelected = textView.selectedRange // not sure about this one
}
}
}
(I'm aware there might be some additional errors here - it's my first time working with UIKit in SwiftUI. Thanks for any help)
It should be differently, because view is struct, so your call updateTextStyle() in button action will have no effect, because applied to copy of above textField
Instead the approach should be like following (scratchy)
struct ContentView: View {
#State private var range: NSRange?
// example of style, on place of color your style
#State var color: Color = .black
var body: some View {
VStack {
MultilineTextField(rangeSelected: $range)
.foregroundColor(self.color) // style state dependency
Button(action: {
self.color = .red // specify new style
}) {
Text("Update text style")
}
}
}
}

Pass from a Binding<String> TextField to a Binding<Int> in SwiftUI

I'm developing a SwiftUI app and I want to recreate the effect of passing from a textField to the following one and, to do that, I need to wrap a TextField from UIKit which has the keyboard option "next" and "done".
I've this situation: 1 TextField (SwiftUI style) which accepts only a String and two TextFields (wrapped from UIKit) which accepts only Ints.
My aim is to make the TextField Binding the first responder of the View and connect the key "next" pressed by the user to the first TextField Binding and, thereafter, the second textField Binding.
I've tried to implement the Binding inside the wrapper but I've got some problems because I don't know how to fill the init inside the View with the proper bindings.
I would really appreciate some help!
struct LinkedTextFields: View {
#State var name : String = ""
#State var number_1 : Int = 100
#State var number_2 : Int = 10
#State var focused: [Bool] = [true, false, false]
var body: some View {
NavigationView{
VStack {
VStack {
Form {
TextField("Name", text: $name)
.textFieldStyle(RoundedBorderTextFieldStyle())
HStack {
Text("Number 1:")
Spacer()
TextFieldWrapped(keyboardType: .namePhonePad, returnVal: .next, tag: 0, number: self.$number_1, isfocusAble: self.$focused)
}
HStack {
Text("Number 2:")
Spacer()
TextFieldWrapped(keyboardType: .namePhonePad, returnVal: .done, tag: 1, number: self.$number_2, isfocusAble: self.$focused)
}
}
}
}
.navigationBarTitle("Add product")
}
}
}
struct TextFieldWrapped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
#Binding var number: Int
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.keyboardType = self.keyboardType
textField.returnKeyType = self.returnVal
textField.tag = self.tag
textField.delegate = context.coordinator
textField.autocorrectionType = .no
textField.textAlignment = .center
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
uiView.text = String(number)
}
func makeCoordinator() -> Coordinator {
Coordinator(self, $number)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapped
var number: Binding<Int>
init(_ textField: TextFieldWrapped,_ number: Binding<Int>) {
self.parent = textField
self.number = number
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.number = Int(textField.text ?? "") ?? 0
}
else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.number = Int(textField.text ?? "") ?? 0
}
return true
}
func textFieldDidEndEditing(_ textField: UITextField, reason: UITextField.DidEndEditingReason) {
if reason == .committed {
textField.resignFirstResponder()
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
let text = textField.text as NSString?
let newValue = text?.replacingCharacters(in: range, with: string)
if let number = Int(newValue ?? "0") {
self.number.wrappedValue = number
return true
} else {
if nil == newValue || newValue!.isEmpty {
self.number.wrappedValue = 1
}
return false
}
}
}
}