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:
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:
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.
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.
You can get it working more like your target design with three small changes:
selectedCategory
to a FocusState
variable:@FocusState private var selectedCategory: String?
Add a .focused
modifier to the NavigationLink
, so that selectedCategory
gets updated when focus changes.
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:
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())
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
))
Btw, you are using deprecated code in some places:
The modifier .edgesIgnoringSafeArea(.all)
is deprecated, use .ignoresSafeArea()
instead.
The modifier .foregroundColor
is deprecated, use .foregroundStyle
instead.
The modifier .cornerRadius
is deprecated. Show a rounded rectangle in the background instead (as in the updated button style above).
UIScreen.main
is deprecated. One way to replace is to wrap the parent view in a GeometryReader
. A GeometryReader
aligns its content to the top-left corner. If the content does not already expand to use all the space available, it can be aligned to screen center by setting a frame with maxWidth and maxHeight:
// 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)
}
}