SwiftUI: How to prevent `onSubmit` on TextField from hiding keyboard? - ios

This simple TextField might be part of a chat feature, and I would like to be able to send chat messages when I press the keyboard button "send".
(Imagine in this chat I don't need to allow users to enter newline, by overriding the return key, to be send with the submitLabel(.send) view modifier.)
TextField(
"Chat...",
text: $draft
)
.submitLabel(.send)
.onSubmit {
if !draft.isEmpty {
sendMessage(draft: draft)
}
}
However, this will hide the keyboard, and I would like to know:
is there any way to prevent the keyboard from hiding when I press send??
I know how to refocus the field, I can do that with #FocusState but that still results in a hide keyboard animation starting which then aborts, so looks glithy.

Here is something you can work with. During .onSubmit set the focusedField back to .field, and use .transaction to capture the current transaction and set the animation to nil:
struct ContentView: View {
enum FocusField: Hashable {
case field
}
#State private var draft = "Hello, world"
#FocusState private var focusedField: FocusField?
var body: some View {
TextField(
"Chat...",
text: $draft
)
.submitLabel(.send)
.focused($focusedField, equals: .field)
.onSubmit {
focusedField = .field
if !draft.isEmpty {
sendMessage(draft: draft)
}
}
.transaction { t in
t.animation = nil
}
}
func sendMessage(draft: String) {
print("The message: \(draft)")
}
}

It is possible to prevent keyboard hiding using a somewhat "hacky" solution, combing re-focus of field together with disable animation.
struct ChatView {
enum Field: String, Hashable {
case chat
}
#State var draft = ""
#FocusState var focusedField: Field?
var body: some View {
VStack {
// messages view omitted
TextField(
"Chat...",
text: $draft
)
.submitLabel(.send)
.onSubmit {
if !draft.isEmpty {
sendMessage()
// We just lost focus because "return" key was pressed
// we re-fucus
focusedField = .chat
}
}
Button("Send") {
sendMessage()
}
}
// Prevent hacky keyboard hide animation
.transaction { $0.animation = nil }
}
func sendMessage() {
// impl omitted, sending message `draft` using some dependency.
}
}

on ios16 the keyboard bounces back everytime when you click on send.

Related

Move to other Picker View tabs only after the user taps on a button in an alert view - SwiftUI

What I would like to be able to do is that when the user taps on a color tab, first present an alert view asking before actually moving to the other tab and if the user decides to Cancel it will not move to the other tab but if the user taps the OK button, the Picker will actually move to the selected tab.
How can I move to a different color tab only after the user taps on the YES button in the alert view?
struct ContentView: View {
#State private var favoriteColor = 0
#State private var showAlert = false
var body: some View {
VStack {
Picker("What is your favorite color?", selection: $favoriteColor) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.pickerStyle(.segmented)
.onChange(of: favoriteColor) { tag in
showAlert = true
}
.alert("By changing color also modifieds other things...change color?", isPresented: $showAlert) {
Button("YES") {
// move to selected color tab
}
Button("Cancel") {
// do nothing
}
}
}
}
}
SwiftUI is reactive world, means our code receives control post-factum... use UIKit representable instead for that...
... Or, here is a possible workaround with proxy binding (however not sure if behavior persists in all OS versions)
Tested with Xcode 13.4 / iOS 15.5
Main part:
var selected: Binding<Int> { // << proxy !!
Binding<Int>(
get: { favoriteColor },
set: {
toConfirm = $0 // store for verification !!
showAlert = true // show alert !!
}
)
}
var body: some View {
VStack {
// << bind to proxy binding here !!
Picker("What is your favorite color?", selection: selected) {
Text("Red").tag(0)
Text("Green").tag(1)
}
.pickerStyle(.segmented)
.alert("By changing color also modifieds other things...change color?", isPresented: $showAlert, presenting: toConfirm) { color in // << inject for verification !!
Button("YES") {
favoriteColor = color // << do verification here !!
}
Button("Cancel") {
// do nothing
}
}
}
Test code on GitHub

SwiftUI #FocusState - how to give it initial value

I am excited to see the TextField enhancement: focused(...): https://developer.apple.com/documentation/swiftui/view/focused(_:)
I want to use it to show a very simple SwitfUI view that contains only one TextField that has the focus with keyboard open immediately. Not able to get it work:
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
.onAppear {
isFocused = true
}
// ...
Anything wrong? I have trouble to give it default value.
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
The theory I have behind the bug is that at the time of onAppear the TextField is not ready to become first responder, so isFocused = true and iOS calls becomeFirstResponder behind the scenes, but it fails (ex. the view hierarchy is not yet done setting up).
struct MyView: View {
#State var text: String
#FocusState private var isFocused: Bool
var body: some View {
Form {
TextEditor(text: $text)
.focused($isFocused)
.onChange(of: isFocused) { isFocused in
// this will get called after the delay
}
.onAppear {
// key part: delay setting isFocused until after some-internal-iOS setup
DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
isFocused = true
}
}
}
}
}
I was also not able to get this work on Xcode 13, beta 5. To fix, I delayed the call to isFocused = true. That worked!
It also works without delay.
DispatchQueue.main.async {
isFocused = true
}
//This work in iOS 15.You can try it.
struct ContentView: View {
#FocusState private var isFocused: Bool
#State private var username = "Test"
var body: some View {
VStack {
TextField("Enter your username", text: $username)
.focused($isFocused).onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isFocused = true
}
}
}
}
}
I've had success adding the onAppear to the outermost view (in your case NavigationView):
struct EditTextView: View {
#FocusState private var isFocused: Bool
#State private var name = "test"
// ...
var body: some View {
NavigationView {
VStack {
HStack {
TextField("Enter your name", text: $name).focused($isFocused)
}
}
}
.onAppear {
isFocused = true
}
}
// ...
I’m not certain but perhaps your onAppear attached to the TextField isn’t running. I would suggest adding a print inside of the onAppear to confirm the code is executing.
I faced the same problem and had the idea to solve it by embedding a UIViewController so could use viewDidAppear. Here is a working example:
import SwiftUI
import UIKit
struct FocusTestView : View {
#State var presented = false
var body: some View {
Button("Click Me") {
presented = true
}
.sheet(isPresented: $presented) {
LoginForm()
}
}
}
struct LoginForm : View {
enum Field: Hashable {
case usernameField
case passwordField
}
#State private var username = ""
#State private var password = ""
#FocusState private var focusedField: Field?
var body: some View {
Form {
TextField("Username", text: $username)
.focused($focusedField, equals: .usernameField)
SecureField("Password", text: $password)
.focused($focusedField, equals: .passwordField)
Button("Sign In") {
if username.isEmpty {
focusedField = .usernameField
} else if password.isEmpty {
focusedField = .passwordField
} else {
// handleLogin(username, password)
}
}
}
.uiKitOnAppear {
focusedField = .usernameField
// If your form appears multiple times you might want to check other values before setting the focus.
}
}
}
struct UIKitAppear: UIViewControllerRepresentable {
let action: () -> Void
func makeUIViewController(context: Context) -> UIAppearViewController {
let vc = UIAppearViewController()
vc.action = action
return vc
}
func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
}
}
class UIAppearViewController: UIViewController {
var action: () -> Void = {}
override func viewDidLoad() {
view.addSubview(UILabel())
}
override func viewDidAppear(_ animated: Bool) {
// had to delay the action to make it work.
DispatchQueue.main.asyncAfter(deadline:.now()) { [weak self] in
self?.action()
}
}
}
public extension View {
func uiKitOnAppear(_ perform: #escaping () -> Void) -> some View {
self.background(UIKitAppear(action: perform))
}
}
UIKitAppear was taken from this dev forum post, modified with dispatch async to call the action. LoginForm is from the docs on FocusState with the uiKitOnAppear modifier added to set the initial focus state.
It could perhaps be improved by using a first responder method of the VC rather than the didAppear, then perhaps the dispatch async could be avoided.

Swift UI need to keep both NavigationLink to detail view and Tap gesture recognizer

I am trying a simple app that is a List with items, they lead to detail view. I also have a search bar that opens keyboard, and I need to hide the keyboard when the user taps anywhere outside of the keyboard.
#State private var keyboardOpen: Bool = false
var body: some View {
NavigationView {
Form {
Section {
TextField("Search", text: $cityStore.searchTerm, onCommit: debouncedFetch)
.keyboardType(.namePhonePad)
.disableAutocorrection(true)
.onTapGesture { self.keyboardOpen = true }
.onDisappear { self.keyboardOpen = false }
}
Section {
List {
ForEach(cities) { city in
NavigationLink(
destination: DetailView(city: city)) {
VStack(alignment: .leading) {
Text("\(city.name)")
}
}
}
}
}
}
.navigationBarTitle("City list")
.onTapGesture {
if self.keyboardOpen {
UIApplication.shared.endEditing()
self.keyboardOpen = false
}
}
}
}
Do you know if it's possible to keep both gesture tap and follow to detail view?
Actually it should work, but it is not due to bug of .all GestureMask. I submitted feedback to Apple #FB7672055, and recommend to do the same for everybody affected, the more the better.
Meanwhile, here is possible alternate approach/workaround to achieve similar effect.
Tested with Xcode 11.4 / iOS 13.4
extension UIApplication { // just helper extension
static func endEditing() {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
}
}
struct TestEndEditingOnNavigate: View {
#State private var cities = ["London", "Berlin", "New York"]
#State private var searchTerm = ""
#State private var tappedLink: String? = nil // track tapped link
var body: some View {
NavigationView {
Form {
Section {
TextField("Search", text: $searchTerm)
}
Section {
List {
ForEach(cities, id: \.self) { city in
self.link(for: city) // decompose for simplicity
}
}
}
}
.navigationBarTitle("City list")
}
}
private func link(for city: String) -> some View {
let selection = Binding(get: { self.tappedLink }, // proxy bindng to inject...
set: {
UIApplication.endEditing() // ... side effect on willSet
self.tappedLink = $0
})
return NavigationLink(destination: Text("city: \(city)"), tag: city, selection: selection) {
Text("\(city)")
}
}
}
I think you could easily handle this scenario with boolean flags, when your keyboard opens you can set a flag as true and when it dismisses a the flag goes back to false, so in that case when the keyboard is open and the tap gesture is triggered you can check if the keyboard flag is active and not go to detail but instead effectively dismiss the keyboard and viceversa. Let me know if maybe I misunderstood you.

How to check validation of TextField on Button Tap (not by using NavigationLink) in SwiftUI?

I want to validate that my TextField "username" and TextField "password" are not empty before pushing to next screen. Is it possible in SwiftUI?
Example I do this in Swift:-
#IBAction func btnSignInTapped(_ sender: UIButton) {
if (String.isStringEmpty(aString: getString(anything: tfUserName.text))) {
//Error message
return
} else if (String.isStringEmpty(aString: getString(anything: tfPassword.text))) {
//Error message
return
} else {
//Push screen
}
}
Similarly how can I do that in SwiftUI?
It's easy to do once you embrace the "declarative" nature of SwiftUI. Here's a Swift playground showing a very barebones approach:
import SwiftUI
import PlaygroundSupport
struct ContentView : View {
#State private var email: String = ""
#State private var password: String = ""
private var validated: Bool {
!email.isEmpty && !password.isEmpty
}
var body: some View {
Group {
TextField("email", text: $email)
TextField("password", text: $password)
if validated {
Button("Login") {
print("Logging in")
}
}
}
}
}
PlaygroundPage.current.liveView = UIHostingController(rootView: ContentView())
Basically you don't work on the TextField but on the #State and instruct the UI to show the button only when the conditions are met.
Of course, if the validation is more complex than what we have here you may want to use a View-Model object instead.

textFieldDidBeginEditing and textFieldDidEndEditing in SwiftUI

how can I use the methods textFieldDidBeginEditing and textFieldDidEndEditing with the default TextField struct by apple.
TextField has onEditingChanged and onCommit callbacks.
For example:
#State var text = ""
#State var text2 = "default"
var body: some View {
VStack {
TextField($text, placeholder: nil, onEditingChanged: { (changed) in
self.text2 = "Editing Changed"
}) {
self.text2 = "Editing Commited"
}
Text(text2)
}
}
The code in onEditingChanged is only called when the user selects the textField, and onCommit is only called when return, done, etc. is tapped.
Edit: When the user changes from one TextField to another, the previously selected TextField's onEditingChanged is called once, with changed (the parameter) equaling false, and the just-selected TextField's onEditingChanged is also called, but with the parameter equaling true. The onCommit callback is not called for the previously selected TextField.
Edit 2:
Adding an example for if you want to call a function committed() when a user taps return or changes TextField, and changed() when the user taps the TextField:
#State var text = ""
var body: some View {
VStack {
TextField($text, placeholder: nil, onEditingChanged: { (changed) in
if changed {
self.changed()
} else {
self.committed()
}
}) {
self.committed()
}
}
}
SwiftUI 3 (iOS 15+)
Since TextField.init(_text:onEditingChanged:) is scheduled for deprecation in a future version it may be best to use #FocusState. This method also has the added benefit of knowing when the TextField is no longer the "first responder" which .onChange(of:) and .onSubmit(of:) alone will not do.
#State private var text = ""
#FocusState private var isTextFieldFocused: Bool
var body: some View {
TextField("Text Field", text: $text)
.focused($isTextFieldFocused)
.onChange(of: isTextFieldFocused) { isFocused in
if isFocused {
// began editing...
} else {
// ended editing...
}
}
}
SwiftUI 2
ios15 and above
The syntax has changed for swiftui2
a modifier onChange is triggered when a property value has changed and onSubmit is triggered when form is submitted ie you press enter
TextField("search", text: $searchQuery)
.onChange(of: searchQuery){ newValue in
print("textChanged")
}
.onSubmit {
print("textSubmitted")
}

Resources