swiftuitvosapple-tv

tvOS - SwiftUI - LazyVGrid Item ContextMenu not displaying as expected


my problem is very simple, i'm trying to implement something similar to the Apple TV app Context menu : appletv+ app example

But in my case, the contextmenu is opening outside of the LazyVGrid and not next to the grid item , and the contextmenu is not getting the context of the selected item, but always the data of the first one : my example

My code :

struct HomeView: View {
    
    var test : [String] = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10", "Item 11", "Item 12", "Item 13", "Item 14"]
    
    var body: some View {
        NavigationStack() {
            List{
                Section(header: Text("Test")
                    .font(.title2)
                    .fontWeight(.bold)
                    .foregroundColor(.primary)) {
                        ScrollView(.vertical, showsIndicators: false) {
                            LazyVGrid(columns: [GridItem(.flexible()),
                                                GridItem(.flexible()),
                                                GridItem(.flexible()),
                                                GridItem(.flexible()),
                                                GridItem(.flexible())], alignment: .leading, spacing: 80) {
                                ForEach(test,  id: \.self) { item in
                                    Button(item) {
                                        
                                    }.buttonStyle(MyButtonStyle()).contextMenu {
                                        Text("Context Menu for : \(item)")
                                        Button("Add Favorite") {}
                                    }
                                }
                            }
                        }
                    }.contentMargins(30)
            }
        }
    }
    
}

struct MyButtonStyle: PrimitiveButtonStyle {
  @Environment(\.isFocused) var focused: Bool
  @State private var isFocused: Bool = false
   
  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .compositingGroup()
      .frame(minWidth: 250,maxWidth: 250, minHeight: 125, maxHeight: 125)
      .padding([.all], 10)
      .multilineTextAlignment(.center)
      .focusable(true, onFocusChange: { focused in
        if focused {
          isFocused = true
        } else {
          isFocused = false
        }
      })
      .background(RoundedRectangle(cornerRadius: 20).fill(isFocused ? .blue : .gray).opacity(0.7))
      .foregroundColor(isFocused ? .white : .white)
      .onTapGesture(perform: configuration.trigger)
  }
}


Solution

  • Rather than using a Button and trying to add a context menu to it, you could use a Menu component, and assign a "primary action" which is the behaviour to enact when you press it.

    Menu {
      Button("Favorite") { ... }
      Button("Mark as watched") { ... }
    } label: {
      // This is where your visual content goes
    } primaryAction: {
      // this code block is executed on single tap
    }
    

    This native component should cut down on the amount of custom code you need to write, and is much more likely to play nicely with LazyVStack's layout engine.