Home > Back-end >  SwiftUI keyboard navigation in lists on MacOS
SwiftUI keyboard navigation in lists on MacOS

Time:08-09

I'm trying to implement a list which can be navigated with arrow keys - up/down. I've created layout, but now I don't totally understand how(and where) to make up/down keys intercepted so I could add my custom logic. I already tried onMoveCommand with focusable but that did not work(wasn't firing at all)

Code I have - below

public var body: some View {
        VStack(spacing: 0.0) {
            VStack {
                HStack(alignment: .center, spacing: 0) {
                    Image(systemName: "command")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 20, height: 20)
                        .padding(.leading, 20)
                        .offset(x: 0, y: 1)
                    TextField("Search Commands", text: $state.commandQuery)
                        .font(.system(size: 20, weight: .light, design: .default))
                        .textFieldStyle(.plain)
                        .onReceive(
                            state.$commandQuery
                                .debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)
                        ) { val in
                            state.fetchMatchingCommands(val: val)
                        }
                    .padding(16)
                    .foregroundColor(Color(.systemGray).opacity(0.85))
                    .background(EffectView(.sidebar, blendingMode: .behindWindow))
                }
            }
            Divider()
            VStack(spacing: 0) {
                List(state.filteredCommands.isEmpty && state.commandQuery.isEmpty ?
                     commandManager.commands : state.filteredCommands, selection: $selectedItem) { command in
                        VStack(alignment: .leading, spacing: 0) {
                            Text(command.title).foregroundColor(Color.white)
                                .padding(EdgeInsets.init(top: 0, leading: 10, bottom: 0, trailing: 0))
                                .frame(height: 10)
                        }.frame(maxWidth: .infinity, maxHeight: 15, alignment: .leading)
                            .listRowBackground(self.selectedItem == command ?
                                               RoundedRectangle(cornerRadius: 5, style: .continuous)
                                .fill(Color(.systemBlue)) :
                                                RoundedRectangle(cornerRadius: 5, style: .continuous)
                                .fill(Color.clear) )

                        .onTapGesture {
                            self.selectedItem = command
                            callHandler(command: command)
                        }.onHover(perform: { _ in self.selectedItem = command })
                    }.listStyle(SidebarListStyle())
            }
        }
        .background(EffectView(.sidebar, blendingMode: .behindWindow))
        .foregroundColor(.gray)
        .edgesIgnoringSafeArea(.vertical)
        .frame(minWidth: 600,
           minHeight: self.state.isShowingCommandsList ? 400 : 28,
           maxHeight: self.state.isShowingCommandsList ? .infinity : 28)
    }

This is how it looks - and I want to make focus move between found list items

enter image description here

CodePudding user response:

If I understand your question correctly, you want to use the arrow keys to "move" from the search TextField, to the list of items, and then navigate the list with the up/down arrow keys.

Try something simple like this example code, to monitor the up/down arrow keys, and take the appropriate action.

Adjust/tweak the logic to suit your needs.

import Foundation
import SwiftUI
import AppKit


struct ContentView: View {
    let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
    @State var selection: Int?
    @State var search = ""
    
    var body: some View {
        VStack {
            VStack {
                HStack(alignment: .center, spacing: 0) {
                    Image(systemName: "command")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 20, height: 20)
                        .padding(.leading, 20)
                        .offset(x: 0, y: 1)
                    TextField("Search", text: $search)
                        .font(.system(size: 20, weight: .light, design: .default))
                        .textFieldStyle(.plain)
                }
            }
            Divider()
            List(selection: $selection) {
                ForEach(fruits.indices, id: \.self) { index in
                    Text(fruits[index]).tag(index)
                }
            }
            .listStyle(.bordered(alternatesRowBackgrounds: true))
        }
        .onAppear {
            NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
                if selection != nil {
                    if nsevent.keyCode == 125 { // arrow down
                        selection = selection! < fruits.count ? selection!   1 : 0
                    } else {
                        if nsevent.keyCode == 126 { // arrow up
                            selection = selection! > 1 ? selection! - 1 : 0
                        }
                    }
                } else {
                    selection = 0
                }
                return nsevent
            }
        }
    }
}
  • Related