AllanJuenemann / CalendarView

UICalendarView for SwiftUI
MIT License
49 stars 6 forks source link

Updating `availableDateRange` might crash the calendar #5

Open benrudhart opened 2 weeks ago

benrudhart commented 2 weeks ago

Updating availableDateRange with a lower end than the currently visible month leads to a crash. The a crash is somewhere in deep in UICalendar. Is this a known issue?

AllanJuenemann commented 1 week ago

Hi @benrudhart, could you share the code that is producing the crash?

I wasn't able to reproduce on my end. Here's what I tried (2.1.0, iOS 17.5):

struct ContentView: View {
    @State private var dateRange = DateInterval(start: .distantPast, end: .distantFuture)

    var body: some View {
        VStack {
            CalendarView(availableDateRange: dateRange)

            Button("Last Month Only") {
                let c = Calendar.current
                let lastMonth = c.date(byAdding: .month, value: -1, to: Date())!
                let lastMonthRange = c.dateInterval(of: .month, for: lastMonth)!
                dateRange = .init(start: lastMonthRange.start, end: lastMonthRange.end - 1)
            }
        }
    }
}
benrudhart commented 1 week ago

Hi @AllanJuenemann, sure, and sorry for not adding the example in the beginning. I took your example and extended it a bit, so the crash is reproducible. One way to force the crash is to also setup the visibleDateComponents. I feel like this crash should be prevented from CalendarView

struct ContentView: View {
    @Environment(\.calendar) private var calendar
    @State private var dateRange = DateInterval(start: .distantPast, end: .now)
    @State private var visibleDateComponents = Calendar.current.dateComponents(in: .current, from: .now)

    var body: some View {
        VStack {
            CalendarView(
                availableDateRange: dateRange,
                visibleDateComponents: $visibleDateComponents
            )

            Button("Last Month Only") {
                decrementMonth()
            }
        }
    }

    private func decrementMonth() {
        let lastMonth = calendar.date(byAdding: .month, value: -1, to: dateRange.end)!
        let lastMonthRange = calendar.dateInterval(of: .month, for: lastMonth)!

        dateRange = .init(start: lastMonthRange.start, end: lastMonthRange.end - 1)

        fixVisibleDateComponentsIfNeeded()
    }

    private func fixVisibleDateComponentsIfNeeded() {
        let end = calendar.dateComponents(in: .current, from: dateRange.end)

        if let endMonth = end.month,
           visibleDateComponents.month! > endMonth {
            // important: not updating `visibleDateComponents` will cause a crash.
            //visibleDateComponents = end
        }

        // -> must do the same for the start month as well
    }
}
AllanJuenemann commented 5 days ago

Thank you, @benrudhart. Great find.

The crash is due to an exception thrown by UICalendarView.setVisibleDateComponents(_:animated:). In fact, the documentation states that what's passed to setVisibleDateComponents(_:animated:) has to be within range of availableDateRange.

But I agree with you. This should be handled gracefully by CalendarView. I'll fix it for the next release.

benrudhart commented 4 days ago

Great - thank you. Looking forward.