feat: Liquid Glass navigation bar for Reader (iOS 26 standard APIs)
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:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user