Prevent keyboard for TextField from dismissing after a [Return] but need to detect a [Return] keypress too

842 Views Asked by At

In short, I’m trying to achieve the Reminders.app interaction when you add an entry, hit [Return] and can then add the next entry immediately (i.e. keyboard does not dismiss, but only does if you hit [Return] on an empty line).

This would be straightforward if it was not for this issue: how can I keep the keyboard opened with @FocusState with SwiftUI without a bounce? (keyboard bounces after a [Return]).

I considered many solutions but the cleanest to keep the keyboard up is to leverage introspect:

class TextFieldKeyboardBehavior: UIView, UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        return false
    }
}
struct TestView: View {
    
    @State var text: String = ""

    var textFieldKeyboardBehavior = TextFieldKeyboardBehavior()
    
    var body: some View {
        VStack {
            TextField("Title", text: $text)
                .introspect(.textField, on: .iOS(.v17)) {
                  $0.delegate = textFieldKeyboardBehavior
                }
                .onSubmit {
                   // code
                }
        }
    }
}

Now the (big!) problem with this approach is that the onSubmit never gets called. If I add an onChange modifier, it is called on every press, but I don’t believe observing value changes here can reliably detect when is the correct moment to actually dismiss the keyboard. I understand I’m effectively disabling [Return] capabilities and so the behaviour should come as no surprise, but then I’m wondering whether there’s another approach leveraging introspect combined with a TextField that enables what I’m after: 1) keyboard doesn’t dismiss on a [Return], and 2) [Return] is always detected.

3

There are 3 best solutions below

1
TimD On

Here's what I come up with using @FocusState. Enter text in the TextField and hit return. The .onSubmit() will add another TextField and shift its focus without dismissing the keyboard. The keyboard will dismiss when hitting return if the TextField is empty.

import SwiftUI

struct TextView: View {
    @State private var textFields: [String] = [""]
    @FocusState private var focusField: Int?
    
    var body: some View {
        VStack {
            ForEach(0..<textFields.count, id: \.self) { index in
                TextField("Enter text", text: $textFields[index])
                    .padding()
                    .focused($focusField, equals: index)
                    .onSubmit {
                        if !textFields[index].isEmpty {
                            textFields.append("")
                            focusField = textFields.count - 1
                        }
                    }
            }
        }
    }
}
0
Orackle On

You would have to manually pass your action to the new delegate. .onSubmit does not work because you replace the original delegate with your own

0
Dean L On

I work around this by passing whatever action I would usually execute in .onSubmit to the UITextFieldDelegate:

class TextFieldDelegate: NSObject, UITextFieldDelegate {
    
    var shouldReturn: (() -> Bool)?
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        
        if let shouldReturn = shouldReturn {
            return shouldReturn()
        }
        else {
            return true
        }
    }
}

Then in our SwiftUI view:

import Introspect

struct TestView: View {
    
    enum FocusedField {
        case username, password
    }
    
    @State private var username: String = ""
    @State private var password: String = ""
    
    var usernameFieldDelegate = TextFieldDelegate()
    var passwordFieldDelegate = TextFieldDelegate()
    
    @FocusState private var focusedField: FocusedField?
    
    var body: some View {
        
        VStack {
            
            TextField(text: $username)
                .focused($focusedField, equals: .username)
                .introspectTextField(customize: { textField in
                    
                    usernameFieldDelegate.shouldReturn = {
                        
                        if usernameIsValid() {
                            focusedField = .password
                        }
                        
                        return false
                    }
                    
                    textField.delegate = usernameFieldDelegate
                })
            
            SecureField(text: $password)
                .focused($focusedField, equals: .password)
                .introspectTextField(customize: { textField in
                    
                    passwordFieldDelegate.shouldReturn = {
                        validateAndProceed()
                        
                        return false
                    }
                    
                    textField.delegate = passwordFieldDelegate
                })
        }
    }
    
    func usernameIsValid() -> Bool {
        return true
    }
    
    func validateAndProceed() {}
}