Here is my code:
struct CustomTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<CustomTextField>) -> UITextField {
let uiView = UITextField()
uiView.delegate = context.coordinator
return uiView
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if uiView.window != nil, !uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ CustomTextField: CustomTextField) {
self.parent = CustomTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
Building doesn't cause the warning, but using CustomTextField in the simulator does.
Here is the warning:
Modifying state during view update, this will cause undefined behavior.
It is showing on this line:
parent.text = textField.text ?? ""
How do I get rid of this warning? What am I doing wrong?
Try to perform modification asynchronously:
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.parent.text = self.textField.text ?? ""
}
}
Related
I have a code and I need to get the selected range of TextView from (func textViewDidChangeSelection) to the ContentView. How do I do such a thing in SwiftUI?
this is a basic idea of my code.
struct ContentView: View {
#State private var text = ""
var body: some View {
UITextViewRepresentable(text: $text)
}
}
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
// SwiftUI -> UIKit
uiView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(text: $text)
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textViewDidChange(_ textView: UITextView) {
// UIKit -> SwiftUI
_text.wrappedValue = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
// Fires off every time the user changes the selection.
print(textView.selectedRange)
}
}
}
Just add an extra binding for it, of type NSRange, as that is the type of textView.selectedRange.
That means adding one to the UIViewRepresentable:
struct UITextViewRepresentable: UIViewRepresentable {
let textView = UITextView()
#Binding var text: String
#Binding var range: NSRange // <---
and one to the coordinator:
func makeCoordinator() -> Coordinator {
Coordinator(text: $text, range: $range) // <---
}
class Coordinator: NSObject, UITextViewDelegate {
#Binding var text: String
#Binding var range: NSRange // <---
init(text: Binding<String>, range: Binding<NSRange>) {
_text = text
_range = range
}
// ...
func textViewDidChangeSelection(_ textView: UITextView) {
range = textView.selectedRange
}
Now in ContentView, you can get it as a #State:
#State private var text = ""
#State private var range = NSRange()
var body: some View {
UITextViewRepresentable(text: $text, range: $range)
}
I am trying to build a simple text editor using swiftUI and UITextView. In UITextView I am trying to use link detection which requires isEditable property to be false. But when I make isEditable false I am not able to make it true again and I am not able to type any text. No delegate method works when you make isEditable false.
Can anyone tell me how to make isEditable true again ?
UITextView, UIViewRepresentable
import SwiftUI
struct TextView: UIViewRepresentable {
#Binding var text: String
let textView = UITextView()
class Coordinator: NSObject, UITextViewDelegate {
var parent: TextView
init(_ parent: TextView) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
textView.isEditable = false
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
textView.dataDetectorTypes = .link
textView.becomeFirstResponder()
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
}
}
ContentView
import SwiftUI
struct ContentView: View {
#State private var text = "www.apple.com"
var body: some View {
TextView(text: $text)
}
}
Thank You!
I'm trying to use UITextField in SwiftUI.
I've created a custom struct for UITextField and made a Bindable String, which holds the input text.
When I'm trying to pass the input text back to the viewModel, it doesn't work.
For the sake of comparing, I placed SwiftUI Textfield in the code and it works
Could someone please help?
import SwiftUI
final class RegistrationViewViewModel: ObservableObject {
#Published var firstName: String = "" {
didSet {
print(oldValue)
}
willSet {
print(newValue)
}
}
#Published var lastName: String = "" {
didSet {
print(oldValue)
}
willSet {
print(newValue)
}
}
func saveData() {
print("Saving...", firstName, lastName)
}
}
struct RegistrationView: View {
#ObservedObject var viewModel = RegistrationViewViewModel()
var body: some View {
VStack(spacing: 50) {
TextField("Last name", text: $viewModel.lastName)
.frame(width: 300, height: 35, alignment: .center)
.border(Color(.systemGray))
CustomTextField(inputText: $viewModel.firstName, placeholder: "First name")
.frame(width: 300, height: 35, alignment: .center)
.border(Color(.systemGray))
Button(action: {
self.viewModel.saveData()
}) {
Text("Submit")
}
}
}
}
struct CustomTextField: UIViewRepresentable {
var inputText: Binding<String>
var placeholder: String
func makeUIView(context: Context) -> UITextField {
let textfield = UITextField(frame: .zero)
textfield.delegate = context.coordinator
textfield.placeholder = placeholder
return textfield
}
func updateUIView(_ uiView: UITextField, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ parent: CustomTextField) {
self.parent = parent
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}
try to set your updateUIView function as below:
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = inputText
}
I want to disable the option of copy/paste from my Textfield in SwiftUI. How to achieve that?
This works for me:
Using UITextField conforms to UIViewRepresentable.
import SwiftUI
struct ContentView: View {
#State private var text = ""
var body: some View {
VStack {
Text(text)
UITextFieldViewRepresentable(text: $text) // Using it
.frame(height: 44)
.border(.red)
}
}
}
// MARK: UITextFieldViewRepresentable
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var text: String
typealias UIViewType = ProtectedTextField
func makeUIView(context: Context) -> ProtectedTextField {
let textField = ProtectedTextField()
textField.delegate = context.coordinator
return textField
}
// From SwiftUI to UIKit
func updateUIView(_ uiView: ProtectedTextField, context: Context) {
uiView.text = text
}
// From UIKit to SwiftUI
func makeCoordinator() -> Coordinator {
return Coordinator(text: $text)
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
init(text: Binding<String>) {
self._text = text
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
}
}
// Custom TextField with disabling paste action
class ProtectedTextField: UITextField {
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) {
return false
}
return super.canPerformAction(action, withSender: sender)
}
}
Use UIViewRepresentable class make wrapper class like this.
import SwiftUI
struct CustomeTextField: View {
#State var textStr = ""
var body: some View {
VStack(spacing: 10) {
Text("This is textfield:")
.font(.body)
.foregroundColor(.gray)
TextFieldWrapperView(text: self.$textStr)
.background(Color.gray)
.frame(width: 200, height: 50)
}
.frame(height: 40)
}
}
struct TextFieldWrapperView: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> TFCoordinator {
TFCoordinator(self)
}
}
extension TextFieldWrapperView {
func makeUIView(context: UIViewRepresentableContext<TextFieldWrapperView>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
class TFCoordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldWrapperView
init(_ textField: TextFieldWrapperView) {
self.parent = textField
}
func canPerformAction(action: Selector, withSender sender: AnyObject?) -> Bool {
if action == #selector(UIResponderStandardEditActions.paste(_:)) {
return false
}
return canPerformAction(action: action, withSender: sender)
}
}
I know how to create a UIViewRepresentable in SwiftUI in order to enable first responder functionality for a TextField:
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (!uiView.isFirstResponder) {
uiView.becomeFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
My problem is attempting to "re-focus" this custom text field. So while this text field DOES get focused when my ContentView is initialized, I want to know how I can re-focus this text field programmatically, AFTER it has lost focus.
Here is my ContentView:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
// ???
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
Here is what I've tried. I thought maybe I could create a #State variable which can control the FirstResponderTextField, so I went ahead and changed my structs as follows:
struct ContentView: View {
#State var textField1: String = ""
#State var textField2: String = ""
#State var isFocused: Bool = true
var body: some View {
Form {
Section {
FirstResponderTextField(placeholder: "Text Field 1", text: $textField1, isFocused: $isFocused)
TextField("Text Field 2", text: $textField2)
}
Section {
Button(action: {
self.isFocused = true
}, label: {
Text("Re-Focus Text Field 1")
})
}
}
}
}
struct FirstResponderTextField: UIViewRepresentable {
var placeholder: String
#Binding var text: String
#Binding var isFocused: Bool
func makeUIView(context: UIViewRepresentableContext<FirstResponderTextField>) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<FirstResponderTextField>) {
uiView.placeholder = placeholder
uiView.text = text
if (isFocused) {
uiView.becomeFirstResponder()
isFocused = false
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var firstResponderTextField: FirstResponderTextField
init(_ firstResponderTextField: FirstResponderTextField) {
self.firstResponderTextField = firstResponderTextField
}
func textFieldDidChangeSelection(_ textField: UITextField) {
firstResponderTextField.text = textField.text ?? ""
}
}
}
It does not appear to be working. I mean, it works when I first click the button, but stops working afterwards.
Also, I am now getting this warning:
Modifying state during view update, this will cause undefined behaviour.
Is it possible to create a UIViewRepresentable that can be re-focused whenever I want?
You should not be setting the isFocused to false in your FirstResponderTextField.
Instead, in FirstResponderTextField, observe the value changes in the isFocused binding, and set your control to being first responder or not accordingly.
I created a solution for you with an ObserableObject corresponding to the TextFieldState in this gist on github
For future reference, this is what I changed:
instead of #State var isFocused: Bool = true in ContentView, use #ObservedObject var textFieldState = TextFieldState()
have a property #ObservedObject var state: TextFieldState in FirstResponderTextField
simply do this in FirstResponderTextField.updateUIView:
if state.isFirstResponder {
uiView.becomeFirstResponder()
}