Sean
Sean

Reputation: 3095

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.

Upvotes: 24

Views: 12883

Answers (5)

kgaidis
kgaidis

Reputation: 15579

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
          }
        }
    }
  }
}

Upvotes: 18

malhal
malhal

Reputation: 30561

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.

Upvotes: 0

kent robert
kent robert

Reputation: 29

//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
                        }
                    }
            }
        }
    }

Upvotes: 2

schornon
schornon

Reputation: 382

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
}

Upvotes: 4

arcyn1c
arcyn1c

Reputation: 31

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.

Upvotes: 3

Related Questions