Swift and SwiftUI tutorials for Swift Developers

How to Select Multiple Dates in SwiftUI

For years, was difficult for any iOS Developer the need to allow the user to select more than one date on a calendar. UIKit did not offer a direct native solution, forcing developers to rely on third-party libraries (like FSCalendar) or build complex custom CollectionView layouts with calendar logic from scratch.

With the arrival of iOS 16 and recent Xcode updates, Apple introduced the component we were all waiting for in SwiftUI: MultiDatePicker.

In this Swift programming tutorial, we will explore in depth how to select multiple dates in SwiftUI, manage the complex DateComponents data structure, and adapt our implementation for iOS, macOS, and watchOS.

What is MultiDatePicker and why is it revolutionary?

MultiDatePicker is a control view that allows the selection of zero, one, or multiple dates. Unlike the traditional DatePicker (which manages a single point in time), this new control is designed for use cases such as:

  • Selecting vacation days.
  • Managing rotating work shifts.
  • Habit Trackers.
  • Planning irregular recurring events.

The Paradigm Shift: Date vs DateComponents

For the iOS Developer accustomed to working with Date objects, MultiDatePicker presents a significant learning curve. It does not work with Date. It works with Set<DateComponents>.

Why? Because Date represents a precise instant in universal time (seconds since 1970). However, “December 25th” is a calendar concept. Apple uses DateComponents to ensure the selected date is robust against time zones and calendar systems (Gregorian, Buddhist, etc.).

Basic Implementation on iOS

Let’s open Xcode and start with the basics. We need a state variable to store a set (Set) of date components.

Step 1: The State and the View

import SwiftUI

struct BasicMultiDateView: View {
    // The source of truth. We use a Set to automatically avoid duplicates.
    @State private var selectedDates: Set<DateComponents> = []

    var body: some View {
        VStack {
            Text("Selected days: \(selectedDates.count)")
                .font(.headline)
                .padding()

            // The star component
            MultiDatePicker("Select dates", selection: $selectedDates)
                .padding()
        }
    }
}

When running this in the iOS simulator, you will see a native calendar where you can tap multiple days. Selected days are marked with a filled circle.

Working with Data: From Components to Real Dates

Here is where most Swift programming tutorials fall short. Having a Set<DateComponents> is useful for UI, but your backend or database (CoreData/SwiftData) probably expects Date objects.

Step 2: Conversion and Sorting

Let’s create a computed property that transforms our selection into something readable.

var formattedDates: [String] {
    let calendar = Calendar.current
    
    // 1. Convert Components to Date
    let dates = selectedDates.compactMap { components -> Date? in
        // It is vital to force the time to noon or start of day to avoid time zone errors
        calendar.date(from: components)
    }
    
    // 2. Sort chronologically
    let sortedDates = dates.sorted()
    
    // 3. Format to String
    return sortedDates.map { date in
        date.formatted(date: .long, time: .omitted)
    }
}

Now we can show the list below the calendar:

List(formattedDates, id: \.self) { dateString in
    Text(dateString)
}

Restrictions and Bounds

A common scenario when selecting multiple dates in SwiftUI is limiting the range. For example, in a payroll app, you might only want to allow selecting days in the current month.

MultiDatePicker accepts the in: (range) parameter, similar to the standard DatePicker.

struct PayrollView: View {
    @State private var selectedDates: Set<DateComponents> = []
    
    // Define the range: From today to end of year
    var bounds: Range<Date> {
        let now = Date()
        let calendar = Calendar.current
        let endOfYear = calendar.date(from: DateComponents(year: 2024, month: 12, day: 31))!
        return now..<endOfYear
    }

    var body: some View {
        MultiDatePicker("Payroll", selection: $selectedDates, in: bounds)
    }
}

By using in: bounds, the calendar will visually disable (gray out) all days outside that range, preventing user interaction.

Multiplatform Adaptation in Xcode

SwiftUI code is portable, but the user experience must adapt.

MultiDatePicker on macOS

On macOS, the paradigm changes. We have more space and a mouse pointer. MultiDatePicker on macOS automatically renders as a desktop calendar.

Pro Tip for macOS: Unlike iOS, where the calendar usually occupies the screen width, on macOS you’ll want to restrict its size or put it in a Popover or sidebar.

// Optimized code for macOS
MultiDatePicker("Dates", selection: $selectedDates)
    .frame(width: 300) // Important to limit width on desktop
    .background(Material.ultraThin)
    .cornerRadius(10)

MultiDatePicker on watchOS 10+

The Apple Watch received support for this control recently. Given the screen size, the design changes radically. Apple presents a compact view that navigates to a full-month detail view when tapped.

Advanced Tutorial: “Shift Manager” App

Let’s combine everything we’ve learned into a real Swift programming example. We will create a view for an employee to select their remote work days.

The ViewModel

import SwiftUI

@Observable // Swift 5.9+ Macro
class ShiftManager {
    var selectedDays: Set<DateComponents> = []
    
    // Validation logic: Do not allow weekends
    func validateSelection() {
        let calendar = Calendar.current
        selectedDays = selectedDays.filter { components in
            guard let date = calendar.date(from: components) else { return false }
            let weekday = calendar.component(.weekday, from: date)
            // 1 = Sunday, 7 = Saturday. We keep only Monday to Friday.
            return weekday != 1 && weekday != 7
        }
    }
    
    // Reset
    func clearAll() {
        selectedDays.removeAll()
    }
}

The Main View

struct ShiftSelectorView: View {
    @State private var manager = ShiftManager()
    @Environment(\.calendar) var calendar // Use environment calendar
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Info Header
                HStack {
                    VStack(alignment: .leading) {
                        Text("Remote Work")
                            .font(.title2.bold())
                        Text("\(manager.selectedDays.count) days selected")
                            .foregroundStyle(.secondary)
                    }
                    Spacer()
                    Button("Clear") {
                        withAnimation { manager.clearAll() }
                    }
                    .buttonStyle(.bordered)
                    .tint(.red)
                }
                .padding()
                
                Divider()
                
                // The Selector
                MultiDatePicker("Select days", selection: $manager.selectedDays)
                    .tint(.indigo) // Customize selection color
                    .padding()
                    .onChange(of: manager.selectedDays) {
                        // Reactive validation
                        manager.validateSelection()
                    }
                
                // Confirmation List
                List {
                    Section("Summary") {
                        if manager.selectedDays.isEmpty {
                            ContentUnavailableView("No dates", systemImage: "calendar.badge.minus")
                        } else {
                            ForEach(manager.selectedDays.sorted(by: { 
                                // Sorting DateComponents is tricky, better convert to Date if critical
                                ($0.year ?? 0, $0.month ?? 0, $0.day ?? 0) < ($1.year ?? 0, $1.month ?? 0, $1.day ?? 0)
                            }), id: \.self) { day in
                                Label {
                                    Text("\(day.day!)/\(day.month!)/\(day.year!)")
                                } icon: {
                                    Image(systemName: "house.fill")
                                        .foregroundStyle(.indigo)
                                }
                            }
                        }
                    }
                }
            }
            .navigationTitle("Planning")
        }
    }
}

Expert Tips and Tricks (Xcode Tips)

To stand out as an iOS Developer, you must know the finer details:

1. The Calendar Environment (.environment)

MultiDatePicker uses Calendar.current by default. If your app needs to force a specific calendar (e.g., Gregorian on a device set to Buddhist), you must inject it.

MultiDatePicker(...)
    .environment(\.calendar, Calendar(identifier: .gregorian))

2. Performance

If you select hundreds of dates (e.g., a whole year), performance can degrade if you try to convert and sort the array on the main thread inside the body. Move sorting logic to a background Task or use Swift Concurrency if handling massive volumes of dates.

3. Limited Customization

Unlike third-party libraries, you cannot easily change the background color of a specific day (e.g., painting holidays red and selected days blue) within the standard MultiDatePicker. If you need that, MultiDatePicker is not the tool; you will need UICalendarView (via UIViewRepresentable) which offers delegates for custom decorations.

Conclusion

The ability to select multiple dates in SwiftUI with MultiDatePicker has democratized the creation of complex calendar interfaces. What used to take weeks of custom development or heavy dependencies can now be achieved with a few lines of native code in Xcode.

Although working with Set<DateComponents> requires a mental shift from using Date, the robustness it offers in terms of calendar precision is worth it. As an iOS Developer, integrating this component into your iOS, macOS, and watchOS apps not only modernizes your interface but ensures you are using the best accessibility and design practices of the Apple ecosystem.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

How to add a Search Bar in SwiftUI

Next Article

Pull to Refresh in SwiftUI

Related Posts