I noticed that the Binding is not working as expected when we use UITextfield as subViews of a SwiftUI List with the help of UIViewRepresentable protocol.
i have implemented like a dynamic form using List View and the Custom TextField Views, when i was trying to update the value of one field from the list other than first, previously updated elements data getting removed when tapping on done button of the accessory view of textField View.
I have added the code snippets below related to Custom TextField, List Row and for ListView body
TextField View
struct TextFieldView: UIViewRepresentable {
// MARK: - Internal Properties
private let inputTextField = UITextField()
public var placeholder: String
public var keyboardType: UIKeyboardType
private let actionsHandler = InputAccessoryViewActionsHandler()
@Binding public var value: String?
func makeUIView(context: Context) -> UITextField {
inputTextField.placeholder = placeholder
inputTextField.textColor = AppTheme.primaryText.color
inputTextField.font = Font.Roboto.regular.of(size: 16)
inputTextField.keyboardType = keyboardType
inputTextField.delegate = context.coordinator
inputTextField.accessibilityIdentifier = "specValueTextField"
inputTextField.isAccessibilityElement = true
if keyboardType == .numberPad {
configureAccessoryView()
}
return inputTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if let value = self.value {
uiView.text = value
}
}
func makeCoordinator() -> Coordinator {
Coordinator($value)
}
func configureAccessoryView() {
// Accessory View
let toolbar = UIToolbar()
toolbar.sizeToFit()
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
target: nil,
action: nil)
let doneButton = UIBarButtonItem(title: "Done".localized,
style: .plain,
target: self.actionsHandler,
action: #selector(self.actionsHandler.doneButtonAction))
doneButton.tintColor = AppTheme.primary.color
doneButton.accessibilityIdentifier = "Done"
toolbar.setItems([flexibleSpace, doneButton], animated: true)
self.actionsHandler.doneButtonTapped = {
self.value = self.inputTextField.text
self.inputTextField.resignFirstResponder()
}
self.inputTextField.inputAccessoryView = toolbar
}
class InputAccessoryViewActionsHandler {
public var doneButtonTapped: (() -> Void)?
@objc func doneButtonAction() {
self.doneButtonTapped?()
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String?>
init(_ text: Binding<String?>) {
self.text = text
}
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)
self.text.wrappedValue = proposedValue
}
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}
List Row Element view
struct SpecificationRow: View {
@Binding var specification: ClassificationSpecItem
// MARK: - Body
var body: some View {
VStack(alignment: .leading) {
TextFieldView(placeholder: "Enter value",
keyboardType: keyboardType,
value: $specification.specValue)
}.padding(EdgeInsets(top: 12, leading: 0, bottom: 5, trailing: 0))
}
}
and the list body is like below
var body: some View {
VStack {
List($viewModel.specifications) { $spec in
SpecificationRow(specification: $spec)
}
}
the viewModel was confirmed to ObservableObject and the specifications property was a @Published property
Am I missing something? Or is this just a bug in SwiftUI? Any help would be greatly appreciated! Thanks!
CodePudding user response:
You are creating the view object in the wrong place. Instead of:
private let inputTextField = UITextField()
do:
private let inputTextField: UITextField?
func makeUIView(context: Context) -> UITextField {
inputTextField = UITextField()
Or perhaps you don't need to hang on to the reference?
Also you can't create objects like this in SwiftUI:
private let actionsHandler = InputAccessoryViewActionsHandler()
You could instead use your coordinator object for the action handling.
SwiftUI View structs should only init value types. If instantiating objects it needs to be wrapped in @StateObject
which associates the lifetime with the View
appearing/dissapearing or can be a global or a singleton if associating with lifetime of the app.
Also in updateUIView
you need to update the text field properties from your properties.
CodePudding user response:
You are updating your textfield value in updateUIView
method instead of makeUIView
of TextFieldView
class.
Just replace your code with the below one
func makeUIView(context: Context) -> UITextField {
//Assign your value to UITextField here
inputTextField.text = self.value
inputTextField.placeholder = placeholder
inputTextField.textColor = AppTheme.primaryText.color
inputTextField.font = Font.Roboto.regular.of(size: 16)
inputTextField.keyboardType = keyboardType
inputTextField.delegate = context.coordinator
inputTextField.accessibilityIdentifier = "specValueTextField"
inputTextField.isAccessibilityElement = true
if keyboardType == .numberPad {
configureAccessoryView()
}
return inputTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
//Comment below code
/* if let value = self.value {
uiView.text = value
}*/
}