9

I am using a ListView in Mac OS. I am trying to change that background color of that ListView. However, it is not that easy as expected.

I tried using .background(Color(.red)) attribute on the ListView. That didn't change anything.

I could only find .listRowBackground(Color(.red))which had an influence on the table rows. However, the other background wasn't effected.

I prepared a little demo to demonstrate:

In my view body:

  VStack
    {
        List()
        {
            Text("Test")
                .listRowBackground(Color.green)

            Text("Test")
                .listRowBackground(Color.green)

            Text("Test")
                .listRowBackground(Color.green)

        }.background(Color(.red))

    }.background(Color(.red))

That is the result I get:

enter image description here

The main background does not change. I read about a solution changing the UITableView.appearance but that is not possible for me in SwiftUI for Mac OS.

Thanks in advance

davidev
  • 7,694
  • 5
  • 21
  • 56

4 Answers4

24

Update 2: I haven't verified this, but user1046037 says there is a new API available for changing scrolling backgrounds: scrollContentBackground.

Update 1: I found a much better way to remove a list's background without affecting the whole app: by using Introspect.

import Introspect
import SwiftUI

extension List {
  /// List on macOS uses an opaque background with no option for
  /// removing/changing it. listRowBackground() doesn't work either.
  /// This workaround works because List is backed by NSTableView.
  func removeBackground() -> some View {
    return introspectTableView { tableView in
      tableView.backgroundColor = .clear
      tableView.enclosingScrollView!.drawsBackground = false
    }
  }
}

Usage:

List {
  ForEach(items) { item in
    ...
  }
}.removeBackground()

Old answer:

@Asperi's answer works, but only until the window is resized. Here's another workaround for overriding List's color:

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

    backgroundColor = NSColor.clear
    enclosingScrollView!.drawsBackground = false
  }
}

A potential downside is that this will affect all lists in the app.

Saket
  • 2,945
  • 1
  • 29
  • 31
  • Great answer! you can always put background behind the list right? thanks a lot! – Oscar Franco May 21 '20 at 05:37
  • Introspect works great.. however you can see that the background is being removed. It is visible for some seconds – davidev Aug 17 '20 at 13:29
  • Your old answer is the best solution at the moment! Thanks!! – davidev Oct 16 '20 at 09:15
  • I think it's very stupid, that you cannot change this in SwiftUI. Any ideas if that is solvable in macOS Monterey? `LazyVStack` and `ForEach` are not an alternative for `List` (because of the missing of selection and navigating with the arrow keys). – Lupurus Jul 27 '21 at 13:50
  • Introspection method seems to work sometimes and not other times. Do you all have the same experience? – neptunes Aug 12 '21 at 18:18
  • Thank you so much, I spent hours trying to crack this! :D – Sabesh Bharathi Apr 13 '22 at 06:55
  • Use `.scrollContentBackground(.hidden)` SwiftUI has a lot more new APIs so best for the latest APIs. Introspection is not a reliable way as it could break in a future release – user1046037 Feb 21 '23 at 15:58
4

First picture shows the origin of the issue - used scroll view is opaque by default, so added color background is there but not visible

demo1

So the solution is to find that scroll view and disable drawing background

demo2

Here is possible approach to solve this, or workaround (but valid as all modifications are made via AppKit API, no hardcoding). Actually it is the same as it would turned off checkbox in XIB for scrollview holding our tableview. Tested with Xcode 11.2 / macOS 10.15.

struct ScrollViewCleaner: NSViewRepresentable {
    
    func makeNSView(context: NSViewRepresentableContext<ScrollViewCleaner>) -> NSView {
        let nsView = NSView()
        DispatchQueue.main.async { // on next event nsView will be in view hierarchy
            if let scrollView = nsView.enclosingScrollView {
                scrollView.drawsBackground = false
            }
        }
        return nsView
    }
    
    func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<ScrollViewCleaner>) {
    }
}

extension View {
    func removingScrollViewBackground() -> some View {
        self.background(ScrollViewCleaner())
    }
}

struct TestListBackground: View {
    var body: some View {
            List()
            {
                ForEach(0..<3) { _ in
                    Text("Test")
                        .listRowBackground(Color.green)
                }
                .removingScrollViewBackground() // must be called _inside_ List
                .background(Color.blue)
            }.background(Color.red)
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Thanks for the answer. The visualization of the views is amazing. I thought that there is a default background, but didn't know how to clear it. I will try the answer soon. Thanks! – davidev Feb 29 '20 at 16:48
  • Jesus, Mary and Christ, that was so complicated, thanks a lot for this answer! – Oscar Franco Apr 17 '20 at 08:42
  • Hmm at first I thought this was working flawlessly but every now and then the color of the list comes back, could there be any other way to achieve this? – Oscar Franco Apr 22 '20 at 13:29
  • 3
    @Asperi The background gets reset immediately when the window is resized. Can there be a workaround to this? – Saket May 03 '20 at 01:52
3

Unfortunately, ListStyle protocol is not documented and .listStyle modifier has very limited usage, you can choose from CarouselListStyle, DefaultListStyle, GroupedListStyle ,PlainListStyle, SidebarListStyle

Try to mimic List with ScrollView combined with ForEach, which gives you a lot of flexibility, the missing parts are easy to write. Once the ListStyle will be available for developers, it will be easy to change the code ...

Example enter image description here with source code

struct ContentView: View {
    var body: some View {
        HStack(spacing: 0) {
            ScrollView {
                ForEach(0 ..< 4) { idx in
                    VStack(alignment: .leading) {
                        Divider().background(Color.blue)
                        Text("Test \(idx)")
                            .padding(.horizontal, 10)
                            .background(Color.pink)
                        //Divider()
                    }.padding(.bottom, -6)
                }
            }
                .frame(maxWidth: 100)
            .background(Color.gray)

            Color.green
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}
user3441734
  • 16,722
  • 2
  • 40
  • 59
  • Thanks for the help! I will try using a ScrollView instead. Do you have any information from apple when the next update will be available for the developers? – davidev Feb 28 '20 at 18:36
  • `ScrollView` does not recycle items and so can be very expensive for a lot of items. – Saket May 03 '20 at 01:44
3

In Xcode 14 (Swift 5) you can do:

 List {
 ...
 }
 .scrollContentBackground(.hidden)
 .background(.red)

This makes the ScrollView background invisible and the List background can now be customised.

enter image description here

Kappe
  • 9,217
  • 2
  • 29
  • 41