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()
}
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)
}
So I want to implement a custom control as a UIViewRepresentable which correctly handles focus using an #FocusState binding.
So I want to be able to manage the focus like so:
struct MyControl: UIViewRepresentable { ... }
struct Container: View {
#FocusState var hasFocus: Bool = false
var body: some View {
VStack {
MyControl()
.focused($hasFocus)
Button("Focus my control") {
hasFocus = true
}
}
}
}
What do I have to implement in MyControl to have it respond to the focus state properly? Is there a protocol or something which must be implemented?
Disclaimer: the solution is not suitable for full custom controls. For these cases, you can try to pass the FocusState as binding: let isFieldInFocus: FocusState<Int?>.Binding
In my case, I have wrapped UITextView. In order to set the focus, I only used .focused($isFieldInFocus).
Some of the information can be obtained through the property wrapper(#Environment(\.)), but this trick does not work with focus.
struct ContentView: View {
#FocusState var isFieldInFocus: Bool
#State var text = "Test message"
#State var isDisabled = false
var body: some View {
VStack {
Text("Focus for UITextView")
.font(.headline)
AppTextView(text: $text)
.focused($isFieldInFocus)
.disabled(isDisabled)
.frame(height: 200)
HStack {
Button("Focus") {
isFieldInFocus = true
}
.buttonStyle(.borderedProminent)
Button("Enable/Disable") {
isDisabled.toggle()
}
.buttonStyle(.borderedProminent)
}
}
.padding()
.background(.gray)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AppTextView: UIViewRepresentable {
#Binding var text: String
#Environment(\.isEnabled) var isEnabled: Bool
#Environment(\.isFocused) var isFocused: Bool // doesn't work
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.font = UIFont.preferredFont(forTextStyle: .body)
textView.delegate = context.coordinator
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = text
print("isEnabled", isEnabled, "isFocused", isFocused)
}
class Coordinator : NSObject, UITextViewDelegate {
private var parent: AppTextView
init(_ textView: AppTextView) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
parent.text = textView.text
}
}
}
I'm using a wrapper around UISearchBar and I'm seeing a different behavior when passing a #Published property into this SearchBar wrapper versus a TextField.
Both are updating the #ObservedObject var query = Query() class #Published var input property as expected but only the TextField is then updating the SearchSheet view. I would like for the view to be updated when input has been changed in SearchBar similarly to how it is updated from TextField.
Edit: I've updated my question to include the ContentView where it looks like this issue is specific to when the sheet is called from a Button in a NavigationBarItem.
struct ContentView: View {
#State var showingSearch = false
var body: some View {
NavigationView {
VStack {
Text("Hello World")
}
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}
)
}
}
}
class Query: ObservableObject {
#Published var input = "" {
didSet {
// Called as expected in both cases but only TextField updates the SearchSheet view.
}
}
}
struct SearchSheet: View {
#ObservedObject var query = Query()
var body: some View {
VStack {
// Does not update the SearchSheet view. I would like it understand why and how to update it.
SearchBar(text: $query.input, placeholder: "Search")
// Does update the SearchSheet view.
TextField("Search", text: $query.input)
Text("\(query.input)")
}
}
}
import SwiftUI
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
struct SearchBar_Previews: PreviewProvider {
#State private static var text = ""
static var previews: some View {
SearchBar(text: $text, placeholder: "Search")
}
}
Moving the display of the sheet out from navigationBarItems resolves the issue. At the time I believe this is a bug.
.navigationBarItems(trailing:
Button(action: {
self.showingSearch.toggle()
}) {
Image(systemName: "magnifyingglass")
}
)
.sheet(isPresented: $showingSearch) {
SearchSheet(isPresented: self.$showingSearch)
}
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)
}
}