Highlight row in SwiftUI sidebar?

Attempting to Building a MacOS Sidebar in SwiftUI

Hey folks, I'm trying to get a generic Mac app proof of concept going with SwiftUI. The skeleton I'm trying to build is a sidebar with a main view, where the sidebar is translucent and I can select items in the list, and the entire row would be highlighted when selected.

Examples of Ideal Solutions

In a perfect world, my goal is something similar to what the Music app in Catalina has going on, where the selected item preserves the translucency:

image

If that's not yet possible without an entirely custom view, I would at least like to achieve something similar to what NetNewsWire has going on, where the selected item is a solid color from edge to edge of the sidebar:

image

The Problem I'm Trying to Solve

I've encountered some problems with the styling. I can't get any background styling of individual items to actually extend edge to edge.

Things I've Tried

  1. Setting the .background of the view.
  2. Setting the .listRowBackground of the view.
  3. Setting the .listRowBackground and also setting .listRowInset to nil.
  4. Setting the .listRowBackground and also setting .listRowInset to 0 on all sides.

Here's a screenshot with the corresponding code:

struct ContentView: View {
    var body: some View {
        NavigationView {
            // Sidebar View
            List {
                Text("Setting the background")
                    .background(Color.red)
                Text("Setting the listRowBackground")
                    .listRowBackground(Color.blue)
                Text("Setting the listRowBackground and setting listRowInsets to nil")
                    .listRowInsets(nil)
                    .listRowBackground(Color.purple)
                Text("Setting the listRowBackground and setting listRowInsets to 0")
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                    .listRowBackground(Color.green)
            }
            .listStyle(SidebarListStyle())
            .frame(minWidth: 300)

            // Main View
            Text("Hello there.")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }.navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

My Question: Does anyone have any idea to actually get the background styling to go edge to edge on a sidebar?

Bonus: Does anyone know how to get background styling to be translucent just like on the Music app in Catalina?

This is just a speculation, but Text is quite economical with the offered screen size, perhaps wrap it in GeometryReader?

GeometryReader { _ in 
  Text("")
}.background(Color.red)

It is also possible that it is a bug in earlier Xcode version. There are a few times that upgrading Xcode fixes weird SwiftUI bug.

PS.
If you want to use it for navigation, maybe use NavigationLink instead.
Also, View has opacity modifier for adjusting the transparency.

That's an interesting suggestion. So I did what you suggested for the item in blue, and sure enough it now highlights the entire row to the right only. I suppose the GeometryReader view is more loosey goosey with space, so it accepts whatever the parent suggests.

However, there is this awkward space to the left that is still "indented" or "inset" or whatever you want to call it.

image

Hmm, GeometryReader uses all the space offered, so the blue region is actually all there is. Maybe we need to change some setting from outside.

On a related note, I remember MovieSwiftUI (article) did some custom sidebar. You may want to look into that. The git itself is a pile of mess, without Xcode atm I can navigate only so far. Perhaps you'll have better luck?

Perhaps play around with opacity and blend modes, ie:

        List {
            Text("Setting the background")
                .background(Color.red)
                .blendMode(.screen)
                .opacity(0.5)
            Text("Setting the listRowBackground")
                .listRowBackground(Color.blue)
            Text("Setting the listRowBackground and setting listRowInsets to nil")
                .listRowInsets(nil)
                .listRowBackground(Color.purple)
            Text("Setting the listRowBackground and setting listRowInsets to 0")
                .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                .listRowBackground(Color.green)
        }
        .background(
            LinearGradient(gradient: Gradient(colors: [Color.black, Color.gray]), startPoint: .leading, endPoint: .trailing)
        )
        .listStyle(SidebarListStyle())
        .frame(minWidth: 400)

You could try the trick from 34:21 in 204 Introducing SwiftUI: Building Your First App setting .frame(minWidth:0, maxWidth:.infinity, minHeight:0, maxHeight:.infinity)

List does have an initializer init(_:selection:rowContent:) where you can provide a binding to a SelectionValue : Hashable.

This will make the List selectable which when using SidebarListStyle it would produce the behavior that you are looking for.

Here's an example showing the usage of selection:


struct ContentView: View {

    enum Menu: Hashable {
        case forYou
        case browse
        case radio
    }

    @State private var selection: Menu? = nil

    var body: some View {
        NavigationView {
            // Sidebar View
            List(selection: $selection) {
                Section(header: Text("Apple Music")) {
                    Text("For You").tag(Menu.forYou)
                    Text("Browse").tag(Menu.browse)
                    Text("Radio").tag(Menu.radio)
                }

            }
            .listStyle(SidebarListStyle())
            .frame(minWidth: 300)

            // Main View
            Text("Hello there.")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
        .navigationViewStyle(DoubleColumnNavigationViewStyle())
    }
}

Note that I had to use the tag(_:) modifier to give identity to every static element in the List and the type of the binding that I'm passing a selection (Binding<ContentView.Menu>) matches the type passed to tag(_:) (ContentView.Menu).
If you are using ForEach you can omit the explicit .tag(_:) as the framework would automatically tag the element for you using the value of id; of course you're free to override this value should you need to.

2 Likes

What version is this? Previously the List selection was only for when in editing mode. Or maybe it SidebarListStyle is what makes it work?

I am using a little bit different approach. By using ScrollView instead of List (see the commented lines) I am able customize my "sidebar list" with different ways, which are impossible otherwise. Till Apple give us public API for ListStyle, we are not able to modify what is currently available.

You can copy - paste - run this snippet

import SwiftUI

class Model: ObservableObject {
    @Published var selection: Int? {
        willSet {
            if let nv = newValue {
                selected = nv
                willChangeSelection?(selected)
            }
        }
    }
    var selected: Int = 0
    let willChangeSelection: ((Int) -> Void)?
    init( onSelection: ((Int)->Void)? ) {
        willChangeSelection = onSelection
        selection = nil
    }
}

struct ContentView: View {
    @ObservedObject var model = Model { i in
        print("selected:", i)
    }
    
    var body: some View {
        
        NavigationView {
            HStack(alignment: .top) {
                ScrollView(.vertical) {
                //List {
                    NavigationLink(destination: Detail(txt: "1"), tag: 1, selection: $model.selection) {
                        RowLabel(txt: "First", tag: 1, selected: model.selected)
                    }
                    NavigationLink(destination: Detail(txt: "2"), tag: 2, selection: $model.selection) {
                        RowLabel(txt: "Second", tag: 2, selected: model.selected)
                    }
                    
                    NavigationLink(destination: Detail(txt: "3"), tag: 3, selection: $model.selection) {
                        RowLabel(txt: "Third", tag: 3, selected: model.selected)
                    }
                    Color.clear
                    }
                //.listStyle(SidebarListStyle())
                .buttonStyle(PlainButtonStyle())
                .frame(width: 180, alignment: .topLeading)
                
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .onAppear {
            self.model.selection = 1
        }
    }
}

struct Detail: View {
    let txt: String
    var body: some View {
        VStack {
            Text(self.txt).font(.system(size: 300))
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct RowLabel: View {
    let txt: String
    let tag: Int
    let selected: Int
    var body: some View {
        ZStack {
            Color.yellow.opacity(0.2)
            HStack {
                Text(txt)
                    .font(selected == tag ? .largeTitle: .subheadline)
                    .padding(.horizontal)
                Spacer()
            }
        }.fixedSize(horizontal: false, vertical: true)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

to see how it works.

1 Like
Terms of Service

Privacy Policy

Cookie Policy