Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass state/binding to UIViewRepresentable

Tags:

ios

swift

swift5

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
    }
}
like image 357
Bryan Potts Avatar asked Apr 26 '20 18:04

Bryan Potts


People also ask

How do I conform to uiviewrepresentable?

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.

What is the difference between uiviewrepresentable and a view controller?

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.

What is the difference between makeuiview and update UIView?

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.

What is uiviewrepresentable in Swift?

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.


2 Answers

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.

like image 147
Md. Yamin Mollah Avatar answered Oct 12 '22 23:10

Md. Yamin Mollah


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.

like image 1
jlywxy Avatar answered Oct 12 '22 22:10

jlywxy