fix: Reader pre-loads on app launch, no more glitchy initial state
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

- ReaderViewModel starts with isLoading=true (shows spinner, not "No articles")
- MainTabView owns ReaderViewModel and pre-fetches in background on launch
- Sub-tabs and feed chips hidden during initial load (no tiny squished layout)
- VStack fills full screen with frame(maxWidth/maxHeight: .infinity)
- WebKit warmer triggers when Reader tab appears
- By the time user taps Reader, data is already loaded

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 20:41:13 -05:00
parent 0b74493db0
commit a0d3f24614
3 changed files with 76 additions and 67 deletions

View File

@@ -26,6 +26,7 @@ struct MainTabView: View {
@State private var selectedTab = 0 @State private var selectedTab = 0
@State private var showAssistant = false @State private var showAssistant = false
@State private var confettiTrigger = 0 @State private var confettiTrigger = 0
@State private var readerVM = ReaderViewModel()
var body: some View { var body: some View {
ZStack { ZStack {
@@ -38,12 +39,16 @@ struct MainTabView: View {
.tabItem { Label("Fitness", systemImage: "flame.fill") } .tabItem { Label("Fitness", systemImage: "flame.fill") }
.tag(1) .tag(1)
ReaderTabView() ReaderTabView(vm: readerVM)
.tabItem { Label("Reader", systemImage: "newspaper.fill") } .tabItem { Label("Reader", systemImage: "newspaper.fill") }
.tag(2) .tag(2)
} }
.tint(Color.accentWarm) .tint(Color.accentWarm)
.modifier(TabBarMinimizeModifier()) .modifier(TabBarMinimizeModifier())
.task {
// Pre-fetch reader data in background while user is on Home/Fitness
await readerVM.loadInitial()
}
// Floating buttons // Floating buttons
VStack { VStack {

View File

@@ -10,7 +10,7 @@ final class ReaderViewModel {
var categories: [ReaderCategory] = [] var categories: [ReaderCategory] = []
var counters: ReaderCounters? var counters: ReaderCounters?
var total = 0 var total = 0
var isLoading = false var isLoading = true
var isLoadingMore = false var isLoadingMore = false
var isRefreshing = false var isRefreshing = false
var error: String? var error: String?

View File

@@ -1,7 +1,7 @@
import SwiftUI import SwiftUI
struct ReaderTabView: View { struct ReaderTabView: View {
@State private var vm = ReaderViewModel() @Bindable var vm: ReaderViewModel
@State private var selectedSubTab = 0 @State private var selectedSubTab = 0
@State private var showFeedSheet = false @State private var showFeedSheet = false
@State private var showFeedManagement = false @State private var showFeedManagement = false
@@ -10,80 +10,85 @@ struct ReaderTabView: View {
var body: some View { var body: some View {
NavigationStack { NavigationStack {
VStack(spacing: 0) { VStack(spacing: 0) {
// Sub-tab selector if !vm.isLoading || !vm.entries.isEmpty {
HStack(spacing: 0) { // Sub-tab selector only show after initial load
ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in HStack(spacing: 0) {
Button { ForEach(Array(subTabs.enumerated()), id: \.offset) { index, tab in
withAnimation(.easeInOut(duration: 0.2)) { Button {
selectedSubTab = index withAnimation(.easeInOut(duration: 0.2)) {
switch index { selectedSubTab = index
case 0: vm.applyFilter(.unread) switch index {
case 1: vm.applyFilter(.starred) case 0: vm.applyFilter(.unread)
case 2: vm.applyFilter(.all) case 1: vm.applyFilter(.starred)
default: break case 2: vm.applyFilter(.all)
default: break
}
} }
} } label: {
} label: { HStack(spacing: 4) {
HStack(spacing: 4) { Text(tab)
Text(tab) .font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular))
.font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular))
if index == 0, let counters = vm.counters, counters.totalUnread > 0 { if index == 0, let counters = vm.counters, counters.totalUnread > 0 {
Text("\(counters.totalUnread)") Text("\(counters.totalUnread)")
.font(.caption2.weight(.bold)) .font(.caption2.weight(.bold))
.foregroundStyle(.white) .foregroundStyle(.white)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.vertical, 2) .padding(.vertical, 2)
.background(Color.accentWarm) .background(Color.accentWarm)
.clipShape(Capsule()) .clipShape(Capsule())
}
} }
} .foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary)
.foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary) .padding(.vertical, 10)
.padding(.vertical, 10) .padding(.horizontal, 16)
.padding(.horizontal, 16) .background {
.background { if selectedSubTab == index {
if selectedSubTab == index { Capsule()
Capsule() .fill(Color.accentWarm.opacity(0.12))
.fill(Color.accentWarm.opacity(0.12)) }
} }
} }
} }
} }
}
.padding(.horizontal)
.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(.horizontal)
.padding(.vertical, 8) .padding(.top, 8)
// Feed filter bar
if !vm.feeds.isEmpty {
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)
}
}
} }
// Entry list // Entry list (shows LoadingView when isLoading)
EntryListView(vm: vm, isCardView: isCardView) EntryListView(vm: vm, isCardView: isCardView)
} }
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.canvas) .background(Color.canvas)
.navigationBarHidden(true) .navigationBarHidden(true)
.toolbar { .toolbar {
@@ -137,9 +142,8 @@ struct ReaderTabView: View {
FeedManagementSheet(vm: vm) FeedManagementSheet(vm: vm)
} }
} }
.task { .onAppear {
WebKitWarmer.shared.warmUp() WebKitWarmer.shared.warmUp()
await vm.loadInitial()
} }
} }