最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swiftui - Passing 'focus' between sibling views - Stack Overflow

programmeradmin1浏览0评论

I am trying to construct a view where they are multiple buttons, and selecting each button can show a different List view. I want to be able to select the controls with just the keyboard, so if the 'focus' is on the buttons, then I can move left or right with the arrow keys, pressing the 'down' key should shift the focus to the List that is currently visible (and select the first row), and if the focus is on the list, then pressing 'up' should shift the focus to the buttons level. I have it working such that left-right arrow keys work for selecting between the buttons, which switches the List views as required, but the 'down' and 'up' keys don't seem to shift focus between the list and the selected button.

In my implementation, I kept 2 top-level @FocusStates, which are passed into subviews as bindings. I keep a view-model to hold the current selected button. I update the list's @FocusState.Binding in the button's key-handler, but it doesn't seem to have any effect. The 'list' doesn't get focused (unless I manually select it using the pointed). Also, if the 'list' is focused on the first row and I press the 'up' button, the handler updates the button focus state (through the @FocusState.Binding) but that doesn't seem to affect anything either.

This is the full code:

public enum ButtonTab: Int, CaseIterable, Hashable, Identifiable {
    
    public var id: Self {
        return self
    }
    public var buttonTitle: String {
        switch self {
            case .one: return "One"
            case .two: return "Two"
            case .three: return "Three"
        }
    }
    case one, two, three
}

public class ContentViewModel: NSObject, ObservableObject {
    
    @Published public var selectedButton: ButtonTab? = ButtonTab.one

    public func rightArrayKeyForSelectedTab(){
        DispatchQueue.main.async {
            switch self.selectedButton {
            case .one:
                self.selectedButton = .two
            case .two:
                self.selectedButton = .three
            case .three:
                self.selectedButton = nil
            case .none:
                self.selectedButton = nil
            }
        }
    }
    public func leftArrayKeyForSelectedTab(){
        DispatchQueue.main.async {
            switch self.selectedButton {
            case .one:
                self.selectedButton = nil
            case .two:
                self.selectedButton = .one
            case .three:
                self.selectedButton = .two
            case .none:
                self.selectedButton = nil
            }
        }
    }
}

struct ButtonTagsContentView: View {
    // top-level focus states
    @FocusState var buttonFocusState: ButtonTab?
    @FocusState var listFocusState: ButtonTab?
    
    @ObservedObject var viewModel: ContentViewModel = ContentViewModel()
    
    var body: some View {
        
        VStack {
            
            HStack {
                ButtonsView(viewModel: viewModel, bindedFocusButtonState: $buttonFocusState, bindedListFocusState: $listFocusState)
            }
            .padding()
            
            if viewModel.selectedButton == .one {
                ListOneView(viewModel: viewModel, bindedFocusButtonState: $buttonFocusState, bindedListFocusState: $listFocusState)
                    .focused($listFocusState, equals: .one)
            } else if viewModel.selectedButton == .two {
                ListTwoView(viewModel: viewModel, bindedFocusButtonState: $buttonFocusState, bindedListFocusState: $listFocusState)
                    .focused($listFocusState, equals: .two)
            } else if viewModel.selectedButton == .three {
                ListThreeView(viewModel: viewModel, bindedFocusButtonState: $buttonFocusState, bindedListFocusState: $listFocusState)
                    .focused($listFocusState, equals: .three)
            }
        }
        .onChange(of: listFocusState) {
            // This doesn't get called
            if let listFocusState {
                print("ContentView: listFocusChange called: \(listFocusState)")
            } else {
                print("ContentView: listFocusChange to nil")
            }
        }
    }
}

struct ButtonsView: View {
    
    @ObservedObject var viewModel: ContentViewModel
    @FocusState.Binding var bindedFocusButtonState: ButtonTab?
    @FocusState.Binding var bindedListFocusState: ButtonTab?
    
    var body: some View {
        HStack {
            ForEach(ButtonTab.allCases, id: \.self) { button in
                Button {
                    print("ButtonsView: \(button.buttonTitle) selected")
                    bindedFocusButtonState = button
                    viewModel.selectedButton = button
                } label: {
                    Text(button.buttonTitle)
                        .underline(viewModel.selectedButton == button)
                }
                .padding()
                .bold(viewModel.selectedButton == button)
                .focusable()
                .focused($bindedFocusButtonState, equals: button)
                //.focusEffectDisabled()
                .onKeyPress(keys: [.downArrow]) { press in
                    print("Down Arrow key pressed on Button - \(viewModel.selectedButton!.buttonTitle)")
                    bindedListFocusState = viewModel.selectedButton // doesn't seem to affect anything
                    bindedFocusButtonState = nil
                    return .handled
                }
                .onKeyPress(keys: [.rightArrow]) { press in
                    print("Right Arrow key pressed on Button")
                    viewModel.rightArrayKeyForSelectedTab()
                    return .handled
                }
                .onKeyPress(keys: [.leftArrow]) { press in
                    print("Left Arrow key pressed on Button")
                    viewModel.leftArrayKeyForSelectedTab()
                    return .handled
                }
            }
        }
        .onChange(of: bindedFocusButtonState) {
            if let value = bindedFocusButtonState {
                print("\(value) selected")
            }
        }
    }
}

struct ListOneView: 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: ContentViewModel
    @FocusState.Binding var bindedFocusButtonState: ButtonTab?
    @FocusState.Binding var bindedListFocusState: ButtonTab?
    
    @State var selectedItem: TimedItem.ID?
    
    var body: some View {
        List(items1, selection: $selectedItem) { item in
            ContentListItemView(item: item)
        }
        .onChange(of: bindedListFocusState) {
            if let bindedListFocusState {
                print("ListOneView - bindedListFocusState has changed to \(bindedListFocusState)")
            } else {
                print("ListOneView - bindedListFocusState has changed to nil")
            }
        }
        .onKeyPress(keys: [.upArrow]) { press in
            if let selectedItem, selectedItem == items1.first?.id {
                print("List1: first item selected, and up key pressed")
                bindedFocusButtonState = viewModel.selectedButton // doesn't work
                return .handled
            }
            
            return .ignored
        }
    }
}

struct ListTwoView: View {
    @State private var items: [TimedItem] = [
        TimedItem(number: 4, timestamp: "2025-01-20 10:00"),
        TimedItem(number: 5, timestamp: "2025-01-20 11:00"),
        TimedItem(number: 6, timestamp: "2025-01-20 12:00")
    ]
    
    @ObservedObject var viewModel: ContentViewModel
    @FocusState.Binding var bindedFocusButtonState: ButtonTab?
    @FocusState.Binding var bindedListFocusState: ButtonTab?
    
    @FocusState var listHasFocus: Bool
    @State var selectedItem: TimedItem.ID?
    
    var body: some View {
        List(items, selection: $selectedItem) { item in
            ContentListItemView(item: item)
        }
        .focused($listHasFocus)
        .onChange(of: listHasFocus) {
            print("ListTwoView focus has changed to \(listHasFocus)")
            if listHasFocus == true && selectedItem == nil {
                selectedItem = items.first?.id
            }
        }
        .onKeyPress(keys: [.upArrow]) { press in
            if let selectedItem, selectedItem == items.first?.id {
                print("List2: first item selected, and up key pressed")
                bindedFocusButtonState = viewModel.selectedButton // doesn't work
                return .handled
            }
            
            return .ignored
        }
    }
}

struct ListThreeView: View {
    
    @State private var items: [TimedItem] = [
        TimedItem(number: 7, timestamp: "2025-02-02 10:00"),
        TimedItem(number: 8, timestamp: "2025-02-02 11:00"),
        TimedItem(number: 9, timestamp: "2025-02-02 12:00")
    ]
    @ObservedObject var viewModel: ContentViewModel
    @FocusState.Binding var bindedFocusButtonState: ButtonTab?
    @FocusState.Binding var bindedListFocusState: ButtonTab?
    
    @FocusState var listHasFocus: Bool
    @State var selectedItem: TimedItem.ID?
    
    var body: some View {
        List(items, selection: $selectedItem) { item in
            ContentListItemView(item: item)
        }
        .onKeyPress(keys: [.upArrow]) { press in
            if let selectedItem, selectedItem == items.first?.id {
                print("List3: first item selected, and up key pressed")
                bindedFocusButtonState = viewModel.selectedButton // doesn't work
                return .handled
            }
            
            return .ignored
        }
    }
}

struct ButtonsTagsContentView_Previews: PreviewProvider {
    static var previews: some View {
        ButtonTagsContentView()
    }
}

struct TimedItem: Identifiable {
    let id = UUID()
    let number: Int
    let timestamp: String
}

The 'lists' are a little verbose, but just for illustration purpose.

Any ideas for what I'm doing wrong, and/or what I should do instead?

EDIT: Ok wait, this seems to work fine when running the application, but not in SwiftUI Preview. Trying to verify.

发布评论

评论列表(0)

  1. 暂无评论