macosswiftuifocus

Text field not being focused in SwiftUI sheet on macOS


I'm using SwfitUI with macOS. I have two text fields in a sheet, a Picker and a TextField. I want the second TextField to have focus when a boolean is set if the Picker only has a single element (because there's nothing to choose). Below, please find simplified code where I'm always setting focusRoadNameField to true. I'm then using this @FocusState variable with focused() on the TextField—but the picker is still always being focused when the sheet opens.

import AppKit
import SwiftUI

struct ContentView: View {
    @State private var sheetOpen: Bool = false
    @State private var roadName: String = ""
    @State private var noFocus: String = ""
    
    @FocusState private var focusRoadNameField: Bool

    var body: some View {
        Button(action: {
            sheetOpen = true
            roadName = ""
            
            // And yet the field isn't focused
            focusRoadNameField = true
        }) {
            Text("Open Sheet")
        }
        .sheet(isPresented: $sheetOpen, content: {
            VStack {
                TextField("No focus", text: $noFocus)

                TextField("Road Name", text: $roadName)
                    .focused($focusRoadNameField)
                
                HStack {
                    Button("Cancel", action: {
                        sheetOpen = false
                    }).keyboardShortcut(.cancelAction)
                    .padding()

                    Spacer()

                    Button("Add Road", action: {
                        print("=> Add road '\(roadName)'")
                        sheetOpen = false
                    }).keyboardShortcut(.defaultAction)
                    .padding()
                }
            }
            .frame(width: 260)
            .padding()
        })
        .frame(width: 100, height: 50)
    }
}

#Preview {
    ContentView()
}

Update: Thanks to Sweeper for the comment. I forgot that I am using Full Keyboard Access, so widgets like the Picker get keyboard focus with a focus ring. I've adjusted the example to use two text fields to show the buggy behavior even on systems which don't have Full Keyboard Access enabled, which is the default state.

Now, you will see the sheet always open with the first, "No focus" field selected, despite the second field having the .focused() modifier.


Solution

  • Sheets are modal, and I found that passing @FocusState can be tricky in iOS18+.

    You could try this simple approach using two local @FocusState and a basic @State private var focus: Field? to pass the focus info to the sheet view. Note capturing the vars for use in the sheet, and the .onAppear.

    Example code:

    enum Field {
        case noFocus
        case roadName
    }
    
    struct ContentView: View {
        @State private var sheetOpen: Bool = false
        @State private var roadName: String = "some road"
        @State private var noFocus: String = "something"
        @State private var text: String = "test"
        
        @State private var focus: Field? // <--- to pass to sheet
        @FocusState private var focusField: Field?  // <--- local to this view
    
        
        var body: some View {
            VStack {
                Button("focus on roadName"){
                    focus = .roadName // <--- here
                    sheetOpen = true
                }
                Button("focus on noFocus"){
                    focus = .noFocus // <--- here
                    sheetOpen = true
                }
                .sheet(isPresented: $sheetOpen) { [$roadName, $noFocus, focus] in  // <--- here
                    MidView(sheetOpen: $sheetOpen,
                            roadName: $roadName,
                            noFocus: $noFocus,
                            focus: focus)
                }
            }
        }
    }
    
    struct MidView: View {
        @Binding var sheetOpen: Bool
        @Binding var roadName: String
        @Binding var noFocus: String
    
        var focus: Field?  // <--- here
        @FocusState private var focusField: Field? // <--- local to this view
    
        
        var body: some View {
            VStack {
                TextField("No focus", text: $noFocus)
                    .focused($focusField, equals: .noFocus)
    
                TextField("Road Name", text: $roadName)
                    .focused($focusField, equals: .roadName)
                
                HStack {
                    Button("Cancel", action: {
                        sheetOpen = false
                    }).keyboardShortcut(.cancelAction)
                    .padding()
    
                    Spacer()
    
                    Button("Add Road", action: {
                        print("----> Add road '\(roadName)'")
                        sheetOpen = false
                    }).keyboardShortcut(.defaultAction)
                    .padding()
                }
            }
            .frame(width: 260)
            .padding()
            .onAppear {
                focusField = focus  // <--- here
            }
        }
    }