TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

Focusable grid view on macOS 13

Forums > SwiftUI

Hello, I am trying to make a collection view where it would be possible to select items.

It is not so hard to make it all work on macOS 14, but I've faced some problems with adopting it to macOS 13.

I use the focusable() modifier which enables focus switch between views by pressing Tab. On macOS 13 the focus ring around the text field just disappears, tho, logLine is never modified.

In my case I want re-create a collection view of items, where an item could be selected. It would be possible also to move the selection by pressing arrow keys.

I've left some inline comments to point out problematic places in the code, mainly in LocalGridView.

Maybe here could be someone who tried to achieve the same behaviour? I will appreciate any suggestion!

struct LocalGridView: View {

    private let columns = [GridItem(.adaptive(minimum: 100))]

    let items = (0..<99).map { "item \($0)" }

    @State private var selectedItem: String? = nil
    @State private var someText: String = ""
    @State private var logLine: String = ""

    private enum FocusableView: String, Hashable {

        case textField
        case grid
    }

    @FocusState private var focusOn: FocusableView?

    var body: some View {
        Self._printChanges()
        return VStack {
            TextField("", text: $someText)
                .padding()
                .focused($focusOn, equals: .textField)

            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(items, id: \.self) { item in
                        LocalGridViewItem(
                            text: item,
                            isSelected: item == selectedItem
                        )
                        .onTapGesture {
                            // Interesting enough, setting `focusOn` to nil, removes the focus from the text field,
                            // but setting it onto the grid view does nothing.
                            focusOn = .grid
                            selectedItem = item
                        }
                    }
                }
                // On macOS 14 focus effect appears, which could be removed with `focusEffectDisabled()`
                // On macOS 13 this modifier enables focus move from and to the text field above,
                // but it never lands up on the grid view.
                .focusable()
                .focused($focusOn, equals: .grid)
            }
            // This works only with a view, which has current focus
//            .onMoveCommand(perform: { direction in
//                switch direction {
//                case .up: onMoveUp()
//                case .down: onMoveDown()
//                case .left: onMoveUp()
//                case .right: onMoveDown()
//                @unknown default: break
//                }
//            })
            .padding(.horizontal)

            Divider()

            Text(logLine)
                .padding()

            // As I cannot use right now .onMoveCommand, I can create invisible buttons which would react on
            // movement shortcuts.
            // But in this case it is not guarded by the focused state and they intercept text field navigation,
            // so I need to set up keyboard shortcuts conditionally, which rises question:
            // how to track focused view on macOS 13 (and preferably on macOS 12 as well).
            HStack {
                Button("") { onMoveUp() }.keyboardShortcut(.upArrow, modifiers: [])
                Button("") { onMoveDown() }.keyboardShortcut(.downArrow, modifiers: [])
                Button("") { onMoveUp() }.keyboardShortcut(.leftArrow, modifiers: [])
                Button("") { onMoveDown() }.keyboardShortcut(.rightArrow, modifiers: [])
            }
            .opacity(0)
            .frame(width: 0, height: 0)
        }
        .onAppear {
            focusOn = .grid
        }
        .onChange(of: focusOn) { value in
            logLine = "onChange: \(value?.rawValue ?? "no focus")"
        }
    }

    private func onMoveUp() {
        guard let item = selectedItem else {
            selectedItem = items.last
            return
        }

        guard let index = items.firstIndex(of: item)?.advanced(by: -1), items.indices.contains(index) else {
            return
        }

        self.selectedItem = items[index]
    }

    private func onMoveDown() {
        guard let item = selectedItem else {
            selectedItem = items.first
            return
        }

        guard let index = items.firstIndex(of: item)?.advanced(by: 1), items.indices.contains(index) else {
            return
        }

        selectedItem = items[index]
    }
}
struct LocalGridViewItem: View {

    // The isFocused state is always false on macOS 13
    @Environment(\.isFocused) var isFocused

    var text: String
    var isSelected: Bool

    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 5)
                .foregroundStyle(selectionColor)

            Text(text)
        }
        .frame(height: 50)
    }

    private var selectionColor: Color {
        if !isSelected {
            return .clear
        }
        return isFocused ? Color(.selectedContentBackgroundColor) : Color(.unemphasizedSelectedContentBackgroundColor)
    }
}

2      

I am curious as to how you managed to implement this even on macOS 14.

Even with the new focus additions, I can't seem to figure out a sensible way of handling the up and down keys in a LazyVGrid. There seems to be no way to accurately guess which item is directly above or beneath the one that currently has focus.

https://developer.apple.com/videos/play/wwdc2023/10162/?time=1084

Of course, the example code given by Apple just leaves out this particularly important point.

2      

Hacking with Swift is sponsored by String Catalog.

SPONSORED Get accurate app localizations in minutes using AI. Choose your languages & receive translations for 40+ markets!

Localize My App

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.