1

I have a SwiftUI view with multiple TextFields and a @FocusState bound to my view model. I want the first field to automatically get focus whenever the view appears(when coming back from a SecondView). Here’s a minimal example:

import SwiftUI import Combine

class MyViewModel: ObservableObject {
    enum Field: Hashable {
        case field1
        case field2
        case field3
    }
    
    @Published var field1: String = ""
    @Published var field2: String = ""
    @Published var field3: String = ""
    
    @Published var focusedField: Field? = nil
}

struct ContentView: View {
    @StateObject private var viewModel: MyViewModel
    @FocusState private var activeField: MyViewModel.Field?
    
    init(viewModel: MyViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 20) {
                TextField("Field 1", text: $viewModel.field1)
                    .focused($activeField, equals: .field1)
                    .textFieldStyle(.roundedBorder)
                
                TextField("Field 2", text: $viewModel.field2)
                    .focused($activeField, equals: .field2)
                    .textFieldStyle(.roundedBorder)
                
                TextField("Field 3", text: $viewModel.field3)
                    .focused($activeField, equals: .field3)
                    .textFieldStyle(.roundedBorder)
                
                NavigationLink("Go to Second View") {
                    SecondView()
                }
                .padding(.top, 40)
            }
            .padding()
            .onAppear {
                // Attempt to focus the first field
                viewModel.focusedField = .field1
                activeField = viewModel.focusedField
            }
            .onChange(of: activeField) { _, newValue in
                viewModel.focusedField = newValue
            }
            .onChange(of: viewModel.focusedField) { _, newValue in
                activeField = newValue
            }
        }
    }
}

struct SecondView: View {
    var body: some View {
        VStack {
            Text("Second View")
                .font(.title)
            Text("Go back to see focus issue")
                .padding()
        }
    }
}

What am I doing wrong?

2
  • How about adding an .onDisappear to SecondView()? Commented Nov 20 at 10:51
  • Hmmm but, I don't want to be dependent to SecondView just to solve thing in a first view @BenzyNeez Commented Nov 20 at 10:55

2 Answers 2

2

Let's look at this code:

import SwiftUI
import Combine

class MyViewModel: ObservableObject {
    enum Field: Hashable {
        case field1
        case field2
        case field3
    }
    
    @Published var field1: String = ""
    @Published var field2: String = ""
    @Published var field3: String = ""
    
    @Published var focusedField: Field? = nil
}

struct ContentView: View {
    @StateObject
    private var viewModel: MyViewModel
    
    @FocusState
    private var activeField: MyViewModel.Field?
    
    @FocusState private var isField1Focused: Bool
    init(viewModel: MyViewModel) {
        self._viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        NavigationStack {
            let _ = Self._printChanges()
            VStack(spacing: 20) {
                TextField("Field 1", text: $viewModel.field1)
                    .focused($activeField, equals: .field1)
                    .focused($isField1Focused)
                    .textFieldStyle(.roundedBorder)
                
                TextField("Field 2", text: $viewModel.field2)
                    .focused($activeField, equals: .field2)
                    .textFieldStyle(.roundedBorder)
                
                TextField("Field 3", text: $viewModel.field3)
                    .focused($activeField, equals: .field3)
                    .textFieldStyle(.roundedBorder)
                
                NavigationLink("Go to Second View") {
                    return SecondView().onDisappear() {
                        debugPrint("2: then this disapears with activeField: \(activeField.debugDescription)")
                        activeField = .field1 /*< hacky but this work */
                    }
                }
                .padding(.top, 40)
            }
            .padding()
            .onAppear {
                debugPrint("1: this first calls with activeField: \(activeField.debugDescription)")
                activeField = .field1 /*< this does not work */
            }
        }
    }
}

struct SecondView: View {
    var body: some View {
        VStack {
            Text("Second View")
                .font(.title)
            Text("Go back to see focus issue")
                .padding()
        }
    }
}

The console outputs when we returns to the first page:

"1: this first calls with activeField: nil" ContentView: _viewModel changed.

"2: then this disapears with activeField: Optional(focus.MyViewModel.Field.field3)" ContentView: _viewModel changed.

That means the activeField is set to nil, before the 2nd page is actually disappeared.

SwiftUI just restores the focus to the field3(I focused on field3 before entering 2nd page). So anything we do in the onAppear of the 1st page is not working unless we restore the focus after a delay.

So Benzy is right, put the focus restoration in the 2nd view's disappear is a smart move.

In my opinion, that code is not so ugly, the code is still in the body tree of the first page. You can still have a clean 2nd view, right?

Sign up to request clarification or add additional context in comments.

9 Comments

Hi... That is not a real code. Just minimal reproducible example. In second view, I don't have a reference to that exact view model, and don't do navigation in this way (separate class is used for navigation)
look at the code, we are not using any view model to update the focused view. Just using active field. And I validate, it works
Yes I understand, but you are using return SecondView().onDisappear() {... I can't use that in real code. As I said, navigation is done using other class...
Okay, what is your real code look like. Just know SwiftUI will try to restore the old focus state on return. Either set a delay to set new focus or do it on 2nd view’s on disappear.
I mean, the code is too complex to share — and I can’t really share it anyway. But the point is that this reproduces the issue, and honestly I don’t want to include a second view in this fix. Thanks for the hints, though. What worked, is to set default value in onDissapear in first view. So, again thanks for the hints. That was useful
Agree with you, SwiftUI has so much hidden features. I just learned that it will auto restore the previous focus state. :D
You'll have better luck if you remove the view model and learn Binding
Its not my code really. I don't use view models when I don't have to
Did you get permission to share someone else's code?
1

One of the reasons why the code is not working seems to be the way the .onChange handlers are propagating the changes. Start by changing these, so that a change is only applied if necessary:

.onChange(of: activeField) { _, newValue in
    if viewModel.focusedField != newValue {
        viewModel.focusedField = newValue
    }
}
.onChange(of: viewModel.focusedField) { _, newValue in
    if activeField != newValue {
        activeField = newValue
    }
}

Then, the update to the FocusState variable works if it is performed after the navigation animation has completed. So ways to get it working are as follows:

1. Add an .onDisappear callback to to SecondView()

NavigationLink("Go to Second View") {
    SecondView()
        .onDisappear {
            // Attempt to focus the first field
            viewModel.focusedField = .field1
            activeField = viewModel.focusedField
        }
}

However, you said in a comment that you didn't like being dependent on SecondView just to solve something in the first view. So another way is...

2. Perform the update with animation

Performing the update withAnimation also seems to provide the necessary delay:

.onAppear {
    // Attempt to focus the first field
    withAnimation {
        viewModel.focusedField = .field1
        activeField = viewModel.focusedField
    }
}

Alternatively...

3. Perform the update using .task

Another way to perform the update is to replace the .onAppear with a .task and include a short wait:

.task { // 👈 replaces .onAppear
    // Attempt to focus the first field
    try? await Task.sleep(for: .seconds(0.5))
    viewModel.focusedField = .field1
    activeField = viewModel.focusedField
}

2 Comments

I will try with withAnimation... If it doesn't change things visually too much, maybe it can be a solution. i can't believe that SwiftUI has flaws like this these days.
I ended up reseting focus value in onDissapear. Thanks for the hints for onChange, that was a good catch!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.