From d75fb870d7ab310adff3f4b4d367127a866208ab Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Sat, 4 Apr 2026 10:03:58 -0500 Subject: [PATCH] feat: Liquid Glass navigation bar for Reader (iOS 26 standard APIs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Features/Reader/Views/ReaderTabView.swift | 271 ++++++++---------- 1 file changed, 120 insertions(+), 151 deletions(-) diff --git a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift index 5535a79..65c67b7 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift @@ -9,169 +9,138 @@ struct ReaderTabView: View { @State private var showFeedManagement = false @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 { NavigationStack { - VStack(spacing: 0) { - // Sub-tab selector - HStack(spacing: 0) { - ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in - Button { - withAnimation(.easeInOut(duration: 0.2)) { - selectedSubTab = index - switch index { - case 0: vm.applyFilter(.unread) - case 1: vm.applyFilter(.starred) - case 2: vm.applyFilter(.all) - default: break - } + // Entry list as the main content — scrolls under the glass nav bar + EntryListView(vm: vm, isCardView: isCardView, isAutoScrolling: $isAutoScrolling, scrollSpeed: scrollSpeed) + .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") } - } label: { - HStack(spacing: 4) { - Text(tab) - .font(.subheadline.weight(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()) - } + 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") } - .foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary) - .padding(.vertical, 10) - .padding(.horizontal, 16) - .background { - if selectedSubTab == index { - Capsule() - .fill(Color.accentWarm.opacity(0.12)) + } + } else { + // Normal mode: sub-tabs + controls + ToolbarItemGroup(placement: .topBarLeading) { + ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in + Button { + withAnimation(.easeInOut(duration: 0.2)) { + selectedSubTab = index + switch index { + case 0: vm.applyFilter(.unread) + case 1: vm.applyFilter(.starred) + case 2: vm.applyFilter(.all) + default: break + } + } + } label: { + Text(tab) + .fontWeight(selectedSubTab == index ? .semibold : .regular) + } + .tint(selectedSubTab == index ? Color.accentWarm : Color.textSecondary) + } + } + + ToolbarSpacer(.fixed, placement: .topBarTrailing) + + ToolbarItemGroup(placement: .topBarTrailing) { + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isCardView.toggle() + } + } label: { + Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2") + } + + Menu { + Button { + Task { await vm.markAllRead() } + } label: { + Label("Mark All Read", systemImage: "checkmark.circle") + } + + Button { + Task { await vm.refresh() } + } label: { + Label("Refresh Feeds", systemImage: "arrow.clockwise") + } + + Divider() + + Button { + showFeedManagement = true + } label: { + Label("Manage Feeds", systemImage: "list.bullet") + } + + Button { + showFeedSheet = true + } label: { + Label("Add Feed", systemImage: "plus") + } + } label: { + Image(systemName: "ellipsis") + } + } + } + + // Feed filter chips — bottom toolbar (scrolls with content) + ToolbarItem(placement: .bottomBar) { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + feedFilterChip("All", isSelected: isAllSelected) { + let tab = selectedSubTab + switch tab { + case 0: vm.applyFilter(.unread) + case 1: vm.applyFilter(.starred) + case 2: vm.applyFilter(.all) + default: vm.applyFilter(.unread) + } + } + + ForEach(vm.feeds) { feed in + let count = vm.counters?.count(forFeed: feed.id) ?? 0 + feedFilterChip( + feed.title, + count: selectedSubTab == 0 ? count : nil, + isSelected: vm.currentFilter == .feed(feed.id) + ) { + vm.applyFilter(.feed(feed.id)) + } } } } } - - Spacer() - - // Inline controls: grid/list + more menu - HStack(spacing: 4) { - Button { - withAnimation(.easeInOut(duration: 0.2)) { - isCardView.toggle() - } - } label: { - Image(systemName: isCardView ? "list.bullet" : "square.grid.2x2") - .font(.subheadline) - .foregroundStyle(Color.accentWarm) - .frame(width: 32, height: 32) - } - - Menu { - Button { - Task { await vm.markAllRead() } - } label: { - Label("Mark All Read", systemImage: "checkmark.circle") - } - - Button { - Task { await vm.refresh() } - } label: { - Label("Refresh Feeds", systemImage: "arrow.clockwise") - } - - Divider() - - Button { - showFeedManagement = true - } label: { - Label("Manage Feeds", systemImage: "list.bullet") - } - - Button { - showFeedSheet = true - } label: { - Label("Add Feed", systemImage: "plus") - } - } label: { - Image(systemName: "ellipsis") - .font(.subheadline) - .foregroundStyle(Color.accentWarm) - .frame(width: 32, height: 32) - } - } - .padding(.trailing, 8) } - .padding(.leading) - .padding(.top, 8) - - // Feed filter bar - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - feedFilterChip("All", isSelected: isAllSelected) { - let tab = selectedSubTab - switch tab { - case 0: vm.applyFilter(.unread) - case 1: vm.applyFilter(.starred) - case 2: vm.applyFilter(.all) - default: vm.applyFilter(.unread) - } - } - - ForEach(vm.feeds) { feed in - let count = vm.counters?.count(forFeed: feed.id) ?? 0 - feedFilterChip( - feed.title, - count: selectedSubTab == 0 ? count : nil, - isSelected: vm.currentFilter == .feed(feed.id) - ) { - vm.applyFilter(.feed(feed.id)) - } - } - } - .padding(.horizontal) - .padding(.vertical, 8) + .sheet(isPresented: $showFeedSheet) { + AddFeedSheet(vm: vm) } - .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: $showFeedManagement) { + FeedManagementSheet(vm: vm) } - } - .sheet(isPresented: $showFeedSheet) { - AddFeedSheet(vm: vm) - } - .sheet(isPresented: $showFeedManagement) { - FeedManagementSheet(vm: vm) - } } .onAppear { ArticleRenderer.shared.reWarmIfNeeded()