What is the correct way to pass a state variable to my custom text field? I was hoping to avoid other approaches/observables. Shouldn't this work?
I have recreated the problem below in an example project.
import SwiftUI
struct ParentView: View {
@State var text: String = "initial"
var body: some View {
VStack {
ChildView(text: $text)
Text(self.text)
}
}
}
struct ChildView: View {
@Binding var text: String
var body: some View {
MyTextField(text: $text).frame(width: 300, height: 40, alignment: .center)
}
}
struct MyTextField: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = UITextField.BorderStyle.roundedRect
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
}
To conform to UIViewRepresentable, you must implement four main lifecycle functions: makeUIView (context:): Create and return an instance of your UIViewType here. updateUIView (_:context:): This is immediately called once after the call to makeUIView (context:), then called whenever any state changes.
Since SwiftUI is all about modularity and componentization, there is no concept of a view controller. UIViewRepresentable uses the concept of a coordinator to port views reliant on the delegate pattern. Learn more about delegation in Swift here.
The makeUIView allows us to set up our representable view ( UIActivityIndicatorView, in our case). It’ll be called only once during the SwiftUI view’s lifecycle. The updateUIView gets triggered whenever its enclosing SwiftUI view changes its state.
UIViewRepresentable uses the concept of a coordinator to port views reliant on the delegate pattern. Learn more about delegation in Swift here. A search bar is a good example of a complex UIKit view.
Create a @Binding
property in your CustomTextField
like:
struct CustomTextField: UIViewRepresentable {
@Binding var text: String
}
Initialize your @Binding
property in init()
like:
init(text: Binding<String>) {
self._text = text
}
Pass the text
property to UITextField
like:
func makeUIView(context: Context) -> UITextField {
// Customise the TextField as you wish
textField.text = text
return textField
}
Update the text
of UITextField
like:
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = self.text
}
Update the @Binding
property with user entered text like:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let value = textField.text as NSString? {
let proposedValue = value.replacingCharacters(in: range, with: string)
parent.text = proposedValue as String // Here is updating
}
return true
}
Your ContentView
should look like:
struct ContentView: View {
@State var text: String = ""
var body: some View {
CustomTextField(text: $text)
}
}
Ok now if you want full code of the CustomTextField
then see this answer.
It seems that the binding text (@Binding var text: String
) won't change while the text of TextField changes even after uiView.text = text
, so you need manually update the binding text by using a closure to fetch the context contains the binding text, the object of TextField and the code statement of value equalization, and using Delegate to automatically call the closure. However, the Delegate of TextField (class UITextFieldDelegate) don't have an efficient method that correctly reflects the change of text (func textField is always called later than the change), so using TextView rather than TextField may be better if using Delegate (class TextViewDelegate), because its Delegate has the method textViewDidChange which will be called certainly while changing text of TextView. The method addTarget of TextField or TextView won't work because Obj-C functions (the closure) cannot use in swift struct.
I tried code below:
(Testing Environment: Xcode 11.4.1, Swift 5.2.2)
//Add new:
var bindingUpdate:()->Void={} //the definition the closure
//Add new:
class TextViewDelegate:UIViewController,UITextViewDelegate{
func textViewDidChange(_ textView: UITextView) {
bindingUpdate() //call the closure while text changed
}
}
//Modify:
struct MyTextView: UIViewRepresentable {
@Binding var text: String
var delegate=TextViewDelegate()
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.font=UIFont(name: "Helvetica", size: 18)
view.layer.borderWidth = 2;
view.layer.cornerRadius = 16;
view.delegate=delegate
bindingUpdate={ //the closure
self.text=view.text
//add other lines here if you want make other effects while text changed
}
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
I'm not sure if the grammar is appropriate but it works fine
Update:
This is a better way if you still want to use TextField: you could use Coordinator to update the binding text.
struct MyTextField: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextField {
let view = UITextField()
view.borderStyle = .roundedRect
view.addTarget(
context.coordinator,
action: #selector(Coordinator.updateText(sender:)),
for: .editingChanged
)
return view
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject{
var myView: MyTextField
init(_ view: MyTextField){
self.myView=view
}
@objc func updateText(sender: UITextField){
myView.text=sender.text!
}
}
}
This is the appropriate grammar and it works too, although it can’t work with TextView: it don’t have method addTarget.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With