SwiftUI: stay on same TextField after on commit? - swiftui

is it possible in SwiftUI to keep the typing cursor on the same Textfield even after the user taps on Return key on keyboard ?
Here is my code:
struct RowView: View {
#Binding var checklistItem: ChecklistItem
#ObservedObject var checklist = Checklist()
#ObservedObject var viewModel: ChecklistViewModel
var body: some View {
HStack {
Button {
self.checklistItem.isChecked.toggle()
self.viewModel.updateChecklist(checklistItem)
} label: {
Circle()
.strokeBorder(checklistItem.isChecked ? checklistSelected : contentPrimary, lineWidth: checklistItem.isChecked ? 6 : 2)
.foregroundColor(backgroundSecondary)
.clipShape(Circle())
.frame(width: 16, height: 16)
}.buttonStyle(BorderlessButtonStyle())
// swiftlint:disable trailing_closure
TextField(
"Add...",
text: $checklistItem.name,
onCommit: {
do {
if !checklistItem.name.isEmpty {
self.viewModel.updateChecklist(checklistItem)
self.checklistItem.name = checklistItem.name
}
}
}
)
// swiftlint:enable trailing_closure
.foregroundColor(checklistItem.isChecked ? contentTertiary : contentPrimary)
Spacer()
}
}
}
So after the user taps on return key on keyboard, TextField() onCommit should be activated normally but the cursor stays in that same textfield so the user can keep typing in new elements.

iOS 15+
You can use #FocusState and, on commit, immediately set the TextField to have focus again.
Example:
struct ContentView: View {
#State private var text = "Hello world!"
#FocusState private var isFieldFocused: Bool
var body: some View {
Form {
TextField("Field", text: $text, onCommit: {
isFieldFocused = true
print("onCommit")
})
.focused($isFieldFocused)
}
}
}
Result:

I was able to achieve this in iOS 14 by creating a custom TextField class:
struct AlwaysActiveTextField: UIViewRepresentable {
let placeholder: String
#Binding var text: String
var focusable: Binding<[Bool]>?
var returnKeyType: UIReturnKeyType = .next
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var isSecureTextEntry: Bool
var tag: Int
var onCommit: () -> Void
func makeUIView(context: Context) -> UITextField {
let activeTextField = UITextField(frame: .zero)
activeTextField.delegate = context.coordinator
activeTextField.placeholder = placeholder
activeTextField.font = .systemFont(ofSize: 14)
activeTextField.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [NSAttributedString.Key.foregroundColor: UIColor(contentSecondary)]
)
activeTextField.returnKeyType = returnKeyType
activeTextField.autocapitalizationType = autocapitalizationType
activeTextField.keyboardType = keyboardType
activeTextField.isSecureTextEntry = isSecureTextEntry
activeTextField.textAlignment = .left
activeTextField.tag = tag
// toolbar
if keyboardType == .numberPad { // keyboard does not have next so add next button in the toolbar
var items = [UIBarButtonItem]()
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
let nextButton = UIBarButtonItem(title: "Next", style: .plain, target: context.coordinator, action: #selector(Coordinator.showNextTextField))
items.append(contentsOf: [spacer, nextButton])
toolbar.setItems(items, animated: false)
activeTextField.inputAccessoryView = toolbar
}
// Editin listener
activeTextField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return activeTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if let focusable = focusable?.wrappedValue {
if focusable[uiView.tag] { // set focused
uiView.becomeFirstResponder()
} else { // remove keyboard
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let activeTextField: AlwaysActiveTextField
var hasEndedViaReturn = false
weak var textField: UITextField?
init(_ activeTextField: AlwaysActiveTextField) {
self.activeTextField = activeTextField
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField = textField
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
var focusable: [Bool] = Array(repeating: false, count: textFieldCount) // remove focus from all text field
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
}
// work around for number pad
#objc
func showNextTextField() {
if let textField = self.textField {
_ = textFieldShouldReturn(textField)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
hasEndedViaReturn = true
guard var focusable = activeTextField.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
activeTextField.onCommit()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if !hasEndedViaReturn {// user dismisses keyboard
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
// reset all text field, so that makeUIView cannot trigger keyboard
activeTextField.focusable?.wrappedValue = Array(repeating: false, count: textFieldCount)
} else {
hasEndedViaReturn = false
}
}
#objc
func textFieldDidChange(_ textField: UITextField) {
activeTextField.text = textField.text ?? ""
}
}
}
and use in in the SwiftUI view by adding this #State variable:
#State var fieldFocus: [Bool] = [false]
and add the Textfield code it self anywhere waiting the view body:
AlwaysActiveTextField(
placeholder: "Add...",
text: $newItemName,
focusable: $fieldFocus,
returnKeyType: .next,
isSecureTextEntry: false,
tag: 0,
onCommit: {
print("any action you want on commit")
}
)

Related

How to clear all text in a UIViewRepresentable Text Field with a view modifier

I'm trying to delete all text inside a Text Field when a button to the right of the text field is tapped. When I tap the clear button it behaves as though the text has been cleared. It shows the placeholder text which is set to be shown when the Text Field is cleared. However, the text is still showing 'underneath' the placeholder text. I can delete the typed in text by using the delete key on the keyboard, one character at a time, but clearing all at once should be a quicker option.
I have a UIViewRepresentable for a TextField as below. The reason I'm using a UIViewRepresentable is because I need the keyboard language layout to change languages as required by the user. SwiftUI doesn't currently support this:
struct UITextFieldViewRepresentable: UIViewRepresentable {
#Binding var language: String
#Binding var text: String
var onCommit: (() -> Bool)
init(language: Binding<String>, text: Binding<String>, onCommit: #escaping() -> Bool = { return true }) {
self._language = language
self._text = text
self.onCommit = onCommit
}
func makeUIView(context: Context) -> WordTextField {
let textField = WordTextField(onCommit: onCommit)
textField.language = self.language
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}
func updateUIView(_ uiView: WordTextField, context: Context) {
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
}
class WordTextField: UITextField, UITextFieldDelegate {
var textDidChange: ((String) -> Void)?
var onCommit: (() -> Bool)
var language: String? {
didSet {
if self.isFirstResponder{
self.resignFirstResponder()
self.becomeFirstResponder()
}
}
}
init(textDidChange: ( (String) -> Void)? = nil, onCommit: #escaping () -> Bool = { return true }, language: String? = nil) {
self.textDidChange = textDidChange
self.onCommit = onCommit
self.language = language
super.init(frame: .zero)
self.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func textFieldDidChangeSelection(_ textField: UITextField) {
textDidChange?(textField.text ?? "")
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return onCommit()
}
override var textInputMode: UITextInputMode? {
if let language = self.language {
print("text input mode: \(language)")
for inputMode in UITextInputMode.activeInputModes {
if let inputModeLanguage = inputMode.primaryLanguage, inputModeLanguage == language {
return inputMode
}
}
}
return super.textInputMode
}
}
I call it as below:
#State private var userAnswer: String = ""
#State private var keyboardLanguage: String = ""
#State private var hintColour: Color = .green
#State private var hintDisabled: Bool = false
UITextFieldViewRepresentable(language: $keyboardLanguage, text: Binding<String>(
get: { self.userAnswer },
set: {
self.userAnswer = $0.allowedCharacters(string: $0)
self.enableHint()
}), onCommit: {
checkAnswer()
return true
})
.showClearButton(userAnswer: $userAnswer, hintDisabled: $hintDisabled, hintColour: $hintColour)
The view modifier .showClearButton does not clear the userAnswer text when tapped. Tapping the keyboard delete button clears userAnswer one character at a time.
The view modifier code is below:
extension View {
func showClearButton(userAnswer: Binding<String>, hintDisabled: Binding<Bool>, hintColour: Binding<Color>) -> some View {
self.modifier(TextFieldClearButton(text: userAnswer, hintDisabled: hintDisabled, hintColour: hintColour))
}
}
struct TextFieldClearButton: ViewModifier {
#Binding var text: String
#Binding var hintDisabled: Bool
#Binding var hintColour: Color
func body(content: Content) -> some View {
HStack {
content
if !text.isEmpty {
Button(
action: { self.text = ""; hintDisabled = false; hintColour = Color.systemBlue },
label: {
Image(systemName: "delete.left")
.foregroundColor(Color(UIColor.gray))
.imageScale(.large)
.frame(width: 44, height: 44, alignment: .trailing)
}
)
}
}
}
}
You forgot to set the new text on the UITextField which in the case of the clear button is an empty string, fix like this:
func updateUIView(_ uiView: WordTextField, context: Context) {
// set the new text
uiView.text = text
// you also forgot to set the new onCommit
uiView.onCommit = onCommit
uiView.textDidChange = {
text = $0
}
// Change the keyboard language only when uiView.language != self.language
//
if uiView.language != self.language {
uiView.language = self.language
}
}
Also, you could benefit from a Coordinator here, it works like this:
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextField {
context.coordinator.textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
context.coordinator.textDidChange = {
text = $0
}
uiView.onCommit = onCommit
uiView.text = text // maybe could check it is different first
if uiView.language != self.language {
uiView.language = self.language
}
}
class Coordinator: NSObject, UITextFieldDelegate {
lazy var textField: {
let textField = UITextField()
textField.textAlignment = .center
textField.font = UIFont.systemFont(ofSize: 15, weight: .regular)
return textField
}()
// your closure properties
// all your delegate methods
}

How do I make a UIBarButtonItem (inside of a UIToolbar, inside of a UITextView) change background colour/image when selected?

I am trying to make a WYSIWYG editor by interfacing between SwiftUI and UIKit via a UIViewRepresentable.
I am not sure how to change the background colour/image of a UIBarButtonItem to show that the underline attribute button is selected. The UIBarButtonItem is inside of a UIToolbar, which is inside of a UITextView. I need the underline button to be a different colour when either the selected text contains the underline attribute, or if the typingAttributes contains the underline attribute (because the underline button has been selected).
Any help would be greatly appreciated.
Below is the code:
import SwiftUI
import UIKit
struct ContentView: View {
#State private var mutableString: NSMutableAttributedString = NSMutableAttributedString(
string: "this is the NSMutableAttributeString with no attributes")
var body: some View {
WYSIWYG(outerMutableString: $mutableString)
}
}
struct WYSIWYG: UIViewRepresentable {
#Binding var outerMutableString: NSMutableAttributedString
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> UITextView {
context.coordinator.textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = outerMutableString
context.coordinator.stringDidChange = { string in
outerMutableString = string
}
}
class Coordinator: NSObject, UITextViewDelegate {
private let fontSize: CGFloat = 32.0
// var to check if the underline button has been pressed
private var underlineIsSelected: Bool = false
lazy var textView: UITextView = {
let textView = UITextView()
textView.font = UIFont(name: "Helvetica", size: fontSize)
textView.delegate = self
// make toolbar
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: textView.frame.size.width, height: 44))
// make toolbar underline button
let underlineButton = UIBarButtonItem(
image: UIImage(systemName: "underline"),
style: .plain,
target: self,
action: #selector(underline))
toolBar.items = [underlineButton]
toolBar.sizeToFit()
textView.inputAccessoryView = toolBar
return textView
}()
var stringDidChange: ((NSMutableAttributedString) -> ())?
func textViewDidChange(_ textView: UITextView) {
stringDidChange?(textView.textStorage)
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var list: [NSAttributedString.Key: Any] = [:]
if underlineIsSelected { list[.underlineStyle] = NSUnderlineStyle.single.rawValue }
textView.typingAttributes = list
stringDidChange?(textView.textStorage)
return true
}
func textViewDidChangeSelection(_ textView: UITextView) { }
#objc func underline() {
let range = textView.selectedRange
if (range.length > 0) {
if (isActive(key: .underlineStyle)) {
textView.textStorage.removeAttribute(
.underlineStyle,
range: range)
} else {
textView.textStorage.addAttribute(
.underlineStyle,
value: NSUnderlineStyle.single.rawValue,
range: range)
}
stringDidChange?(textView.textStorage)
}
underlineIsSelected.toggle()
}
// func to check if the selected part of the NSMutableAttributedString contains the attribute key
func isActive(key: NSAttributedString.Key) -> Bool {
var range = textView.selectedRange
if range.length > 0 {
return (textView.textStorage.attribute(
key,
at: range.location,
longestEffectiveRange: &range,
in: range) != nil) ? true : false
}
return false
}
}
}

SwiftUI: Always active textfield keyboard doesn't dismiss

I made a custom Textfield an always active textfield with the help of some tutorials, meaning that user can tap "next on keyboard" and commits the message and continue doing so without hiding the keyboard, but now the keyboard is stuck, doesn't close when tapped outside, I tried everything I saw online, some of them were basically triggering UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
Note: my minimum iOS requirements is iOS 14 which restricts some of the new functions that helps with my issue in iOS15.
that didn't work with that Custom Textfield, its like its overriding those functions.
here is the textfield code:
import SwiftUI
struct AlwaysActiveTextField: UIViewRepresentable {
let placeholder: String
#Binding var text: String
var focusable: Binding<[Bool]>?
var returnKeyType: UIReturnKeyType = .next
var autocapitalizationType: UITextAutocapitalizationType = .none
var keyboardType: UIKeyboardType = .default
var isSecureTextEntry: Bool
var tag: Int
var onCommit: () -> Void
func makeUIView(context: Context) -> UITextField {
let activeTextField = UITextField(frame: .zero)
activeTextField.delegate = context.coordinator
activeTextField.placeholder = placeholder
activeTextField.font = .systemFont(ofSize: 14)
activeTextField.attributedPlaceholder = NSAttributedString(
string: placeholder,
attributes: [NSAttributedString.Key.foregroundColor: UIColor(contentTertiary)]
)
activeTextField.returnKeyType = returnKeyType
activeTextField.autocapitalizationType = autocapitalizationType
activeTextField.keyboardType = keyboardType
activeTextField.isSecureTextEntry = isSecureTextEntry
activeTextField.textAlignment = .left
activeTextField.tag = tag
// toolbar
if keyboardType == .numberPad { // keyboard does not have next so add next button in the toolbar
var items = [UIBarButtonItem]()
let spacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let toolbar: UIToolbar = UIToolbar()
toolbar.sizeToFit()
let nextButton = UIBarButtonItem(title: "Next", style: .plain, target: context.coordinator, action: #selector(Coordinator.showNextTextField))
items.append(contentsOf: [spacer, nextButton])
toolbar.setItems(items, animated: false)
activeTextField.inputAccessoryView = toolbar
}
// Editin listener
activeTextField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldDidChange(_:)), for: .editingChanged)
return activeTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.text = text
if let focusable = focusable?.wrappedValue {
if focusable[uiView.tag] { // set focused
uiView.becomeFirstResponder()
} else { // remove keyboard
uiView.resignFirstResponder()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, UITextFieldDelegate {
let activeTextField: AlwaysActiveTextField
var hasEndedViaReturn = false
weak var textField: UITextField?
init(_ activeTextField: AlwaysActiveTextField) {
self.activeTextField = activeTextField
}
func textFieldDidBeginEditing(_ textField: UITextField) {
self.textField = textField
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
var focusable: [Bool] = Array(repeating: false, count: textFieldCount) // remove focus from all text field
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
}
// work around for number pad
#objc
func showNextTextField() {
if let textField = self.textField {
_ = textFieldShouldReturn(textField)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
hasEndedViaReturn = true
guard var focusable = activeTextField.focusable?.wrappedValue else {
textField.resignFirstResponder()
return true
}
focusable[textField.tag] = true // mark current textField focused
activeTextField.focusable?.wrappedValue = focusable
activeTextField.onCommit()
return true
}
func textFieldDidEndEditing(_ textField: UITextField) {
if !hasEndedViaReturn {// user dismisses keyboard
guard let textFieldCount = activeTextField.focusable?.wrappedValue.count else { return }
// reset all text field, so that makeUIView cannot trigger keyboard
activeTextField.focusable?.wrappedValue = Array(repeating: false, count: textFieldCount)
} else {
hasEndedViaReturn = false
}
}
#objc
func textFieldDidChange(_ textField: UITextField) {
activeTextField.text = textField.text ?? ""
}
}
}
used it on sample SwiftUI Sheet view and inside the .sheet view:
AlwaysActiveTextField(
placeholder: "Add...",
text: $newItemName,
focusable: $fieldFocus,
returnKeyType: .next,
isSecureTextEntry: false,
tag: 0,
onCommit: {
if !newItemName.isEmpty {
let newChecklistItem = ChecklistItem(
shotId: shotlistViewModel.shot.id,
name: self.newItemName,
isChecked: false,
description: ""
)
self.checklistItems.append(newChecklistItem)
if !offlineMode {
self.viewModel.addChecklistItem(newChecklistItem)
}
self.newItemName = ""
}
}
)

Keyboard dismiss not working properly when dismiss the view swiftui

I'm trying to dismiss the keyboard when view dismissed but keyboard not dismiss properly, they only hide button but they showing height. More detail please check my code and screenshot.
Keyboard shows properly
Go To next Screen when keyboard open and dismiss
Keyboard button hide but show height with background
import SwiftUI
struct ContentView: View {
#State private var titleDtr: String = ""
#State private var clearAllText: Bool = false
#State var updateKeyboard = true
#State var userProfiles = [""]
var body: some View {
NavigationView{
HStack{
CustomTxtfldForFollowerScrn(isDeleteAcntScreen: .constant(false), text: $titleDtr, clearAllText: $clearAllText, isFirstResponder: $updateKeyboard, completion: { (reponse) in
if titleDtr.count >= 3 {
userProfiles.append(titleDtr)
}else if titleDtr.count <= 3 {
userProfiles.removeAll()
}
}).background(Color.red)
VStack(spacing:0) {
List {
if userProfiles.count > 0 {
ForEach(userProfiles.indices, id: \.self) { indexs in
NavigationLink(destination: ShowLoadingText()) {
Text(userProfiles[indexs]).foregroundColor(.blue)
}
}
}
}
}
}.onDisappear{
updateKeyboard = false
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct ShowLoadingText: View {
var body: some View {
ZStack {
VStack(alignment:.center, spacing: 15) {
HStack(spacing:10){
Group {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color.white))
Text("Loading More Users...")
}
}.foregroundColor(.black)
}
}
}
}
struct CustomTxtfldForFollowerScrn: UIViewRepresentable {
#Binding var isDeleteAcntScreen: Bool
#Binding var text: String
#Binding var clearAllText: Bool
#Binding var isFirstResponder: Bool
var completion: (String) -> Void
func makeUIView(context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) -> UITextField {
let textField = UITextField(frame: .zero)
textField.text = text
textField.delegate = context.coordinator
textField.backgroundColor = .clear
if isDeleteAcntScreen {
textField.placeholder = "DELETE"
}else{
textField.placeholder = "Username"
}
textField.returnKeyType = .default
textField.textColor = .black
return textField
}
func makeCoordinator() -> CustomTxtfldForFollowerScrn.Coordinator {
return Coordinator(self)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}else if clearAllText {
DispatchQueue.main.async {
uiView.text! = ""
text = ""
clearAllText = false
}
}else if !isFirstResponder {
DispatchQueue.main.async {
UIApplication.shared.endEditing()
}
}
}
class Coordinator: NSObject, UITextFieldDelegate {
var didBecomeFirstResponder = false
var parent: CustomTxtfldForFollowerScrn
init(_ view: CustomTxtfldForFollowerScrn) {
self.parent = view
}
func textFieldDidChangeSelection(_ textField: UITextField) {
DispatchQueue.main.async {
self.parent.text = textField.text ?? ""
self.parent.completion(textField.text!)
}
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
}
import UIKit
extension UIApplication {
func endEditing() {
sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}
}
Here is the alternative way to dismissing the keyboard.
First, remove the main queue.
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<CustomTxtfldForFollowerScrn>) {
uiView.text = text
if isFirstResponder && !context.coordinator.didBecomeFirstResponder {
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}else if clearAllText {
DispatchQueue.main.async {
uiView.text! = ""
text = ""
clearAllText = false
}
}else if !isFirstResponder { // < -- From here
UIApplication.shared.endEditing()
}
}
and then update updateKeyboard on ShowLoadingText() appear.
ForEach(userProfiles.indices, id: \.self) { indexs in
NavigationLink(destination: ShowLoadingText().onAppear() { updateKeyboard = false }) { // <-- Here
Text(userProfiles[indexs]).foregroundColor(.blue)
}
}
Remove onDisappear code.

Custom UITextField wrapped in UIViewRepresentable interfering with ObservableObject in NavigationView

Context
I have created a UIViewRepresentable to wrap a UITextField so that:
it can be set it to become the first responder when the view loads.
the next textfield can be set to become the first responder when enter is pressed
Problem
When used inside a NavigationView, unless the keyboard is dismissed from previous views, the view doesn't observe the value in their ObservedObject.
Question
Why is this happening? What can I do to fix this behaviour?
Screenshots
Keyboard from root view not dismissed:
Keyboard from root view dismissed:
Code
Here is the said UIViewRepresentable
struct SimplifiedFocusableTextField: UIViewRepresentable {
#Binding var text: String
private var isResponder: Binding<Bool>?
private var placeholder: String
private var tag: Int
public init(
_ placeholder: String = "",
text: Binding<String>,
isResponder: Binding<Bool>? = nil,
tag: Int = 0
) {
self._text = text
self.placeholder = placeholder
self.isResponder = isResponder
self.tag = tag
}
func makeUIView(context: UIViewRepresentableContext<SimplifiedFocusableTextField>) -> UITextField {
// create textfield
let textField = UITextField()
// set delegate
textField.delegate = context.coordinator
// configure textfield
textField.placeholder = placeholder
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.tag = self.tag
// return
return textField
}
func makeCoordinator() -> SimplifiedFocusableTextField.Coordinator {
return Coordinator(text: $text, isResponder: self.isResponder)
}
func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<SimplifiedFocusableTextField>) {
// update text
uiView.text = text
// set first responder ONCE
if self.isResponder?.wrappedValue == true && !uiView.isFirstResponder && !context.coordinator.didBecomeFirstResponder{
uiView.becomeFirstResponder()
context.coordinator.didBecomeFirstResponder = true
}
}
class Coordinator: NSObject, UITextFieldDelegate {
#Binding var text: String
private var isResponder: Binding<Bool>?
var didBecomeFirstResponder = false
init(text: Binding<String>, isResponder: Binding<Bool>?) {
_text = text
self.isResponder = isResponder
}
func textFieldDidChangeSelection(_ textField: UITextField) {
text = textField.text ?? ""
}
func textFieldDidBeginEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = true
}
}
func textFieldDidEndEditing(_ textField: UITextField) {
DispatchQueue.main.async {
self.isResponder?.wrappedValue = false
}
}
}
}
And to reproduce, here is the contentView:
struct ContentView: View {
var body: some View {
return NavigationView { FieldView(tag: 0) }
}
}
and here's the view with the field and its view model
struct FieldView: View {
#ObservedObject private var viewModel = FieldViewModel()
#State private var focus = false
var tag: Int
var body: some View {
return VStack {
// listen to viewModel's value
Text(viewModel.value)
// text field
SimplifiedFocusableTextField("placeholder", text: self.$viewModel.value, isResponder: $focus, tag: self.tag)
// push to stack
NavigationLink(destination: FieldView(tag: self.tag + 1)) {
Text("Continue")
}
// dummy for tapping to dismiss keyboard
Color.green
}
.onAppear {
self.focus = true
}.dismissKeyboardOnTap()
}
}
public extension View {
func dismissKeyboardOnTap() -> some View {
modifier(DismissKeyboardOnTap())
}
}
public struct DismissKeyboardOnTap: ViewModifier {
public func body(content: Content) -> some View {
return content.gesture(tapGesture)
}
private var tapGesture: some Gesture {
TapGesture().onEnded(endEditing)
}
private func endEditing() {
UIApplication.shared.connectedScenes
.filter {$0.activationState == .foregroundActive}
.map {$0 as? UIWindowScene}
.compactMap({$0})
.first?.windows
.filter {$0.isKeyWindow}
.first?.endEditing(true)
}
}
class FieldViewModel: ObservableObject {
var subscriptions = Set<AnyCancellable>()
// diplays
#Published var value = ""
}
It looks like SwiftUI rendering engine again over-optimized...
Here is fixed part - just make destination unique forcefully using .id. Tested with Xcode 11.4 / iOS 13.4
NavigationLink(destination: FieldView(tag: self.tag + 1).id(UUID())) {
Text("Continue")
}