Nick K9
Nick K9

Reputation: 4718

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.

Upvotes: 1

Views: 111

Answers (3)

Nick K9
Nick K9

Reputation: 4718

Thanks to workingdog for the answer. Here's the code I used to solve the problem:

struct ContentView: View {
    @State private var sheetOpen: Bool = false
    @State private var field1: String = ""
    @State private var roadName: String = ""
    @State private var addNewRoad: Bool = false

    var body: some View {
        Button(action: {
            addNewRoad = false
            sheetOpen = true
            field1 = "test" // <-- Remove text here to focus field1. Otherwise, roadName will be focused
            roadName = ""
        }) {
            Text("Open Sheet")
        }
        .sheet(isPresented: $sheetOpen, onDismiss: {
            if addNewRoad {
                print("Add road with name \(roadName)")
            }
        }) {
            AddRoadSheetView(sheetOpen: $sheetOpen, field1: $field1, roadName: $roadName, addNewRoad: $addNewRoad)
        }
        .frame(width: 100, height: 50)
    }
}

struct AddRoadSheetView: View {
    @Binding var sheetOpen: Bool
    @Binding var field1: String
    @Binding var roadName: String
    @Binding var addNewRoad: Bool

    @FocusState private var focusRoadNameField
    
    var body: some View {
        VStack {
            TextField("Conditional focus", text: $field1)
            
            TextField("Road Name", text: $roadName)
                .focused($focusRoadNameField)
            
            HStack {
                Button("Cancel", action: {
                    sheetOpen = false
                })
                .keyboardShortcut(.cancelAction)
                .padding()
                
                Spacer()
                
                Button("Add Road", action: {
                    addNewRoad = true
                    sheetOpen = false
                })
                .keyboardShortcut(.defaultAction)
                .disabled(roadName.isEmpty)
                .padding()
            }
        }
        .frame(width: 260)
        .padding()
        .onAppear {
            focusRoadNameField = !field1.isEmpty // Choose whether the Road Name field has focus
        }
    }
}

Upvotes: 0

malhal
malhal

Reputation: 30746

From what I’ve read and my own experiments @FocusState is currently broken however for a TextField to be initially focused we are supposed to use .defaultFocus():

https://developer.apple.com/documentation/swiftui/view/defaultfocus(_:_:priority:)

Example: https://developer.apple.com/videos/play/wwdc2023/10162?time=773

struct GroceryListView: View {
    @State private var list = GroceryList.examples
    @FocusState private var focusedItem: GroceryList.Item.ID?

    var body: some View {
        NavigationStack {
            List($list.items) { $item in
                HStack {
                    Toggle("Obtained", isOn: $item.isObtained)
                    TextField("Item Name", text: $item.name)
                        .onSubmit { addEmptyItem() }
                        .focused($focusedItem, equals: item.id)
                }
            }
            .defaultFocus($focusedItem, list.items.last?.id)
            .toggleStyle(.checklist)
        }
        .toolbar {
            Button(action: addEmptyItem) {
                Label("New Item", systemImage: "plus")
            }
        }
    }

    private func addEmptyItem() {
        let newItem = list.addItem()
        focusedItem = newItem.id
    }
}

Upvotes: 0

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

Upvotes: 0

Related Questions