swiftswiftuitvosxcode16

Categories or Selection Menus Don't Fit the Design


I'm currently working on my own Apple TV app. So far, things are going pretty well, but right now, I'm stuck on the design of the categories or selection menus.

Here's a screenshot of how it looks right now: See screenshot here

The green color and the border are intentionally added for now so I can see what is where. My actual goal is to remove the gray bar (or is this the "main bar"?). The pink bar and its border are just design elements that can be removed if needed. I want it to look more "original," like this: See screenshot here

Here is the code:

struct SettingsCategoryView: View {
    let title: String
    let isSelected: Bool

    var body: some View {
        HStack {
            Text(title)
                .foregroundColor(isSelected ? .black : .white)
                .font(.system(size: 22, weight: .regular))
                .padding(.leading, 20)

            Spacer()

            Image(systemName: "chevron.right")
                .foregroundColor(isSelected ? .black : .gray)
                .padding(.trailing, 20)
        }
        .frame(height: 50) // Einheitliche Höhe für die Kategorien
        .background(Color.pink) // Innerer Hintergrund auf pink gesetzt
        .cornerRadius(10) // Abrundung direkt auf den Hintergrund anwenden
        .overlay(
            RoundedRectangle(cornerRadius: 10)
                .stroke(Color.green, lineWidth: 3) // Äußerer Rahmen auf grün gesetzt
        )
        .padding(.horizontal, 0) // Entferne äußere Ränder
        .background(Color.clear) // Entferne alle anderen Hintergründe
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

I’ve adjusted the code, but it’s still not quite right. When a category is not selected, it appears black instead of gray, like in the original design. See screenshot here

Here is the code:

import SwiftUI

struct SettingsView: View {
    @State private var selectedCategory: String?

    var body: some View {
        NavigationStack {
            ZStack {
                Color.black
                    .edgesIgnoringSafeArea(.all)

                VStack(spacing: 0) {
                    // Überschrift oben in der Mitte
                    Text("Einstellungen")
                        .font(.system(size: 40, weight: .semibold))
                        .foregroundColor(.white)
                        .padding(.top, 30)

                    HStack {
                        // Linke Seite mit Logo
                        VStack {
                            Spacer()
                            Image(systemName: "applelogo")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 120, height: 120)
                                .foregroundColor(.white)
                            Spacer()
                        }
                        .frame(width: UIScreen.main.bounds.width * 0.4)

                        // Rechte Seite mit Kategorien
                        VStack(spacing: 15) {
                            ForEach(categories, id: \.self) { category in
                                NavigationLink(
                                    value: category,
                                    label: {
                                        SettingsCategoryView(
                                            title: category,
                                            isSelected: selectedCategory == category
                                        )
                                    }
                                )
                                .buttonStyle(PlainButtonStyle())
                            }
                        }
                        .frame(width: UIScreen.main.bounds.width * 0.5)
                    }
                }
            }
            .navigationDestination(for: String.self) { value in
                Text("\(value)-Ansicht")
                    .font(.title)
                    .foregroundColor(.white)
                    .navigationTitle(value)
            }
        }
    }

    private var categories: [String] {
        ["Allgemein", "Benutzer:innen und Accounts", "Video und Audio", "Bildschirmschoner", "AirPlay und HomeKit", "Fernbedienungen und Geräte", "Apps", "Netzwerk", "System", "Entwickler"]
    }
}

struct SettingsCategoryView: View {
    let title: String
    let isSelected: Bool

    var body: some View {
        HStack {
            Text(title)
                .foregroundColor(.white)
                .font(.system(size: 22, weight: .medium))
                .padding(.leading, 20)

            Spacer()

            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
                .padding(.trailing, 20)
        }
        .frame(height: 50) // Einheitliche Höhe für die Kategorien
        .background(isSelected ? Color.gray.opacity(0.3) : Color.clear) // Hervorhebung des ausgewählten Elements
        .cornerRadius(8) // Abgerundete Ecken
        .scaleEffect(isSelected ? 1.05 : 1.0) // Fokus-Animation
        .animation(.easeInOut, value: isSelected)
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

I just can’t get it right. Now I can see the unselected area, but that annoying large gray bar is back again. See screenshot here

Here is the code:

import SwiftUI

struct SettingsView: View {
    @State private var selectedCategory: String?

    var body: some View {
        NavigationStack {
            ZStack {
                Color.black
                    .edgesIgnoringSafeArea(.all)

                VStack(spacing: 0) {
                    // Überschrift oben in der Mitte
                    Text("Einstellungen")
                        .font(.system(size: 40, weight: .semibold))
                        .foregroundColor(.white)
                        .padding(.top, 30)

                    HStack {
                        // Linke Seite mit Logo
                        VStack {
                            Spacer()
                            Image(systemName: "applelogo")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 120, height: 120)
                                .foregroundColor(.white)
                            Spacer()
                        }
                        .frame(width: UIScreen.main.bounds.width * 0.4)

                        // Rechte Seite mit Kategorien
                        VStack(spacing: 15) {
                            ForEach(categories, id: \.self) { category in
                                NavigationLink(
                                    value: category,
                                    label: {
                                        SettingsCategoryView(
                                            title: category,
                                            isSelected: selectedCategory == category
                                        )
                                    }
                                )
                                .buttonStyle(PlainButtonStyle())
                            }
                        }
                        .frame(width: UIScreen.main.bounds.width * 0.5)
                    }
                }
            }
            .navigationDestination(for: String.self) { value in
                Text("\(value)-Ansicht")
                    .font(.title)
                    .foregroundColor(.white)
                    .navigationTitle(value)
            }
        }
    }

    private var categories: [String] {
        ["Allgemein", "Benutzer:innen und Accounts", "Video und Audio", "Bildschirmschoner", "AirPlay und HomeKit", "Fernbedienungen und Geräte", "Apps", "Netzwerk", "System", "Entwickler"]
    }
}

struct SettingsCategoryView: View {
    let title: String
    let isSelected: Bool

    var body: some View {
        HStack {
            Text(title)
                .foregroundColor(.white)
                .font(.system(size: 22, weight: .medium))
                .padding(.leading, 20)

            Spacer()

            Image(systemName: "chevron.right")
                .foregroundColor(.gray)
                .padding(.trailing, 20)
        }
        .frame(height: 50) // Einheitliche Höhe für die Kategorien
        .background(isSelected ? Color.gray.opacity(0.3) : Color.gray.opacity(0.15)) // Hintergrundfarbe für ausgewählte und nicht ausgewählte Kategorien
        .cornerRadius(8) // Abgerundete Ecken
        .scaleEffect(isSelected ? 1.05 : 1.0) // Fokus-Animation
        .animation(.easeInOut, value: isSelected)
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
    }
}

I want it to look like it does in the second screenshot. I’ve adjusted the code several times, but it never works. I actually like how it looks in the last screenshot even more, but the large gray bar just won’t disappear.


Solution

  • You can get it working more like your target design with three small changes:

    1. Change the state variable selectedCategory to a FocusState variable:
    @FocusState private var selectedCategory: String?
    
    1. Add a .focused modifier to the NavigationLink, so that selectedCategory gets updated when focus changes.

    2. Apply .borderless as button style to the navigation link, instead of PlainButtonStyle:

    NavigationLink(
        // ...
    )
    .focused($selectedCategory, equals: category)
    .buttonStyle(.borderless)
    

    This is how it looks with these changes:

    Animation


    You might notice in the gif above that there is a selection marker (a small dot) on the right side of the selected row. This seems to be something that comes with button style .borderless. If you want to get rid of the marker, you can define your own custom button style and use this instead of .borderless:

    struct SettingsViewButtonStyle: ButtonStyle {
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
        }
    }
    
    .buttonStyle(SettingsViewButtonStyle())
    

    Animation


    To give the custom button style a bit more purpose, you could move some of the styling from SettingsCategoryView into the button style. You probably want to update the styling for the selected row too. For example:

    struct SettingsViewButtonStyle: ButtonStyle {
        let isSelected: Bool
    
        func makeBody(configuration: Configuration) -> some View {
            configuration.label
                .foregroundStyle(isSelected ? .black : .white)
                .padding(.horizontal, 20)
                .frame(height: 50) // Einheitliche Höhe für die Kategorien
                .background {
                    RoundedRectangle(cornerRadius: 8)
                        .fill(isSelected ? .white : .gray.opacity(0.3))
                }
                .scaleEffect(isSelected ? 1.05 : 1.0) // Fokus-Animation
                .animation(.easeInOut, value: isSelected)
        }
    }
    

    SettingsCategoryView then simplifies to the following:

    struct SettingsCategoryView: View {
        let title: String
    
        var body: some View {
            HStack {
                Text(title)
                    .font(.system(size: 22, weight: .medium))
    
                Spacer()
    
                Image(systemName: "chevron.right")
                    .foregroundStyle(.gray)
            }
        }
    }
    

    Updated use:

    NavigationLink(value: category) {
        SettingsCategoryView(title: category)
    }
    .focused($selectedCategory, equals: category)
    .buttonStyle(SettingsViewButtonStyle(
        isSelected: selectedCategory == category
    ))
    

    Animation


    Btw, you are using deprecated code in some places:

    // SettingsView
    
    var body: some View {
        GeometryReader { proxy in
            let screenWidth = proxy.size.width
            NavigationStack {
    
                // ...
    
                            // Linke Seite mit Logo
                            VStack {
                                // ...
                            }
                            .frame(width: screenWidth * 0.4)
    
                            // Rechte Seite mit Kategorien
                            VStack(spacing: 15) {
                                // ...
                            }
                            .frame(width: screenWidth * 0.5)
    
                // ...
    
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }