kittonian
kittonian

Reputation: 1431

SwiftUI - Updating FocusState from onChange

I need to set the FocusState to nil (or if I have to, another value) in an onChange modifier of a Picker in a macOS app (build for 13.3).

This should be so simple, but the code below does not work. The console output of the print isFocused statement shows the current isFocused value instead of showing nil. You can test this by clicking into one of the text fields and then changing the value of the picker to get the print to console output.

I have tried wrapping the isFocused = nil call in a DispatchQueue.main.async { isFocused = nil }, I have tried adding delays, booleans, etc. Nothing works and I'm at a loss.

When the picker value changes, isFocused must be nil before the other code (not shown) that is within the onChange modifier is run. Hoping I just overlooked something simple.

struct MyTestView: View {

    enum Field: Hashable {
        case field1
        case field2
    }
    
    @State private var myArray = ["One", "Two", "Three"]
    @State private var myPickerVar: String? = "One"
    @FocusState var isFocused: MyView.Field?

    var body: some View {

        VStack {
            Picker(selection: $myPickerVar.animation()) {
               ForEach(myArray, id: \.self) { option in
                   Text(option)
                       .tag(Optional(option))
               }
            } label: {
                Text("My Picker")
            }
            .onChange(of: myPickerVar ?? "") { option in
                isFocused = nil
                print("Focused: \(String(describing: isFocused))")
            }

            TextField("", $myTextVar)
                .focused($isFocused, equals: .field1)

            TextField("", $myTextVar2)
                .focused($isFocused, equals: .field2)
        }
    }
}

Upvotes: 1

Views: 1238

Answers (1)

Sweeper
Sweeper

Reputation: 273540

As the documentation for FocusState says:

When focus enters the modified view, the wrapped value of this property updates to match a given prototype value. Similarly, when focus leaves, the wrapped value of this property resets to nil or false. Setting the property’s value programmatically has the reverse effect, causing focus to move to the view associated with the updated value.

The value of isFocused only changes to nil when focus leaves. It is reasonable to believe that the focus does not immediately leave as soon as you set it to nil. Some time is needed for the views to update. Also, FocusState is a property wrapper, so it is entirely possible for its setter to not update the wrapped value immediately.

The setter of a property wrapped by a property wrapper can even do nothing at all, or run any arbitrary code instead.

@propertyWrapper
struct SetterDoesNothing<T> {
    private var value: T
    init(wrappedValue: T) { value = wrappedValue }
    var wrappedValue: T {
        get { value }
        set { /* do nothing! */ }
    }
}

Instead of wrapping the assignment with DispatchQueue.main.async, wrap whatever code you have below instead.

isFocused = nil
DispatchQueue.main.async {
    // this prints nil
    print("Focused: \(String(describing: isFocused))")
}

Alternatively, detect the change of isFocused with onChange too:

.onChange(of: isFocused) { newValue in
    if newValue == nil {
        // do your things...
    }
}

Upvotes: 1

Related Questions