3

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

3
  • What is code for EffectView or state or so many other things? can you give a code that already build? Commented Aug 5, 2022 at 17:16
  • Focusable should fire or we should understand why it is not firing, can you manually select one of the list items and then use arrow keys and see if that works. If so then focusable is the problem to solve Commented Aug 5, 2022 at 17:22
  • Refer developer.apple.com/wwdc21/10023 Commented Aug 5, 2022 at 17:23

1 Answer 1

11

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
            }
        }
    }
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, the approach with NSEvent.addLocalMonitorForEvents works great. Had to remove monitor when the view would be closed.
I wonder if there is a solution for horizontal scroll view (I mean it doesn't use List)

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.