swiftuiscrollviewreader

ScrollTo with ScrollViewReader doesn't work onAppear using Date as data


Hi Stack Overflow community, I have a problem. I want to create a date picker based on a ScrollView where the starting date is today, and the range is 15 days forward and backward. For some reason, SwiftUI doesn't navigate to values that are off-screen, and also, during initialization, it doesn't scroll to today's date, which is currently March 25th. Is this a known issue? I tried various methods like using DispatchQueue delays or Task.sleep, but nothing works. I would appreciate any help.

PS: When I changed the values to Int, everything works, so something is wrong with handling dates.

Code example:

import SwiftUI

struct ContentView: View {
    private let dates: [Date] = {
        let today = Date()
        let calendar = Calendar.current
        return (0...30)
            .map {
                calendar.date(byAdding: .day,
                              value: $0 - 15,
                              to: today)!
            }
    }()
    
    @State var selectedDate: Date = Date()
    
    var body: some View {
        GeometryReader { geometry in
            ScrollViewReader { scrollValue in
                ScrollView(.horizontal, showsIndicators: false) {
                    HStack(spacing: 8) {
                        ForEach(dates, id: \.self) { date in
                            DateCell(date: date, isSelected: Calendar.current.isDate(date, inSameDayAs: selectedDate))
                                .id(date)
                                .onTapGesture {
                                    withAnimation {
                                        selectedDate = date
                                    }
                                }
                        }
                    }
                    .padding(.horizontal, geometry.size.width / 2)
                }
                .onChange(of: selectedDate, initial: true) { _, newValue in
                    withAnimation {
                        scrollValue.scrollTo(newValue, anchor: .center)
                    }
                }
            }
        }
    }
}

struct DateCell: View {
    let date: Date
    var isSelected: Bool
    
    var body: some View {
        VStack {
            Text(date, format: .dateTime.weekday(.short))
            Text(date, format: .dateTime.day())
                .bold()
        }
        .frame(width: 50, height: 50)
        .background(isSelected ? Color.red : Color.gray.opacity(0.3))
        .clipShape(Circle())
    }
}

#Preview {
    ContentView()
}

enter image description here


Solution

  • This problem is probably happening because Date() gives you a date that is accurate to some fraction of a second (nanoseconds?). So the date you use to initialize the collection is going to be different to the date that you use to initialize selectedDate. In other words, the initial value of selectedDate will not be present in the array of dates.

    To fix, try truncating the dates to start of day. Calendar has a function that does this:

    private let dates: [Date] = {
        let calendar = Calendar.current
        let today = calendar.startOfDay(for: Date()) // 👈 get today from Calendar
        return (0...30)
            .map {
                calendar.date(byAdding: .day,
                              value: $0 - 15,
                              to: today)!
            }
    }()
    
    @State var selectedDate: Date = Calendar.current.startOfDay(for: Date()) // 👈 here too