From a0d3f24614210bc778a57381fd5fb43cd09aaaf2 Mon Sep 17 00:00:00 2001 From: Yusuf Suleman Date: Fri, 3 Apr 2026 20:41:13 -0500 Subject: [PATCH] fix: Reader pre-loads on app launch, no more glitchy initial state - 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) --- ios/Platform/Platform/ContentView.swift | 7 +- .../Reader/ViewModels/ReaderViewModel.swift | 2 +- .../Features/Reader/Views/ReaderTabView.swift | 134 +++++++++--------- 3 files changed, 76 insertions(+), 67 deletions(-) diff --git a/ios/Platform/Platform/ContentView.swift b/ios/Platform/Platform/ContentView.swift index 8f6eea9..4304e36 100644 --- a/ios/Platform/Platform/ContentView.swift +++ b/ios/Platform/Platform/ContentView.swift @@ -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 { diff --git a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift index 0da0460..a4a07e7 100644 --- a/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift +++ b/ios/Platform/Platform/Features/Reader/ViewModels/ReaderViewModel.swift @@ -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? diff --git a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift index 7fd54fb..abd70a0 100644 --- a/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift +++ b/ios/Platform/Platform/Features/Reader/Views/ReaderTabView.swift @@ -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() } }