A month calendar for iOS. Drop it in, give it a start and an end date, and it scrolls one month per page, horizontally or vertically, with today marked, weekends coloured, days selected by tap and events shown as dots.
- UIKit and SwiftUI.
CalendarViewfor UIKit,KDCalendarViewfor SwiftUI with a two-way selection binding. - Any calendar. The user's current calendar and time zone by default; Gregorian, Persian, Hebrew or anything else through
Style.calendar, with the week starting on the day the locale says. - System events. The optional
KDCalendarEventKitproduct loads events from the user's calendars and adds new ones. - Single, multiple or range selection. Two taps select every day between them, across months.
- Styled by value.
CalendarView.Styleis a struct; change a property and the view follows. Dynamic system colours and Dynamic Type by default, and a per-day style hook for holidays and disabled days. - Swift 6, iOS 17+. Main-actor isolated,
Sendablewhere it matters, VoiceOver labels on every day.
In Xcode choose File, then Add Package Dependencies, and enter:
https://github.com/mmick66/CalendarView
Or in Package.swift:
.package(url: "https://github.com/mmick66/CalendarView", from: "2.0.0")Add the KDCalendar product to your target, and KDCalendarEventKit as well if you want system calendar events.
2.0.0 is the final CocoaPods release. Prefer Swift Package Manager.
pod 'KDCalendar', '~> 2.0' # the calendar
pod 'KDCalendar/EventKit', '~> 2.0' # plus system eventsCalendarView is a UIView. Create it in code or in a storyboard (set the module to KDCalendar), then give it a data source and a delegate:
import KDCalendar
let calendarView = CalendarView(frame: .zero)
calendarView.dataSource = self
calendarView.delegate = self
calendarView.direction = .horizontal
calendarView.selectionMode = .singleThe data source returns the first and last selectable day. Every month those days touch is shown; days before the start or after the end are greyed out. Without a data source the view shows the current month.
extension ViewController: CalendarViewDataSource {
func startDate() -> Date { Calendar.current.date(byAdding: .month, value: -1, to: Date())! }
func endDate() -> Date { Calendar.current.date(byAdding: .month, value: 12, to: Date())! }
}The delegate hears about scrolling and selection. Only the first two methods are required.
extension ViewController: CalendarViewDelegate {
func calendar(_ calendar: CalendarView, didScrollToMonth date: Date) { }
func calendar(_ calendar: CalendarView, didSelectDate date: Date, withEvents events: [CalendarEvent]) { }
func calendar(_ calendar: CalendarView, canSelectDate date: Date) -> Bool { true }
func calendar(_ calendar: CalendarView, didDeselectDate date: Date) { }
func calendar(_ calendar: CalendarView, didLongPressDate date: Date, withEvents events: [CalendarEvent]?) { }
}The view holds both weakly, so keep them alive yourself. A view controller that owns the calendar is the usual arrangement.
Every date the view computes or hands out is the start of a day in calendarView.calendar, which is style.calendar and defaults to Calendar.current. didScrollToMonth receives the first day of the month. To work in another time zone or calendar, set one on the style:
var style = CalendarView.Style()
style.calendar = Calendar(identifier: .persian)
style.firstWeekday = .automatic // the calendar's own first weekday
calendarView.style = stylecalendarView.setDisplayDate(Date()) // jump to a month
calendarView.setDisplayDate(date, animated: true) // scroll to it
calendarView.goToNextMonth()
calendarView.goToPreviousMonth()
calendarView.isScrollEnabled = false // buttons only
calendarView.displayDate // the first day of the month on screenCall setDisplayDate from viewDidAppear or later; before that the view has no size to scroll.
calendarView.selectionMode = .multiple // .single, .multiple or .range
calendarView.selectDate(date) // same rules as a tap: in range, canSelectDate allows it
calendarView.deselectDate(date)
calendarView.selectRange(monday...friday)
calendarView.selectedDates // in selection order
calendarView.clearAllSelectedDates() // no delegate callbacks
calendarView.enableDeselection = false // taps cannot deselect; deselectDate still canIn .single mode selecting a day deselects the previous one and reports it. In .range mode the first tap picks one end, the second picks the other and every selectable day between them is selected, in either order and across months; the delegate then receives didSelectRange. A third tap starts a new range and tapping a selected day clears it. multipleSelectionEnable still works and maps to .single and .multiple.
CalendarView.Style is a value. Change what you need and assign it, or mutate the view's style in place.
var style = CalendarView.Style()
style.cellShape = .round // .round, .square or .bevel(radius)
style.cellColorToday = .systemOrange.withAlphaComponent(0.3)
style.cellSelectedBorderColor = .systemOrange
style.cellEventColor = .systemOrange
style.firstWeekday = .sunday // .sunday, .monday, .saturday or .automatic
style.showAdjacentDays = true // the neighbouring months' days in the empty cells
style.locale = Locale(identifier: "en_US")
calendarView.style = style
calendarView.style.headerFont = .preferredFont(forTextStyle: .title2) // also restyles
calendarView.marksWeekends = trueThe defaults are dynamic system colours, so a calendar follows dark mode, and fonts scaled for Dynamic Type. Custom colours and fonts are used as given.
The data source can replace the month title:
func headerString(_ date: Date) -> String? { date.formatted(.dateTime.month(.wide)) }The delegate can style single days. Return nil for the calendar's own style. Together with canSelectDate this greys out days the user must not pick:
func calendar(_ calendar: CalendarView, canSelectDate date: Date) -> Bool {
!Calendar.current.isDateInWeekend(date)
}
func calendar(_ calendar: CalendarView, styleForDate date: Date) -> CalendarView.Style? {
guard Calendar.current.isDateInWeekend(date) else { return nil }
var disabled = calendar.style
disabled.cellTextColorWeekend = disabled.cellColorOutOfRange
disabled.cellColorDefault = .clear
return disabled
}events is an array of CalendarEvent; a day gets a dot for every event that covers it, and didSelectDate receives that day's events.
calendarView.events = [
CalendarEvent(title: "Launch", startDate: launch, endDate: launch.addingTimeInterval(3600))
]To show the user's system calendars, add the KDCalendarEventKit product and declare NSCalendarsFullAccessUsageDescription in your Info.plist. The first call asks for access.
import KDCalendarEventKit
try await calendarView.loadEvents() // or the completion form
calendarView.addEvent("Dinner", date: date, duration: 2) // hours; false if access is missingimport KDCalendar
struct ContentView: View {
@State private var selection: [Date] = []
var body: some View {
KDCalendarView(range: start...end, selection: $selection)
.calendarStyle(style)
.selectionMode(.range)
.styleForDate { date in holidays.contains(date) ? holidayStyle : nil }
.events(events)
.displayDate(Date())
.onScrollToMonth { month in print(month) }
.aspectRatio(1, contentMode: .fit)
}
}Taps update the binding; assigning to it selects or deselects days.
Example/KDCalendarDemo.xcodeproj has three tabs: the classic styled calendar with system events, the default style in a vertical calendar with disabled Sundays and a holiday, and the SwiftUI wrapper with the three selection modes. Launch with -sampleEvents to seed events without calendar access and -tab 1 or -tab 2 to open a tab.
xcodebuild -scheme KDCalendar-Package -destination 'platform=iOS Simulator,name=iPhone 17 Pro' test
xcodebuild -project Example/KDCalendarDemo.xcodeproj -scheme KDCalendarDemo -destination 'platform=iOS Simulator,name=iPhone 17 Pro' build
xcrun swift-format lint --strict --recursive Sources Tests Example/KDCalendarDemo
Requires Xcode 26. Documentation is a DocC catalog in Sources/KDCalendar/KDCalendar.docc, including a guide for migrating from 1.x.
MIT. See LICENSE.