I have an infinite week scroller implemented using a TabView's page styling.
basically when you scroll to the next week, it pre-loads the week after so that you can scroll infinitely.
Since iOS 17.4, it seems to partially scroll two pages ahead. Scrolling backwards works fine.
I made a radar: FB13718482
Here is a simplified implementation that has the issue reproduced. It uses the swift ordered collections library.
Video of the issue: https://youtu.be/JW8dHqawURA
import Foundation import OrderedCollections import SwiftUI struct ContentView: View { private let calendar: Calendar private let dateFormatter: DateFormatter @State var weeks: OrderedDictionary<String, WeekView.Week> @State var selectedWeek: WeekView.Week.ID init() { let calendar = Calendar.autoupdatingCurrent self.calendar = calendar let formatter = DateFormatter() formatter.calendar = calendar formatter.dateFormat = "MMM d" dateFormatter = formatter // Setup initial week let currentDate = Date() let weekIdentifier = Self.weekIdentifier(for: currentDate, calendar: calendar) let weeks: OrderedDictionary<WeekView.Week.ID, WeekView.Week> = [ weekIdentifier: Self.createWeek(for: currentDate, calendar: calendar) ] self._weeks = .init(initialValue: weeks) self._selectedWeek = .init(initialValue: weekIdentifier) } var body: some View { NavigationStack { TabView(selection: $selectedWeek) { ForEach(weeks.values) { week in WeekView(week: week) .tag(week.id) } } .onChange(of: selectedWeek, initial: true) { oldValue, newValue in createNextWeekIfRequired(for: weeks[newValue]!) } .tabViewStyle(.page(indexDisplayMode: .always)) .indexViewStyle(.page(backgroundDisplayMode: .always)) .navigationTitle(selectedWeek) } .environment(\.dateFormatter, dateFormatter) } private func createNextWeekIfRequired(for week: WeekView.Week) { guard let finalWeek = weeks.values.last, week.id == finalWeek.id, let day = finalWeek.days.first else { return } let nextWeek = calendar.date(byAdding: .weekOfYear, value: 1, to: day)! let identifier = Self.weekIdentifier(for: nextWeek, calendar: calendar) guard weeks[identifier] == nil else { return } weeks[identifier] = Self.createWeek(for: nextWeek, calendar: calendar) } static func weekIdentifier(for date: Date, calendar: Calendar) -> WeekView.Week.ID { let year = calendar.component(.yearForWeekOfYear, from: date) let week = calendar.component(.weekOfYear, from: date) return "\(year)-\(week)" } static func createWeek(for date: Date, calendar: Calendar) -> WeekView.Week { let startOfDay = calendar.startOfDay(for: date) let weekOfYear = calendar.component(.weekOfYear, from: startOfDay) let startOfWeek = calendar.nextDate( after: startOfDay + 1, matching: .init(hour: 0, minute: 0, second:0, nanosecond: 0, weekday: 1, weekOfYear: weekOfYear), matchingPolicy: .nextTime, direction: .backward )! var dates: [Date] = [] calendar.enumerateDates( startingAfter: startOfWeek - 1, matching: .init(hour: 0, minute: 0, second:0, nanosecond: 0), matchingPolicy: .nextTime ) { result, exactMatch, stop in guard let result, calendar.component(.weekOfYear, from: result) == weekOfYear else { stop = true return } dates.append(result) } return WeekView.Week(id: weekIdentifier(for: date, calendar: calendar), days: dates) } } #Preview { ContentView() }
import SwiftUI struct WeekView: View { struct Week: Identifiable { var id: String var days: [Date] } var week: Week private let columnDefinition = [GridItem]( repeating: GridItem(.flexible(minimum: 10, maximum: 200), alignment: .center), count: 7 ) var body: some View { LazyVGrid(columns: columnDefinition, alignment: .center) { ForEach(week.days, id: \.timeIntervalSinceReferenceDate) { date in DayView(date: date) } } .frame(maxWidth: .infinity) } }
import SwiftUI struct DayView: View { @Environment(\.dateFormatter) private var dateFormatter let date: Date var body: some View { VStack { Text(date, formatter: dateFormatter) Image(systemName: "calendar") .foregroundStyle(Color.blue) } } } #Preview { DayView(date: Date()) }
import Foundation import SwiftUI struct DateFormatterEnvironmentKey: EnvironmentKey { static var defaultValue: DateFormatter = { let formatter = DateFormatter() formatter.calendar = .autoupdatingCurrent formatter.dateFormat = "MMM d" return formatter }() } extension EnvironmentValues { var dateFormatter: DateFormatter { get { self[DateFormatterEnvironmentKey.self] } set { self[DateFormatterEnvironmentKey.self] = newValue } } }