3

I'm building a macOS app with SwiftUI, and I'm trying to remove (or even cover up) the border added to a List item when I right-click it.

Here it is by default:

List Row

Now with a right-click and a contextMenu view modifier:

List Row Right Click

I figured this is an NSTableView quirk, so I tried the approaches in these three Stack Overflow posts:

  1. Customize right click highlight on view-based NSTableView
  2. NSTableView with menu, how to change the border color with right click?
  3. Disabling the NSTableView row focus ring
  4. NSTableView: blue outline on right-clicked rows

I couldn't get any of those to work, and that may be due to the fact that I can't subclass an NSTableView, but can only override its properties and methods with an extension. Here's what I have so far that successfully removes the default table background and such:

extension NSTableView{
  open override func viewDidMoveToWindow() {
    super.viewDidMoveToWindow()

    //Remove default table styles
    backgroundColor = NSColor.clear
    enclosingScrollView!.drawsBackground = false
    selectionHighlightStyle = .none
  }
}

Is there any way to remove that right-click border in SwiftUI? I'm even open to covering it with other views, but I can't seem to draw SwiftUI views in that space around the table cell.

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128
  • Were you able to find a solution for this? – Euan Traynor Mar 08 '22 at 08:17
  • @EuanTraynor I found a workaround. I'll post it as an answer since it contains some code. – Clifton Labrum Mar 09 '22 at 05:59
  • thanks for the extension of NSTableView! I have no idea how AppKit works but I was struggling to simply remove the default selection highlight... SwiftUI solutions (especially .listRowBackground(Color.clear)) work well for iOS and iPadOS but on AppKit is stubborn... – domi852 Feb 09 '23 at 07:37

2 Answers2

1

I found a workaround for this. I put my List in a ZStack and then set its opacity to zero. I then built out a fully custom version of the same list, but using LazyVStack:

//Message List
ZStack{
  //Ghost list for keyboard control
  List($model.messages, id: \.self, selection: $model.selectedMessages){ $message in
    MessageItemView(message: $message)
  }
  .focusable()
  .opacity(0)
  
  //Custom UI for the above List
  ScrollView{
    ZStack{ 
      LazyVStack(spacing: 5){
        ForEach($model.messagesToday){ $message in
          MessageItemView(message: $message)
        }
      }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)

Each list is bound to the same model, so if I click a message to select it in the custom UI, the same thing gets selected in the invisible List. All the keyboard shortcuts that come with table use in a List look like they are working on the custom version.

So how does this solve my original problem? You can right-click on the custom MessageItemView and the default ring around the cell is invisible, but the contextMenu still works (it's defined inside my MessageItemView).

This isn't as elegant as I'd like, but it's nice to have 100% control over the UI but still get all the keyboard controls that come for free with a List.

Clifton Labrum
  • 13,053
  • 9
  • 65
  • 128
0
.onReceive(NotificationCenter.default.publisher(for: NSTableView.selectionIsChangingNotification)) { notification in
  if let tableView = notification.object as? NSTableView {
    tableView.selectionHighlightStyle = .none
  }
}

You can use this. It will reload your List. I'm looking for another approach but this works.

Jordi Bruin
  • 1,588
  • 1
  • 11
  • 20