airbnb / HorizonCalendar

A declarative, performant, iOS calendar UI component that supports use cases ranging from simple date pickers all the way up to fully-featured calendar apps.
Apache License 2.0
2.77k stars 232 forks source link

Error: NavigationLink presenting a value must appear inside a NavigationStack... when having a NavigationLink inside .days #308

Open BredBurr opened 3 months ago

BredBurr commented 3 months ago

Hi guys, thanks so much for this great calendar code!

I'm on iOS16+ and I use NavigationStack and NavigationLink(value: ) and I encounter an error if I put the NavigationLink inside .days modifier.

I have a simple app MainView -> CalendarView -> ContentView. My .days modifier looks like this:

var body: some View {
    VStack(alignment: .center) {
        CalendarViewRepresentable(calendar: calendar,
                                  visibleDateRange: startDate...Date(),
                                  monthsLayout: .horizontal(options: HorizontalMonthsLayoutOptions(maximumFullyVisibleMonths: 1,
                                                                                                   scrollingBehavior: .paginatedScrolling(.init(restingPosition: .atLeadingEdgeOfEachMonth, restingAffinity: .atPositionsAdjacentToPrevious)))),
                                  dataDependency: selectedDayRange,
                                  proxy: calendarViewProxy)

        //HERE:
        .days { day in
            NavigationLink(value: myValue) {
                ArchiveDayView(dayNumber: day.day, value: myValue)
            }
        }
    }
}

I get this:

Errors

This code has been working FINE for the past half a year. So it must be something new in the latest Xcode. What if the links placed inside modifiers stop working...

Can you suggest the correct way to place a link inside a day? Thanks!

BredBurr commented 3 months ago

I've made a barebones working example, which you can copy-paste and see for yourself. Run it on the real device.

import SwiftUI
import HorizonCalendar

@main
struct SwiftUICalendarNavigationApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink(value: 7) {
                Text("Just go to day 7")
            }.padding()

            NavigationLink(value: "Calend") {
                Text("Go to Calendar view")
            }

            .navigationDestination(for: Int.self) { day in
                DayView(day: day)
            }
            .navigationDestination(for: String.self) { destination in
                CalendView()
            }
        }
    }
}

struct DayView: View {
    let day: Int

    var body: some View {
        Text("You are viewing day \(day)")
    }
}

struct CalendView: View {
    @StateObject private var calendarViewProxy = CalendarViewProxy()
    @State private var selectedDayRange: DayComponentsRange?

    private var calendar: Calendar
    private let startDate: Date
    private let monthDateFormatter: DateFormatter

    init() {
        calendar = Calendar(identifier: .gregorian)

        startDate = calendar.date(from: DateComponents(timeZone: TimeZone(abbreviation: "UTC")!,
                                                       year: 2024,
                                                       month: 01,
                                                       day: 01))!

        monthDateFormatter = DateFormatter()
        monthDateFormatter.calendar = calendar
        monthDateFormatter.dateFormat = "MMMM yyyy"
    }

    var body: some View {
        VStack(alignment: .center) {
            CalendarViewRepresentable(calendar: calendar,
                                      visibleDateRange: startDate...Date(),
                                      monthsLayout: .horizontal(options: HorizontalMonthsLayoutOptions(maximumFullyVisibleMonths: 1,
                                                                                                       scrollingBehavior: .paginatedScrolling(.init(restingPosition: .atLeadingEdgeOfEachMonth, restingAffinity: .atPositionsAdjacentToPrevious)))),
                                      dataDependency: selectedDayRange,
                                      proxy: calendarViewProxy)

            .interMonthSpacing(20)
            .verticalDayMargin(14)
            .horizontalDayMargin(14)

            .monthHeaders { month in
                let monthHeaderText = monthDateFormatter.string(from: calendar.date(from: month.components)!)
                Text(monthHeaderText)
                    .font(.title2)
                    .padding()
            }

            //Here it is:
            .days { day in
                NavigationLink(value: day.day) {
                    Text("\(day.day)")
                }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .padding()
        .navigationBarTitleDisplayMode(.inline)
    }
}