feat: Liquid Glass navigation bar for Reader (iOS 26 standard APIs)
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

Removed .navigationBarHidden(true) and all custom header layout.
Now uses standard iOS 26 navigation APIs that get Liquid Glass free:

- .navigationTitle("Reader") + .navigationSubtitle("74 unread")
- ToolbarItemGroup for sub-tabs (Unread/Starred/All) on leading
- ToolbarSpacer between groups
- ToolbarItemGroup for grid/list + menu on trailing
- Feed filter chips in .bottomBar toolbar
- When auto-scrolling: toolbar swaps to speed controls

The glass nav bar is translucent — content scrolls underneath.
Collapses to inline glass pill on scroll (system behavior).
No custom backgrounds, no custom layout — all system-managed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-04 10:03:58 -05:00
parent 61cd78e080
commit d75fb870d7

View File

@@ -9,11 +9,42 @@ struct ReaderTabView: View {
@State private var showFeedManagement = false @State private var showFeedManagement = false
@State private var isCardView = true @State private var isCardView = true
private var subtitleText: String {
if let counters = vm.counters, counters.totalUnread > 0 {
return "\(counters.totalUnread) unread"
}
return "All caught up"
}
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: 0) { // Entry list as the main content scrolls under the glass nav bar
// Sub-tab selector EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed)
HStack(spacing: 0) { .navigationTitle("Reader")
.navigationSubtitle(subtitleText)
.toolbar {
if isAutoScrolling {
// Auto-scroll mode: speed controls in toolbar
ToolbarItemGroup(placement: .topBarLeading) {
Button {
scrollSpeed = max(0.25, scrollSpeed - 0.25)
} label: {
Image(systemName: "minus")
}
Text(String(format: "%.2fx", scrollSpeed))
.font(.body.weight(.bold).monospacedDigit())
.foregroundStyle(Color.textPrimary)
Button {
scrollSpeed = min(3.0, scrollSpeed + 0.25)
} label: {
Image(systemName: "plus")
}
}
} else {
// Normal mode: sub-tabs + controls
ToolbarItemGroup(placement: .topBarLeading) {
ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in
Button { Button {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
@@ -26,45 +57,22 @@ struct ReaderTabView: View {
} }
} }
} label: { } label: {
HStack(spacing: 4) {
Text(tab) Text(tab)
.font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular)) .fontWeight(selectedSubTab == index ? .semibold : .regular)
if index == 0, let counters = vm.counters, counters.totalUnread > 0 {
Text("\(counters.totalUnread)")
.font(.caption2.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.accentWarm)
.clipShape(Capsule())
}
}
.foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary)
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background {
if selectedSubTab == index {
Capsule()
.fill(Color.accentWarm.opacity(0.12))
}
} }
.tint(selectedSubTab == index ? Color.accentWarm : Color.textSecondary)
} }
} }
Spacer() ToolbarSpacer(.fixed, placement: .topBarTrailing)
// Inline controls: grid/list + more menu ToolbarItemGroup(placement: .topBarTrailing) {
HStack(spacing: 4) {
Button { Button {
withAnimation(.easeInOut(duration: 0.2)) { withAnimation(.easeInOut(duration: 0.2)) {
isCardView.toggle() isCardView.toggle()
} }
} label: { } label: {
Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2") Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2")
.font(.subheadline)
.foregroundStyle(Color.accentWarm)
.frame(width: 32, height: 32)
} }
Menu { Menu {
@@ -95,17 +103,12 @@ struct ReaderTabView: View {
} }
} label: { } label: {
Image(systemName: "ellipsis") Image(systemName: "ellipsis")
.font(.subheadline)
.foregroundStyle(Color.accentWarm)
.frame(width: 32, height: 32)
} }
} }
.padding(.trailing, 8)
} }
.padding(.leading)
.padding(.top, 8)
// Feed filter bar // Feed filter chips bottom toolbar (scrolls with content)
ToolbarItem(placement: .bottomBar) {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) { HStack(spacing: 8) {
feedFilterChip("All", isSelected: isAllSelected) { feedFilterChip("All", isSelected: isAllSelected) {
@@ -129,41 +132,7 @@ struct ReaderTabView: View {
} }
} }
} }
.padding(.horizontal)
.padding(.vertical, 8)
} }
.frame(height: 44)
// Entry list
EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.canvas)
.navigationBarHidden(true)
.safeAreaBar(edge: .bottom) {
if isAutoScrolling {
HStack(spacing: 16) {
Button {
scrollSpeed = max(0.25, scrollSpeed - 0.25)
} label: {
Image(systemName: "minus")
.font(.caption.weight(.bold))
.foregroundStyle(Color.accentWarm)
}
Text(String(format: "%.2fx", scrollSpeed))
.font(.system(size: 13, weight: .bold, design: .monospaced))
.foregroundStyle(Color.textPrimary)
Button {
scrollSpeed = min(3.0, scrollSpeed + 0.25)
} label: {
Image(systemName: "plus")
.font(.caption.weight(.bold))
.foregroundStyle(Color.accentWarm)
}
}
.padding(.horizontal, 20)
} }
} }
.sheet(isPresented: $showFeedSheet) { .sheet(isPresented: $showFeedSheet) {