From 544b86c94846eb30388a596cc9680329c6965e3a Mon Sep 17 00:00:00 2001 From: "simon.franken" Date: Mon, 23 Feb 2026 10:09:51 +0100 Subject: [PATCH] fix(ios): replace 2001-page TabView with 3-page recycling carousel Eliminates the eager instantiation of 2001 view bodies by keeping only three pages (previous, current, next) alive at all times. After each swipe settles, dayOffset is shifted and tabSelection is silently reset to the middle page, preserving the native paging animation. --- .../TimeEntries/TimeEntriesView.swift | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift index 7b17d49..f9de6cb 100644 --- a/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift +++ b/ios/TimeTracker/TimeTracker/Features/TimeEntries/TimeEntriesView.swift @@ -2,11 +2,13 @@ import SwiftUI struct TimeEntriesView: View { @StateObject private var viewModel = TimeEntriesViewModel() - @State private var selectedDate: Date = Calendar.current.startOfDay(for: Date()) - - // For infinite paging, we use an offset from 'today' - @State private var dayOffset: Int = 0 - + + // dayOffset is the source of truth: 0 = today, -1 = yesterday, etc. + @State private var dayOffset: Int = 0 + // tabSelection is always snapped back to 1 (middle) after each swipe. + // Pages are: 0 = dayOffset-1, 1 = dayOffset, 2 = dayOffset+1 + @State private var tabSelection: Int = 1 + @State private var showFilterSheet = false @State private var showAddEntry = false @State private var entryToEdit: TimeEntry? @@ -101,20 +103,37 @@ struct TimeEntriesView: View { // MARK: - Main content private var mainContent: some View { - // We use a wide range of offsets to simulate infinite paging. - // -1000 days is roughly 2.7 years in the past. - // 1000 days is roughly 2.7 years in the future. - TabView(selection: $dayOffset) { - ForEach(-1000...1000, id: \.self) { offset in - let dayForPage = Calendar.current.date(byAdding: .day, value: offset, to: Calendar.current.startOfDay(for: Date())) ?? Date() - + // Only 3 pages exist at any time: previous, current, next. + // After each swipe settles, we reset tabSelection to 1 and shift + // dayOffset, so the carousel appears infinite while staying cheap. + TabView(selection: $tabSelection) { + ForEach(0..<3, id: \.self) { page in + let offset = dayOffset + (page - 1) + let day = Calendar.current.date( + byAdding: .day, + value: offset, + to: Calendar.current.startOfDay(for: Date()) + ) ?? Date() + ScrollView { - dayEntriesSection(for: dayForPage) + dayEntriesSection(for: day) } - .tag(offset) // Important: tag must match the selection type (Int) + .tag(page) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .onChange(of: tabSelection) { _, newPage in + guard newPage != 1 else { return } + // Shift the logical day offset by how many pages we moved. + dayOffset += newPage - 1 + // Snap back to the middle page without animation so the + // surrounding pages are refreshed invisibly. + var tx = Transaction() + tx.disablesAnimations = true + withTransaction(tx) { + tabSelection = 1 } } - .tabViewStyle(.page(indexDisplayMode: .never)) // Native swipe gesture } // MARK: - Day entries section