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

View File

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

View File

@@ -1,7 +1,7 @@
import SwiftUI
struct ReaderTabView: View {
@State private var vm = ReaderViewModel()
@Bindable var vm: ReaderViewModel
@State private var selectedSubTab = 0
@State private var showFeedSheet = false
@State private var showFeedManagement = false
@@ -10,80 +10,85 @@ struct ReaderTabView: View {
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
if !vm.isLoading || !vm.entries.isEmpty {
// Sub-tab selector only show after initial load
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
}
}
}
} label: {
HStack(spacing: 4) {
Text(tab)
.font(.subheadline.weight(selectedSubTab == index ? .semibold : .regular))
} 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())
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))
.foregroundStyle(selectedSubTab == index ? Color.accentWarm : Color.textSecondary)
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background {
if selectedSubTab == index {
Capsule()
.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(.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)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.canvas)
.navigationBarHidden(true)
.toolbar {
@@ -137,9 +142,8 @@ struct ReaderTabView: View {
FeedManagementSheet(vm: vm)
}
}
.task {
.onAppear {
WebKitWarmer.shared.warmUp()
await vm.loadInitial()
}
}