SwiftUI List with Divider but no extra Insets in MacOS

Viewed 553

In a SwiftUI List on MacOS I want to have Dividers but no extra spacing between the lines.
The following example produces two Lists like this:

enter image description here

What I would like to have is:

enter image description here

Dividers in the second List with the "original" total height without extra spacing.

This is the code I tried:

struct TestViews_MacOS: View {
  @State var selectedLine: String?

  var body: some View {
    VStack{
      List(selection: $selectedLine) {
        TestLineWithSeperator(text: "Line 1")
        TestLineWithSeperator(text: "Line 2")
        TestLineWithSeperator(text: "Line 3")
        TestLineWithSeperator(text: "Line 4")
      }
      Text("==========================")
      List(selection: $selectedLine) {
        TestLineWithOutSeparator(text: "Line 1")
        TestLineWithOutSeparator(text: "Line 2")
        TestLineWithOutSeparator(text: "Line 3")
        TestLineWithOutSeparator(text: "Line 4")
      }
//      .onReceive(NotificationCenter.default.publisher(for: NSView.frameDidChangeNotification)) {
//        guard let tableView = $0.object as? NSTableView else { return }
//        print("set inte to 0")
//        tableView.intercellSpacing = .zero
//    }
      //.listStyle(DefaultListStyle())
      //.listStyle(InsetListStyle())
      //.listStyle(PlainListStyle())
      //.listStyle(SidebarListStyle())
      //.listRowInsets(.none)
      Spacer()
      HStack{
        Text("\(selectedLine ?? "nothing Selected")").padding()
        Spacer()
      }
    }
  }
}

struct TestLineWithSeperator: View {
  var text: String

  var body: some View {
    VStack {
      HStack{
      Text(text)
//    .padding(.top, -4)
//    .padding(.bottom, -4)
//    .listRowInsets(.none)
//    .border(Color.green)
        Spacer()
      }.padding(0)
      Divider()
    }
    .padding(0)
    .tag("x \(text)")
  }
}
4 Answers

The divider should really be provided by the List automatically as it does on iOS but unfortunately is not as of macOS 12.3.

The padding around each row in a List seems to be there to account for the selection highlight. So if we want to provide our own Divider but want it to look native while accounting for the highlight padding we would need to offset the Divider like so:

struct ContentView: View {
    @Binding var selection: Set<Item>

    var body: some View {
        List(selection: $selection) {
           SomeView()
              .swiftyListDivider()
        }
    }
}

import SwiftUI
extension View {
    func swiftyListDivider() -> some View {
        background(Divider().offset(y: 4.0), alignment: .bottom)
    }
}

But to replicate the exact same Divider behavior like for example in Mail.app you would additionally need to add logic for:

  • hide divider of selected row
  • hide divider of row above selected row
  • hide divider of last row

The way I handled this was using the .overlay() modifier. You may need to play with the padding depending on the other styles.

List {
    ForEach(self.listItems, id: \.self) { item in
        TestLine(item).overlay(Divider(), alignment: .bottom).padding(.vertical, -4) // Or, whatever you wanted to call it.
    }
}

A few thoughts on getting the result you want:

  1. Custom Color and Separator Thickness: You could play around with Rectangle().fill(.red).frame(height: 1, alignment: .bottom) in the overlay.
  2. No Padding Please: You could try .offset(y: -4) on the Divider().
  3. Alignment: Try adjusting the overlay alignment (e.g. .top, .trailing)

An alternative approach, since List is based upon NSTableView on macOS is to add an extension to your SwiftUI view and set the gridStyleMask to .solidHoriontalGridLineMask:

extension NSTableView {
   open override func viewDidMoveToWindow() {
       super.viewDidMoveToWindow()
       gridStyleMask = .solidHorizontalGridLineMask
   }
}

Now there's no more hoops to jump through or drawing your own dividers. They properly show and hide when not selected or selected. And the spacing is proper too.

None selected:

enter image description here

First row selected:

enter image description here

Last row selected:

enter image description here

The separator isn't actually causing the extra padding. VStack comes with some spacing by default, which is causing the row to appear like it has extra padding.

VStack(spacing: 0) {
    Text("Hello World!")
    Divider()
}

Doing this should move the divider directly up underneath the text, without extra padding.

Related