2

I've built a view that has scroll view of horizontal type with HStack for macOS app. Is there a way to circle those items using keyboard arrows?

(I see that ListView has a default behavior but for other custom view types there are none)

click here to see the screenshot

var body: some View {
   VStack {
     ScrollView(.horizontal, {
        HStack {
          ForEach(items.indices, id: \.self) { index in
               //custom view for default state and highlighted state
          }
        }
     }
    }
}


any help is appreciated :)
3
  • Does this answer your question? SwiftUI keyboard navigation in lists on MacOS Commented Nov 11, 2022 at 13:37
  • @workingdogsupportUkraine unfortunately no, it uses List whereas in my case I can't use List because it has to scroll horizontally Commented Nov 11, 2022 at 13:39
  • added an answer, using a horizontal scroll. Commented Nov 11, 2022 at 23:20

2 Answers 2

1

Approach I used

  • Uses keyboard shortcuts on a button

Alternate approach

Code:

Model

struct Item: Identifiable {
    var id: Int
    var name: String
}

class Model: ObservableObject {
    @Published var items = (0..<100).map { Item(id: $0, name: "Item \($0)")}
}

Content

struct ContentView: View {
    
    @StateObject private var model = Model()
    @State private var selectedItemID: Int?
    
    var body: some View {
        VStack {
            Button("move right") {
                moveRight()
            }
            .keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
            
            
            ScrollView(.horizontal) {
                LazyHGrid(rows: [GridItem(.fixed(180))]) {
                    ForEach(model.items) { item in
                        ItemCell(
                            item: item,
                            isSelected: item.id == selectedItemID
                        )
                        .onTapGesture {
                            selectedItemID = item.id
                        }
                    }
                }
            }
        }
    }
    
    private func moveRight() {
        if let selectedItemID {
            if selectedItemID + 1 >= model.items.count {
                self.selectedItemID = model.items.last?.id
            } else {
                self.selectedItemID = selectedItemID + 1
            }
        } else {
            selectedItemID = model.items.first?.id
        }
    }
}

Cell

struct ItemCell: View {
    let item: Item
    let isSelected: Bool
    var body: some View {
        ZStack {
            Rectangle()
                .foregroundColor(isSelected ? .yellow : .blue)
            Text(item.name)
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

0

You could try this example code, using my previous post approach, but with a horizontal scrollview instead of a list. You will have to adjust the code to your particular app. My approach consists only of a few lines of code that monitors the key events.

import Foundation
import SwiftUI
import AppKit

struct ContentView: View {
    let fruits = ["apples", "pears", "bananas", "apricot", "oranges"]
    @State var selection: Int = 0
    @State var keyMonitor: Any?
    
    var body: some View {
        ScrollView(.horizontal) {
            HStack(alignment: .center, spacing: 0) {
                ForEach(fruits.indices, id: \.self) { index in
                    VStack {
                        Image(systemName: "globe")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 20, height: 20)
                            .padding(10)
                        Text(fruits[index]).tag(index)
                    }
                    .background(selection == index ? Color.red : Color.clear)
                    .padding(10)
                }
            }
        }
        .onAppear {
            keyMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { nsevent in
                if nsevent.keyCode == 124 { // arrow right
                    selection = selection < fruits.count ? selection + 1 : 0
                } else {
                    if nsevent.keyCode == 123 { // arrow left
                        selection = selection > 1 ? selection - 1 : 0
                    }
                }
                return nsevent
            }
        }
        .onDisappear {
            if keyMonitor != nil {
                NSEvent.removeMonitor(keyMonitor!)
                keyMonitor = nil
            }
        }
    }
}

5 Comments

it works like charm, I went ahead & added ScrollViewReader below ScrollView & I have used scrollView method under 124 condition, so that it will actually scroll and show the highlighted image but it doesn't seem to scroll. Is this the right way to do it?
ok I think I have solved this by using "scrollTo" method under "onChange" instead of in keyboard monitor closure
This is great. But there are a couple problems with it. First, when you use the arrow keys, you hear the alert sound. Second, when the SwiftUI view that has this code is not in the foreground, events are still being processed by it, reacting to the arrow keys.
@Tap Forms, as mentioned, you will have to adjust the code to your particular app. For example when the SwiftUI view is not in the foreground you could use the class AppDelegate to remove the keyMonitor, like in the .onDisappear {...} when entering background state. As for the sound .... you probably know how to deal with this better than me.
@workingdogsupportUkraine Ya, just returning nil instead of nsevent from the key monitor closure will prevent the beep sound. I also check to see if the window on the nsevent is the same as my current window. If so, then I process the keyboard, if not, I just return the nsevent. Seems to work for me.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.