Cannot update value when wrapping SwiftUI binding in another binding - swiftui

I want to make a login code screen. This consists of 4 separate UITextField elements, each accepting one character. What I did is implement a system whereby every time one of the UITextField's changes it will verify if all the values are filled out, and if they are update a boolean binding to tell the parent object that the code is correct.
In order to do this I wrap the #State variables inside a custom binding that does a callback on the setter, like this:
#State private var chars:[String] = ["","","",""]
...
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
and those bindings are passed to the components. Every time my text value changes validateCode is called. This works perfectly.
However now I want to add an extra behavior: If the user types 4 characters and the code is wrong I want to move the first responder back to the first textfield and clear its contents. The first responder part works fine (I also manage that using #State variables, but I do not use a binding wrapper for those), however I can't change the text inside my code. I think it's because my components use that wrapped binding, and not the variable containing the text.
This is what my validateCode looks like:
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.chars = ["","","",""]
}
}
hasFocus does its thing correctly and the cursor is being moved to the first UITextField. The text however remains in the text fields. I tried creating those bindings in the init so I could also use them in my validateCode function but that gives all kinds of compile errors because I am using self inside the getter and the setter.
Any idea how to solve this? Should I work with Observables? I'm just starting out with SwiftUI so it's possible I am missing some tools that I can use for this.
For completeness, here is the code of the entire file:
import SwiftUI
struct CWCodeView: View {
var value:String
#Binding var isValid:Bool
#State private var chars:[String] = ["","","",""]
#State private var hasFocus = [true,false,false,false]
#State private var nothingHasFocus:Bool = false
init(value:String,isValid:Binding<Bool>) {
self.value = value
self._isValid = isValid
}
func validateCode() {
let combinedCode = chars.reduce("") { (result, string) -> String in
return result + string
}
self.isValid = value == combinedCode
if !isValid && combinedCode.count == chars.count {
self.hasFocus = [true,false,false,false]
self.nothingHasFocus = false
self.chars = ["","","",""]
}
}
var body: some View {
var bindings:[Binding<String>] = []
for x in 0..<self.chars.count {
let b = Binding<String>(get: {
return self.chars[x]
}, set: {
self.chars[x] = $0
self.validateCode()
})
bindings.append(b)
}
return GeometryReader { geometry in
ScrollView (.vertical){
VStack{
HStack {
CWNumberField(letter: bindings[0],hasFocus: self.$hasFocus[0], previousHasFocus: self.$nothingHasFocus, nextHasFocus: self.$hasFocus[1])
CWNumberField(letter: bindings[1],hasFocus: self.$hasFocus[1], previousHasFocus: self.$hasFocus[0], nextHasFocus: self.$hasFocus[2])
CWNumberField(letter: bindings[2],hasFocus: self.$hasFocus[2], previousHasFocus: self.$hasFocus[1], nextHasFocus: self.$hasFocus[3])
CWNumberField(letter: bindings[3],hasFocus: self.$hasFocus[3], previousHasFocus: self.$hasFocus[2], nextHasFocus: self.$nothingHasFocus)
}
}
.frame(width: geometry.size.width)
.frame(height: geometry.size.height)
.modifier(AdaptsToSoftwareKeyboard())
}
}
}
}
struct CWCodeView_Previews: PreviewProvider {
static var previews: some View {
CWCodeView(value: "1000", isValid: .constant(false))
}
}
struct CWNumberField : View {
#Binding var letter:String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
var body: some View {
CWSingleCharacterTextField(character:$letter,hasFocus: $hasFocus, previousHasFocus: $previousHasFocus, nextHasFocus: $nextHasFocus)
.frame(width: 46,height:56)
.keyboardType(.numberPad)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.init("codeBorder"), lineWidth: 1)
)
}
}
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField.init()
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}
Thanks!

you just make a little mistake, but i cannot believe you just "started" SwiftUI ;)
1.) just build textfield one time, so i took it as a member variable instead of building always a new one
2.) update the text in updateuiview -> that's it
3.) ...nearly: there is still a focus/update problem...the last of the four textfields won't update correctly ...i assume this is a focus problem....
try this:
struct CWSingleCharacterTextField : UIViewRepresentable {
#Binding var character: String
#Binding var hasFocus:Bool
#Binding var previousHasFocus:Bool
#Binding var nextHasFocus:Bool
let textField = UITextField.init()
func makeUIView(context: Context) -> UITextField {
//textField.isSecureTextEntry = true
textField.keyboardType = .numberPad
textField.delegate = context.coordinator
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 16)
textField.tintColor = .black
textField.text = character
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = character
if hasFocus {
DispatchQueue.main.async {
uiView.becomeFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator : NSObject, UITextFieldDelegate {
var parent:CWSingleCharacterTextField
init(_ parent:CWSingleCharacterTextField) {
self.parent = parent
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let result = (textField.text! as NSString).replacingCharacters(in: range, with: string)
if result.count > 0 {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.nextHasFocus = true
}
} else {
DispatchQueue.main.async{
self.parent.hasFocus = false
self.parent.previousHasFocus = true
}
}
if result.count <= 1 {
parent.character = string
return true
}
return false
}
}
}

Related

How to clear all text in a UIViewRepresentable Text Field with a view modifier

I'm trying to delete all text inside a Text Field when a button to the right of the text field is tapped. When I tap the clear button it behaves as though the text has been cleared. It shows the placeholder text which is set to be shown when the Text Field is cleared. However, the text is still showing 'underneath' the placeholder text. I can delete the typed in text by using the delete key on the keyboard, one character at a time, but clearing all at once should be a quicker option.
I have a UIViewRepresentable for a TextField as below. The reason I'm using a UIViewRepresentable is because I need the keyboard language layout to change languages as required by the user. SwiftUI doesn't currently support this:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
var onCommit: (() -> Bool)
init(language: Binding<String>, text: Binding<String>, onCommit: #escaping() -> Bool = { return true }) {
self._language = language
self._text = text
self.onCommit = onCommit
}
func makeUIView(context: Context) -> WordTextField {
let textField = WordTextField(onCommit: onCommit)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func updateUIView(_ uiView: WordTextField, context: Context) {
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
}
class WordTextField: UITextField, UITextFieldDelegate {
var textDidChange: ((String) -> Void)?
var onCommit: (() -> Bool)
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
init(textDidChange: ( (String) -> Void)? = nil, onCommit: #escaping () -> Bool = { return true }, language: String? = nil) {
self.textDidChange = textDidChange
self.onCommit = onCommit
self.language = language
super.init(frame: .zero)
self.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text ?? "")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return onCommit()
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
I call it as below:
#State private var userAnswer: String = ""
#State private var keyboardLanguage: String = ""
#State private var hintColour: Color = .green
#State private var hintDisabled: Bool = false
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0.allowedCharacters(string: $0)
self.enableHint()
}), onCommit: {
checkAnswer()
return true
})
.showClearButton(userAnswer: $userAnswer, hintDisabled: $hintDisabled, hintColour: $hintColour)
The view modifier .showClearButton does not clear the userAnswer text when tapped. Tapping the keyboard delete button clears userAnswer one character at a time.
The view modifier code is below:
extension View {
func showClearButton(userAnswer: Binding<String>, hintDisabled: Binding<Bool>, hintColour: Binding<Color>) -> some View {
self.modifier(TextFieldClearButton(text: userAnswer, hintDisabled: hintDisabled, hintColour: hintColour))
}
}
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
#Binding var hintDisabled: Bool
#Binding var hintColour: Color
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = ""; hintDisabled = false; hintColour = Color.systemBlue },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.gray))
.imageScale(.large)
.frame(width: 44, height: 44, alignment: .trailing)
}
)
}
}
}
}
You forgot to set the new text on the UITextField which in the case of the clear button is an empty string, fix like this:
func updateUIView(_ uiView: WordTextField, context: Context) {
// set the new text
uiView.text = text
// you also forgot to set the new onCommit
uiView.onCommit = onCommit
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
Also, you could benefit from a Coordinator here, it works like this:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.textDidChange = {
text = $0
}
uiView.onCommit = onCommit
uiView.text = text // maybe could check it is different first
if uiView.language != self.language {
uiView.language = self.language
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: {
let textField = UITextField()
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}()
// your closure properties
// all your delegate methods
}

SwiftUI: stay on same TextField after on commit?

is it possible in SwiftUI to keep the typing cursor on the same Textfield even after the user taps on Return key on keyboard ?
Here is my code:
struct RowView: View {
#Binding var checklistItem: ChecklistItem
#ObservedObject var checklist = Checklist()
#ObservedObject var viewModel: ChecklistViewModel
var body: some View {
HStack {
Button {
self.checklistItem.isChecked.toggle()
self.viewModel.updateChecklist(checklistItem)
} label: {
Circle()
.strokeBorder(checklistItem.isChecked ? checklistSelected : contentPrimary, lineWidth: checklistItem.isChecked ? 6 : 2)
.foregroundColor(backgroundSecondary)
.clipShape(Circle())
.frame(width: 16, height: 16)
}.buttonStyle(BorderlessButtonStyle())
// swiftlint:disable trailing_closure
TextField(
"Add...",
text: $checklistItem.name,
onCommit: {
do {
if !checklistItem.name.isEmpty {
self.viewModel.updateChecklist(checklistItem)
self.checklistItem.name = checklistItem.name
}
}
}
)
// swiftlint:enable trailing_closure
.foregroundColor(checklistItem.isChecked ? contentTertiary : contentPrimary)
Spacer()
}
}
}
So after the user taps on return key on keyboard, TextField() onCommit should be activated normally but the cursor stays in that same textfield so the user can keep typing in new elements.
iOS 15+
You can use #FocusState and, on commit, immediately set the TextField to have focus again.
Example:
struct ContentView: View {
#State private var text = "Hello world!"
#FocusState private var isFieldFocused: Bool
var body: some View {
Form {
TextField("Field", text: $text, onCommit: {
isFieldFocused = true
print("onCommit")
})
.focused($isFieldFocused)
}
}
}
Result:
I was able to achieve this in iOS 14 by creating a custom TextField class:
struct AlwaysActiveTextField: UIViewRepresentable {
let placeholder: String
#Binding var text: String
var focusable: Binding<[Bool]>?
var returnKeyType: UIReturnKeyType = .next
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var isSecureTextEntry: Bool
var tag: Int
var onCommit: () -> Void
func makeUIView(context: Context) -> UITextField {
let activeTextField = UITextField(frame: .zero)
activeTextField.delegate = context.coordinator
activeTextField.placeholder = placeholder
activeTextField.font = .systemFont(ofSize: 14)
activeTextField.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [NSAttributedString.Key.foregroundColor: UIColor(contentSecondary)]
)
activeTextField.returnKeyType = returnKeyType
activeTextField.autocapitalizationType = autocapitalizationType
activeTextField.keyboardType = keyboardType
activeTextField.isSecureTextEntry = isSecureTextEntry
activeTextField.textAlignment = .left
activeTextField.tag = tag
// toolbar
if keyboardType == .numberPad { // keyboard does not have next so add next button in the toolbar
var items = [UIBarButtonItem]()
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
let nextButton = UIBarButtonItem(title: "Next", style: .plain, target: context.coordinator, action: #selector(Coordinator.showNextTextField))
items.append(contentsOf: [spacer, nextButton])
toolbar.setItems(items, animated: false)
activeTextField.inputAccessoryView = toolbar
}
// Editin listener
activeTextField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return activeTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if let focusable = focusable?.wrappedValue {
if focusable[uiView.tag] { // set focused
uiView.becomeFirstResponder()
} else { // remove keyboard
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let activeTextField: AlwaysActiveTextField
var hasEndedViaReturn = false
weak var textField: UITextField?
init(_ activeTextField: AlwaysActiveTextField) {
self.activeTextField = activeTextField
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField = textField
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
var focusable: [Bool] = Array(repeating: false, count: textFieldCount) // remove focus from all text field
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
}
// work around for number pad
#objc
func showNextTextField() {
if let textField = self.textField {
_ = textFieldShouldReturn(textField)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
hasEndedViaReturn = true
guard var focusable = activeTextField.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
activeTextField.onCommit()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if !hasEndedViaReturn {// user dismisses keyboard
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
// reset all text field, so that makeUIView cannot trigger keyboard
activeTextField.focusable?.wrappedValue = Array(repeating: false, count: textFieldCount)
} else {
hasEndedViaReturn = false
}
}
#objc
func textFieldDidChange(_ textField: UITextField) {
activeTextField.text = textField.text ?? ""
}
}
}
and use in in the SwiftUI view by adding this #State variable:
#State var fieldFocus: [Bool] = [false]
and add the Textfield code it self anywhere waiting the view body:
AlwaysActiveTextField(
placeholder: "Add...",
text: $newItemName,
focusable: $fieldFocus,
returnKeyType: .next,
isSecureTextEntry: false,
tag: 0,
onCommit: {
print("any action you want on commit")
}
)

Custom UITextField wrapped in UIViewRepresentable interfering with ObservableObject in NavigationView

Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}

How can you move the cursor to the end in a SwiftUI TextField?

I am using a SwiftUI TextField with a Binding String to change the user's input into a phone format. Upon typing, the formatting is happening, but the cursor isn't moved to the end of the textfield, it remains on the position it was when it was entered. For example, if I enter 1, the value of the texfield (after formatting) will be (1, but the cursor stays after the first character, instead of at the end of the line.
Is there a way to move the textfield's cursor to the end of the line?
Here is the sample code:
import SwiftUI
import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextField("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You might have to use UITextField instead of TextField. UITextField allows setting custom cursor position. To position the cursor at the end of the text you can use textField.endOfDocument to set UITextField.selectedTextRange when the text content is updated.
#objc func textFieldDidChange(_ textField: UITextField) {
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
The following SwiftUI code snippet shows a sample implementation.
import SwiftUI
import UIKit
//import AnyFormatKit
struct ContentView: View {
#State var phoneNumber = ""
let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
var body: some View {
let phoneNumberProxy = Binding<String>(
get: {
return (self.phoneFormatter.format(self.phoneNumber) ?? "")
},
set: {
self.phoneNumber = self.phoneFormatter.unformat($0) ?? ""
}
)
return TextFieldContainer("Phone Number", text: phoneNumberProxy)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
/************************************************/
struct TextFieldContainer: UIViewRepresentable {
private var placeholder : String
private var text : Binding<String>
init(_ placeholder:String, text:Binding<String>) {
self.placeholder = placeholder
self.text = text
}
func makeCoordinator() -> TextFieldContainer.Coordinator {
Coordinator(self)
}
func makeUIView(context: UIViewRepresentableContext<TextFieldContainer>) -> UITextField {
let innertTextField = UITextField(frame: .zero)
innertTextField.placeholder = placeholder
innertTextField.text = text.wrappedValue
innertTextField.delegate = context.coordinator
context.coordinator.setup(innertTextField)
return innertTextField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldContainer
init(_ textFieldContainer: TextFieldContainer) {
self.parent = textFieldContainer
}
func setup(_ textField:UITextField) {
textField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged)
}
#objc func textFieldDidChange(_ textField: UITextField) {
self.parent.text.wrappedValue = textField.text ?? ""
let newPosition = textField.endOfDocument
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
}
}
Unfortunately I can't comment on ddelver's excellent answer, but I just wanted to add that for me, this did not work when I changed the bound string.
My use case is that I had a custom text field component used to edit the selected item from a list, so as you change selected item, the bound string changes. This meant that TextFieldContainer's init method was being called whenever the binding changed, but parent inside the Coordinator still referred to the initial parent.
I'm new to Swift so there may be a better fix for this, but I fixed it by adding a method to the Coordinator:
func updateParent(_ parent : TextFieldContainer) {
self.parent = parent
}
and then calling this from func updateUIView like:
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<TextFieldContainer>) {
uiView.text = self.text.wrappedValue
context.coordinator.updateParent(self)
}
You can do something like this:
final class ContentViewModel: ObservableObject {
private let phoneFormatter = DefaultTextFormatter(textPattern: "(###) ###-####")
private var realPhoneNumber = ""
#Published var formattedPhoneNumber = "" {
didSet {
let formattedText = phoneFormatter.format(formattedPhoneNumber) ?? ""
// Need this check to avoid infinite loop
if formattedPhoneNumber != formattedText {
let realText = phoneFormatter.unformat(formattedPhoneNumber) ?? ""
formattedPhoneNumber = formattedText
realPhoneNumber = realText
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ContentViewModel()
var body: some View {
return TextField("Phone Number", text: $viewModel.formattedPhoneNumber)
}
}
The idea here is that when you manually set (assign) the text binding, the cursor of the textField moves to the end of the text.

Cannot assign to property: '$text' is immutable

I wanted to make a custom textfield in SwiftUI to can handle first responder but I had this error in the code and struct is immutable I don't know what should I do?
struct CustomTextField: UIViewRepresentable {
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
var didBecomeFirstResponder = false
init(txt: Binding<String>) {
self.$text = txt
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
#Binding var text: String
var isFirstResponder: Bool = false
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.delegate = context.coordinator
return textField
}
func makeCoordinator() -> CustomTextField.Coordinator {
return Coordinator(txt: $text)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
}
In beta 4, the implementation of property wrappers changed.
Until beta 3, this was valid:
self.$text = txt
In beta 4, it changed to:
self._text = txt
Check for the difference in implementation, in this other question I posted:
https://stackoverflow.com/a/57088052/7786555
And for more details:
https://stackoverflow.com/a/56975728/7786555