1

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 :)
user4150758
  • 384
  • 4
  • 17

2 Answers2

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)
        }
    }
}
user1046037
  • 16,755
  • 12
  • 92
  • 138
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
            }
        }
    }
}
  • 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? – user4150758 Nov 12 '22 at 06:47
  • ok I think I have solved this by using "scrollTo" method under "onChange" instead of in keyboard monitor closure – user4150758 Nov 12 '22 at 07:07
  • 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 Jan 03 '23 at 09:15
  • @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. – workingdog support Ukraine Jan 04 '23 at 12:21
  • @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. – Tap Forms Jan 05 '23 at 21:20