19

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.

Sean
  • 2,967
  • 2
  • 29
  • 39

5 Answers5

15

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.7) {
            isFocused = true
          }
        }
    }
  }
}
kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • 1
    Any reason why the delay is half a second? I tried smaller delays--it stopped working at about 0.05. – John Sorensen Nov 14 '21 at 17:58
  • 1
    @JohnSorensen The 0.5 seconds was picked as a value large enough to avoid cases where this 'hack' would break (ex. maybe delay increases for some reason due to age of device or software changes), yet small enough where it does not have large user experience impact. Given that all iOS code is not open sourced, it's all speculation anyway. – kgaidis Nov 14 '21 at 18:09
  • 2
    This works, but I doesn't like the hack as things like this usually break sometime... – G. Marc Nov 28 '21 at 07:36
  • 12
    I have a case where the textfield is on a sheet. The delay of 0.5 doesn't work for that case I had to raise it to 0.7. That's most certainly because of the animation. Isn't there a better handler than "onAppear" which is triggered AFTER the view is finished animating? – G. Marc Nov 28 '21 at 07:46
  • This approach works indeed, but creates a memory leak, at least in sheet view. Basically, every time you come to a view with asyncAfter focus, it will keep spawning AccessibilityFocusInputKey objects in memory and keep them there indefinitely. Furthermore, it will also retain any other views on the same page as focus in memory. – NeverwinterMoon Aug 16 '22 at 14:04
  • On iOS 16, when implementing on my project, it worked without any delay. Seems like the onAppear focus problem is related to <16 iOS versions. – liudasbar Mar 09 '23 at 00:29
3

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.

arcyn1c
  • 31
  • 3
3

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
}
schornon
  • 342
  • 3
  • 4
2
//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
                        }
                    }
            }
        }
    }
kent robert
  • 29
  • 1
  • 1
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 21 '22 at 13:10
0

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.

malhal
  • 26,330
  • 7
  • 115
  • 133