1

I am new in SwiftUI and I am trying to implement MultiDatePicker. MultiDatePicker opens with the current month and year by default but I want to change it to open with a selected month and year. For example, currently MultiDatePicker opens with January 2025 (current month and year) but I want to show default month like March 2024, and then user can select any dates as per their choice.

Here is my code:

import SwiftUI

struct ContentView: View {
    @State private var dates: Set<DateComponents> = []
    
    var body: some View {
        VStack {
            MultiDatePicker("Select dates", selection: $dates)
                .frame(height: 300)
            
            Text("Selected dates: \(dates.count)")
                .font(.title)
        }
        .padding()
     }
}

In the above code, the user can select multiple dates from MultiDatePicker and it's working as per my expectation, but I just want to open MultiDatePicker with a particular month instead of the default selection.

1 Answer 1

0

If the MultiDatePicker is initialized with a date range that begins in the future then it begins with the start-of-range as the selected month, instead of today's date.

There is no need to use a bounded range (start-end), you can also set a partial range by using init(selection:in:label:):

Creates an instance that selects multiple dates on or after some start date

It seems that the bounds are dynamic and can be changed after the picker has been launched. So in .onAppear, the start date can be reset to today's date. This then allows dates from today onwards to be selected.

If the month that was first shown is still in-range, the picker leaves it showing. So if the picker is initialized with a start-of-range of today + 2 months (= a date in March), the picker will show March when launched. If the start-of-range is then reset to today (= date in January), March remains showing, because it is still in-range.

struct ContentView: View {
    @Environment(\.calendar) var calendar
    @State private var dates: Set<DateComponents> = []
    @State private var startDate: Date?

    private var bounds: PartialRangeFrom<Date> {
        if let startDate {
            startDate...
        } else if let todayPlusTwoMonths = calendar.date(byAdding: .month, value: 2, to: .now) {
            todayPlusTwoMonths...
        } else {
            .now...
        }
    }

    var body: some View {
        VStack {
            MultiDatePicker("Select dates", selection: $dates, in: bounds)
                .frame(height: 300)

            Text("Selected dates: \(dates.count)")
                .font(.title)
        }
        .onAppear {
            startDate = Date()
        }
        .padding()
     }
}

In the question, you said you actually wanted March 2024 to be the first month shown, in other words, a month in the past. This is possible too, by using the initializer with a start-end date range and then manipulating the end date of the range. You will need to choose some sensible min/max dates to use for the full range, for example, today +/- 10 years.

This can be extended to a more generic solution, where the month to be shown is determined by a target date:

  • if the target date is > today, use it as the initial start date of the date range
  • otherwise, if the target date is < today, use it as the initial end date of the date range.

One thing I discovered is that when the target date is in the future and the start-of-range date is changed from a future date to a (distant) past date, the picker month changes to show the month which has the same delta to the current date as the current date had to the previous start date. This movement seems to depend on the mid-date of the range. It can be prevented by using the target date as the mid date for the range, instead of today.

struct MultiDatePickerWithTargetMonth: View {
    let targetDate: Date?
    @Environment(\.calendar) var calendar
    @State private var dates: Set<DateComponents> = []
    @State private var isShowing = false

    private var startDate: Date {
        if !isShowing, let targetDate, targetDate.timeIntervalSinceNow > 0 {
            targetDate
        } else {
            calendar.date(byAdding: .year, value: -10, to: targetDate ?? .now) ?? .now
        }
    }

    private var endDate: Date {
        if !isShowing, let targetDate, targetDate.timeIntervalSinceNow < 0 {
            targetDate
        } else {
            calendar.date(byAdding: .year, value: 10, to: targetDate ?? .now) ?? .now
        }
    }

    var body: some View {
        VStack {
            MultiDatePicker("Select dates", selection: $dates, in: startDate..<endDate)
                .frame(height: 300)

            Text("Selected dates: \(dates.count)")
                .font(.title)
        }
        .onAppear {
            isShowing = true
        }
        .padding()
     }
}

Example use:

struct ContentView: View {
    @Environment(\.calendar) var calendar

    var body: some View {
        MultiDatePickerWithTargetMonth(
            targetDate: calendar.date(from: DateComponents(year: 2024, month: 3, day: 1))
        )
    }
}
Sign up to request clarification or add additional context in comments.

2 Comments

Answer updated again with a more generic solution
somehow updating the bounds in runtime causes it to crash with "Invalid state. Unable to find a lower bounds in range." for me. UPD: only if I'm using .task, using .onAppear works fine

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.