Search code examples
swiftswiftui

SwiftUI: use @FocusState.Binding with view initializer


I have a view with a @FocusState that I want to pass into a subview. For the subview, I use @FocusState.Binding. This works fine if I don't use a custom initializer ... but I can't figure out the syntax for how it works with a custom view initializer, which I need for other reasons.

Here's the code that works:

struct TestListButtonsAndListFocusView: View {
    
    @FocusState var buttonFocusState: ButtonState?
    @ObservedObject var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        TestListView(viewModel: viewModel, bindedFocusButtonState: $buttonFocusState) // for custom init, replace with following line
        //TestListView(viewModel: viewModel, focusButtonState: $buttonFocusState) // 1. Uncomment this for custom initializer
    }
}

struct TestListView: View {
    @State private var items1: [TimedItem] = [
        TimedItem(number: 1, timestamp: "2024-11-20 10:00"),
        TimedItem(number: 2, timestamp: "2024-11-20 11:00"),
        TimedItem(number: 3, timestamp: "2024-11-20 12:00")
    ]
    
    @ObservedObject var viewModel: ViewModel
    @FocusState.Binding var bindedFocusButtonState: ButtonState?
    
    @State var selectedItem: TimedItem.ID?
    
    var body: some View {
        List(items1, selection: $selectedItem) { item in
            ContentListItemView(item: item)
        }
        .onChange(of: bindedFocusButtonState) {
            if let bindedFocusButtonState {
                print("TestListView - bindedListFocusState has changed to \(bindedFocusButtonState)")
                if bindedFocusButtonState == .one && selectedItem == nil {
                    selectedItem = items1.first?.id
                }
            } else {
                print("ListOneView - bindedListFocusState has changed to nil")
            }
        }
    }
    
    // 2. Uncomment this
    /*init(viewModel: ViewModel, focusButtonState: FocusState<ButtonState?>.Binding) {
        // how to pass in @FocusState.Binding?
        self.viewModel = viewModel
        self.bindedFocusButtonState = focusButtonState
        // Error: Cannot assign value of type 'FocusState<ButtonState?>.Binding' to type 'ButtonState?'
    }*/
}

public class ViewModel: NSObject, ObservableObject {
    
    @Published public var selectedButton: ButtonState? = ButtonState.one
}

public enum ButtonState: Int, CaseIterable, Hashable, Identifiable {
    
    public var id: Self {
        return self
    }
    case one, two, three
}

    
#Preview {
    TestListButtonsAndListFocusView()
}

However, if I uncomment out the line for the custom initializer, and then the line in TestListButtonsAndListFocusView to use the custom initializer, the syntax is wrong and I get the error:

Error: Cannot assign value of type 'FocusState<ButtonState?>.Binding' to type 'ButtonState?'

I'm not sure how to then initialize the @FocusState.Binding this way. I know it works if I use var bindedFocusButtonState: FocusState<ContactTabStyle?>.Binding instead, and then use that in the initializer as well. But I'd really like to figure out how to use the new @FocusState.Binding with the custom initializer, as it avoids having to access wrappedValue and is easier to observe with onChange


Solution

  • You should assign to the underscore-prefixed property _bindedFocusButtonState.

    self._bindedFocusButtonState = focusButtonState
    

    This is the property actually containing the FocusState.Binding. bindedFocusButtonState on the other hand, is actually a computed property that returns _bindedFocusButtonState.wrappedValue.

    In particular, the line

    @FocusState.Binding var bindedFocusButtonState: ButtonState?
    

    declares three properties:

    private var _bindedFocusButtonState: FocusState<ButtonState?>.Binding
    
    var bindedFocusButtonState: ButtonState? {
        get { _bindedFocusButtonState.wrappedValue }
        set { _bindedFocusButtonState.wrappedValue = newValue }
    }
    
    var $bindedFocusButtonState: FocusState<ButtonState?>.Binding {
        get { _bindedFocusButtonState.projectedValue }
    }