Content offset in ScrollView SwiftUI

5.4k views Asked by At

I built a simple horizontal calendar with SwiftUI and it works fine. But I want that it automatically scroll to the current date at app launch. How can I do this? I assume I should use GeometryReader to get a position of frame with current date and set an offset based on it but ScrollView doesn't have a content offset modifier. I know that in iOS 14 we now have ScrollViewReader but what about iOS 13?

struct MainCalendar: View {
    @Environment(\.calendar) var calendar
    
    @State private var month = Date()
    @State private var selectedDate: Date = Date()
    
    var body: some View {
        VStack(spacing: 30) {
            MonthAndYearView(date: $month)
            
            WeekDaysView(date: month)
        }
        .padding(.vertical)
    }
}

struct MonthAndYearView: View  {
    @Environment(\.calendar) var calendar
    private let formatter = DateFormatter.monthAndYear
    
    @Binding var date: Date
    
    var body: some View {
        HStack(spacing: 20) {
            Button(action: {
                self.date = calendar.date(byAdding: .month, value: -1, to: date)!
            }, label: {
                Image(systemName: "chevron.left")
            })
            
            Text(formatter.string(from: date))
                .font(.system(size: 18, weight: .semibold))
            
            Button(action: {
                self.date = calendar.date(byAdding: .month, value: 1, to: date)!
            }, label: {
                Image(systemName: "chevron.right")
            })
        }
    }
}

struct WeekDaysView: View {
    @Environment(\.calendar) var calenar
    
    let date: Date
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            HStack(spacing: 30) {
                ForEach(days, id: \.self) { day in
                    VStack(spacing: 20) {
                        
                        Text(daySymbol(date: day))
                            .font(.system(size: 14, weight: .regular))
                        
                        if calenar.isDateInToday(day) {
                            Text("\(dayNumber(date: day))")
                                .foregroundColor(Color.white)
                                .background(Circle().foregroundColor(.blue)
                                                .frame(width: 40, height: 40))
                        } else {
                            Text("\(dayNumber(date: day))")
                        }
                    }
                }
            }
            .padding([.horizontal])
            .padding(.bottom, 15)
        }
    }
    
    private func dayNumber(date: Date) -> String {
        let formatter = DateFormatter.dayNumber
        let dayNumber = formatter.string(from: date)
        return dayNumber
    }
    
    private var days: [Date] {
        guard let interval = calenar.dateInterval(of: .month, for: date) else { return [] }
        return calenar.generateDates(inside: interval, matching: DateComponents(hour: 0, minute: 0, second: 0))
    }
    
    private func daySymbol(date: Date) -> String {
        let dayFormatter = DateFormatter.weekDay
        let weekDay = dayFormatter.string(from: date)
        return weekDay
    }
}
1

There are 1 answers

0
SunnyBunny27 On BEST ANSWER

This library https://github.com/Amzd/ScrollViewProxy is solved my problem.

struct WeekDaysView: View {
    let date: Date
    
    @Environment(\.calendar) var calendar
    @State private var scrollTarget: Int? = nil
    
    var body: some View {
        ScrollViewReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack(spacing: 30) {
                    ForEach(Array(zip(days.indices, days)), id: \.0) { index, day in
                        VStack(spacing: 20) {
                            
                            Text(daySymbol(date: day))
                                .font(.system(size: 14, weight: .regular))
                                .scrollId(index)
                            
                            if calendar.isDateInToday(day) {
                                Text("\(dayNumber(date: day))")
                                    .foregroundColor(Color.white)
                                    .background(Circle().foregroundColor(.blue)
                                                    .frame(width: 40, height: 40))
                                    .onAppear {
                                        scrollTarget = index
                                    }
                                
                            } else {
                                Text("\(dayNumber(date: day))")
                            }
                        }
                    }
                }
                .padding([.horizontal])
                .padding(.bottom, 15)
            }
            .onAppear {
                DispatchQueue.main.async {
                    withAnimation {
                        proxy.scrollTo(scrollTarget, alignment: .center, animated: true)
                    }
                }
            }
        }
    }