In SwiftUI I have a simple search TextField where the user type something to be searched and a Button search.
I just want to add the option to have the button Search in the lower right corner of the Keyboard (I saw it in some application)
how can I do that?
iOS 15
You can change the return key for each textField with a simple modifier called: .submitLabel that takes the return key type and you should pass .search. Take a look at the following example:
Also, as you can see, you can have a callback to handle the return key press action just like the old textFieldShouldReturn function that is accessible by '.onSubmit' modifier.
If I understand correctly you want to change the UIReturnKeyType.
In that case you have to use UIKit since there isn't yet any option to change the type of return key in SwiftUI.
To do this, you have to make a custom TextField using UIIKit and then modify it the way you like.
Also keep in mind that the UIReturnKeyType enum is under discussion and may replace with a different implementation.
// MARK: Custom TextField
struct TextFieldTyped: UIViewRepresentable {
let keyboardType: UIKeyboardType
let returnVal: UIReturnKeyType
let tag: Int
#Binding var text: String
#Binding var isfocusAble: [Bool]
func makeUIView(context: Context) -> UITextField {
let textField = UITextField(frame: .zero)
textField.keyboardType = self.keyboardType
textField.returnKeyType = self.returnVal
textField.tag = self.tag
textField.delegate = context.coordinator
textField.autocorrectionType = .no
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if isfocusAble[tag] {
uiView.becomeFirstResponder()
} else {
uiView.resignFirstResponder()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: TextFieldTyped
init(_ textField: TextFieldTyped) {
self.parent = textField
}
func updatefocus(textfield: UITextField) {
textfield.becomeFirstResponder()
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.tag == 0 {
parent.isfocusAble = [false, true]
parent.text = textField.text ?? ""
} else if parent.tag == 1 {
parent.isfocusAble = [false, false]
parent.text = textField.text ?? ""
}
return true
}
}
}
And you can use it like this:
(Change the returnVal to .search in your case.)
struct CustomeKT: View {
#State var myTextForTX = ""
#State var focused: [Bool] = [false, true]
var body: some View {
TextFieldTyped(keyboardType: .default, returnVal: .search, tag: 0, text: self.$myTextForTX, isfocusAble: self.$focused)
}
}
Simple Use SearchTextField.swift
import SwiftUI
import UIKit
class UIKitTextField: UITextField, UITextFieldDelegate {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
delegate = self
}
required override init(frame: CGRect) {
super.init(frame: frame)
delegate = self
self.setContentHuggingPriority(.defaultHigh, for: .vertical)
}
var action:(() -> Void)? = nil
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.action?()
if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
return true;
}
return false
}
}
struct SearchTextField : UIViewRepresentable {
#Binding var text: String
var action:() -> Void
func makeCoordinator() -> SearchTextField.Coordinator {
return Coordinator(value: self)
}
class Coordinator: NSObject,UITextFieldDelegate {
var parent:SearchTextField
init(value: SearchTextField) {
self.parent = value
}
#objc func textFieldEditingChanged(_ sender: UIKitTextField) {
self.parent.text = sender.text ?? ""
}
}
func makeUIView(context: Context) -> UIKitTextField {
let textfield = UIKitTextField(frame: .zero)
textfield.addTarget(context.coordinator, action: #selector(Coordinator.textFieldEditingChanged(_:)), for: .editingChanged)
textfield.text = self.text
textfield.placeholder = "search"
textfield.borderStyle = .none
textfield.returnKeyType = .search
textfield.action = self.action
return textfield
}
func updateUIView(_ uiView: UIKitTextField,
context: Context) {
uiView.text = self.text
}
}
Use :
SearchTextField(text: self.$searchKey) {
self.search(key: self.searchKey)
// or...
}
Related
I would like to respond to keyboard presses in a UITextField wrapped as UIViewRepresentable. This is the code for the wrapper view:
import SwiftUI
import UIKit
struct ContentView: View {
#State var text: String = "Hello."
var body: some View {
WrappedUITextField(text: $text)
}
}
struct WrappedUITextField: UIViewRepresentable {
#Binding var text: String
func makeCoordinator() -> Coordinator {
Coordinator($text)
}
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
textField.text = text
textField.autocapitalizationType = .none
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if text != context.coordinator.currentText { uiView.text = text }
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var text: Binding<String>
var currentText: String
init(_ text: Binding<String>) {
self.text = text
currentText = text.wrappedValue
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let oldText = textField.text,
let textRange = Range(range, in: oldText) else {
return false
}
self.currentText = oldText.replacingCharacters(in: textRange, with: string)
print("oldText = \"\(oldText)\", new text = \"\(currentText)\"")
self.text.wrappedValue = currentText
return true
}
func textFieldDidChangeSelection(_ textField: UITextField) {
print("textFieldDidChangeSelection")
}
}
Specifically, I want to trigger some action when pressing left/right arrow key when the cursor is at the left/rightmost edge, e.g., "|abc", where "|" is the cursor position, and the user presses left arrow.
None of the UITextFieldDelegate methods will work here because pressing left/right arrow key when the cursor is already on the left/rightmost edge (respectively) neither changes the text contents (e.g., textFieldDidBeginEditing() won't be called) nor cursor position (e.g., textFieldDidChangeSelection() won't be called).
How do I respond to left/right arrow keys in this case? Something like a pressesBegan() method would be nice here, but I couldn't figure out how to integrate that with UIViewRepresentable (e.g., adding override func pressesBegan(...) to Coordinator doesn't work).
Thanks
Just create your class inherited UITextField and override handler there
func makeUIView(context: Context) -> UITextField {
let textField = MyTextField() // << here !!
textField.delegate = context.coordinator
textField.text = text
textField.autocapitalizationType = .none
return textField
}
class MyTextField: UITextField {
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
// handle here requried events
// call super if not handled above
super.pressesBegan(presses, with: event)
}
}
I try to show the text field keyboard as soon as the view appears and dismiss the keyboard when tap on the keyboard "return" key, the first part of the problem is solved by the code example bellow, But that make the keyboard "return" key doesn't work, Did any one can help to achieve my to show and dismiss the textfiled keyboard as I need.
import SwiftUI
struct MyTextField: UIViewRepresentable {
typealias UIViewType = UITextField
#Binding var becomeFirstResponder: Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
if self.becomeFirstResponder {
DispatchQueue.main.async {
textField.becomeFirstResponder()
self.becomeFirstResponder = false
}
}
}
}
struct TextFieldFirstResponder: View {
#State private var becomeFirstResponder = false
var body: some View {
MyTextField(becomeFirstResponder: self.$becomeFirstResponder)
.onAppear {
self.becomeFirstResponder = true
}
}
}
Use textFieldShouldReturn delegate method. For this make Coordinator for UIViewRepresentable.
struct MyTextField: UIViewRepresentable {
typealias UIViewType = UITextField
#Binding var becomeFirstResponder: Bool
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ textField: UITextField, context: Context) {
if self.becomeFirstResponder {
DispatchQueue.main.async {
textField.becomeFirstResponder()
self.becomeFirstResponder = false
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(parent: self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: MyTextField
init(parent: MyTextField) {
self.parent = parent
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
}
}
}
I attempted to bind a isEditing variable to under my UIViewRepresentable which is controlled by a close button in my SwiftUI View.
Under the UIViewRepresentable, I create a UITextfield and what I want to accomplish here is to tap the close button which triggers the isEditing variable and reset the UITextfield to make it leave edit mode. I tried to detect this change under the updateUIView
struct SearchBarViewController: UIViewRepresentable {
let searchEngine = SearchEngine()
let textField = LeftPaddedTextField(frame: .zero)
#Binding var text: String
#Binding var searchArray:[String]
#Binding var isEditing: Bool
func makeUIView(context: UIViewRepresentableContext<SearchBarViewController>) -> UITextField {
textField.delegate = context.coordinator
textField.textColor = UIColor.gray
textField.placeholder = "Where are you going?"
textField.layer.cornerRadius = 20
//textField.layer.borderWidth = 1.5
textField.layer.borderColor = UIColor.tertiaryLabel.cgColor
textField.backgroundColor = UIColor.systemGray6
textField.borderStyle = .none
textField.addTarget(context.coordinator, action: #selector(context.coordinator.textFieldDidChange), for: .editingChanged)
textField.clearButtonMode = .whileEditing
searchEngine.delegate = context.coordinator
return textField
}
func updateUIView(_ uiViewController: UITextField, context: UIViewRepresentableContext<SearchBarViewController>) {
if isEditing {
return
}
print("update is called")
if !isEditing {
//textField.resignFirstResponder()
textField.endEditing(true)
}
}
func makeCoordinator() -> SearchBarViewController.Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate, SearchEngineDelegate {
var control: SearchBarViewController
init(_ control: SearchBarViewController) {
self.control = control
}
func resultsUpdated(searchEngine: SearchEngine) {
self.control.searchArray = []
if !searchEngine.items.isEmpty {
for i in searchEngine.items {
if let description = i.descriptionText {
self.control.searchArray.append(description)
}
}
}
print()
}
func resolvedResult(result: SearchResult) {
print()
}
func searchErrorHappened(searchError: SearchError) {
print("Error during search: \(searchError)")
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.control.isEditing = true
}
func textFieldShouldClear(_ textField: UITextField) -> Bool {
return true
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
self.control.textField.resignFirstResponder()
self.control.isEditing = false
return true
}
// Update model.text when textField.text is changed
#objc func textFieldDidChange() {
if let text = self.control.textField.text {
self.control.text = text
}
if self.control.textField.text != "" {
if let text = control.textField.text {
control.searchEngine.query = text
}
} else {
self.control.searchArray = []
}
}
}
}
And here is the code of the outside view:
struct SearchBarView: View {
#State var isEditing = false
...
var body: some View {
Button(action: {
self.isEditing = false
self.text = ""
}) {
Text("Return")
}
SearchBarViewController(text: $text, searchArray: $searchArray, isEditing: $isEditing)
}
}
But the problem is it doesn't work. After I click the close button the UITextField doesn't exit its edit mode and I still can type. So I am asking whether there is a way to accomplish it.
Thanks for your help in advance.
Try to use passed in instance of text field
func updateUIView(_ uiTextField: UITextField, context: UIViewRepresentableContext<SearchBarViewController>) {
if isEditing {
return
}
print("update is called")
if !isEditing {
uiTextField.resignFirstResponder() // << here !!
// uiTextField.endEditing(true)
}
}
My problem is shown in the photo below:
I have tried using .fixedSize(horizontal:vertical) on the parent view and the textfield and no positive results.
TheTextField:
struct TheTextField: UIViewRepresentable {
#Binding var text : String
#Binding var placeholder : String
func makeCoordinator() -> TheTextField.Coordinator {
return TheTextField.Coordinator(parent1: self)
}
func makeUIView(context: UIViewRepresentableContext<TheTextField>) -> UITextView {
let tview = UITextView()
tview.isEditable = true
tview.isUserInteractionEnabled = true
tview.isScrollEnabled = false
tview.text = placeholder
tview.textColor = .gray
tview.font = .systemFont(ofSize: 20)
tview.delegate = context.coordinator
return tview
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<TheTextField>) {
}
class Coordinator : NSObject, UITextViewDelegate {
var parent : TheTextField
init(parent1 : TheTextField) {
parent = parent1
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
func textViewDidBeginEditing(_ textView: UITextView) {
textView.text = ""
textView.textColor = .label
}
}
}
I want the textview to not expand and move the cursor to the next line, instead of messing up the parent's view.
Example:
Divider()
TheTextField(text: self.$imageToUpload.textCaption, placeholder: self.$placeholder).padding(.horizontal)
Spacer()
import SwiftUI
struct UITextViewWrapper: UIViewRepresentable {
#Binding var text : String
#Binding var calculatedHeight: CGFloat
func makeCoordinator() -> UITextViewWrapper.Coordinator {
return UITextViewWrapper.Coordinator(text: $text, height: $calculatedHeight)
}
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
let tview = UITextView()
tview.isEditable = true
tview.isUserInteractionEnabled = true
tview.isScrollEnabled = false
tview.textColor = .gray
tview.font = .systemFont(ofSize: 20)
tview.delegate = context.coordinator
tview.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return tview
}
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<UITextViewWrapper>) {
if uiView.text != self.text {
uiView.text = self.text
}
if uiView.window != nil, uiView.isFirstResponder {
uiView.becomeFirstResponder()
}
}
static func recalculateHeight(view: UIView, result: Binding<CGFloat>){
let newSize = view.sizeThatFits(CGSize(width: view.frame.size.width, height: CGFloat.greatestFiniteMagnitude))
if result.wrappedValue != newSize.height {
DispatchQueue.main.async {
result.wrappedValue = newSize.height // !! must be called asynchronously
}
}
}
class Coordinator : NSObject, UITextViewDelegate {
var text : Binding<String>
var calculatedHeight : Binding<CGFloat>
init(text: Binding<String>, height: Binding<CGFloat>) {
self.text = text
self.calculatedHeight = height
}
func textViewDidChange(_ textView: UITextView) {
text.wrappedValue = textView.text
UITextViewWrapper.recalculateHeight(view: textView, result: calculatedHeight)
}
func textViewDidBeginEditing(_ textView: UITextView) {
textView.text = ""
textView.textColor = .label
}
}
}
I added functionality for changing the height of the dynamically depending on the length of the text per suggestion by Asperi.
You can easily edit label property from .storyboard file of our project.
Change lines to 0 and line break property to your desired type and then you can have as many lines as you want your label to have.Check this image to see what I'm saying
I have been stumped by this issue for a bit so I hoping to get some help. I have a SwiftUI application that is using UITextView but when I put content in it the text does not wrap. This SwiftUI view I am making both displays and edits text but it currently works for neither.
The text in the second box is much longer but the height does not change.
func makeUIView(context: Context) -> UITextView {
view.isScrollEnabled = false
view.isEditable = editable
view.isUserInteractionEnabled = true
view.contentInset = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0)
view.text = self.text
// For debugging
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 1.0
var fontPref = UIFont.preferredFont(forTextStyle: render.font)
if render.bold && render.italic {
fontPref = fontPref.bolditalic()
} else if render.bold {
fontPref = fontPref.bold()
} else if render.italic {
fontPref = fontPref.italic()
}
view.font = fontPref
view.textColor = render.color
view.textAlignment = render.align
view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
view.textContainer.lineBreakMode = .byWordWrapping
view.delegate = context.coordinator
return view
}
I have tried a few solutions but none of them are working for my use case.
Thanks!
here is a working custom textview that wraps the text and adjusts height:
import SwiftUI
struct NoteTextView: UIViewRepresentable {
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
#Binding var text: String
var onEndEditing: () -> Void
typealias UIViewType = UITextView
var configuration = { (view: UIViewType) in }
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIViewType {
let textField = UIViewType()
textField.delegate = context.coordinator
textField.font = UIFont.preferredFont(forTextStyle: .body)
textField.text = text
textField.isScrollEnabled = true
textField.isEditable = true
textField.isUserInteractionEnabled = true
return textField
}
func updateUIView(_ uiView: UIViewType, context: UIViewRepresentableContext<Self>) {
uiView.text = text
configuration(uiView)
}
class Coordinator : NSObject, UITextViewDelegate {
var parent: NoteTextView
init(_ uiTextView: NoteTextView) {
self.parent = uiTextView
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
return true
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
}
}