KDCalendar, a month grid for iOS with range selection and any calendar system
An Easy to Use Calendar for iOS (Swift 5.0)
At a glance
- What is it?
- KDCalendar is a UIKit and SwiftUI month calendar for iOS 17 and later that scrolls one month per page, marks today, supports range selection across months, and can render Gregorian, Persian, Hebrew or any other calendar system. It is styled by mutating a value type, and it draws events from EventKit only if you add the second product.
- Who is it for?
- KDCalendar suits an iOS team that needs a month grid with real selection behaviour rather than a date picker, and it suits an app that must show a non-Gregorian calendar, since the calendar identifier and locale are properties you set rather than a fork you maintain. It does not suit a project that needs week or day views, since it is a month calendar only, and it does not suit a team still on CocoaPods who expects bug fixes, because 2.0.1 is the last CocoaPods release.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 20 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A month grid, not a date picker
KDCalendar is a month calendar for iOS. The project description is precise about the shape of it: 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.
That is a different component from UIDatePicker. A date picker asks the user for one instant. A month grid shows a range of time and lets the user point at days. If your product needs a booking screen, a leave request form, a rental period picker or a hotel search, this is the shape you want, and the presence of a start date and an end date in the data source is the first hint of it.
The iOS support is modern. The feature list names Swift 6, iOS 17+, main-actor isolation, `Sendable` where it matters, and VoiceOver labels on every day. The package manifest for the release confirms it, with the 2.1.0 tag carrying the version and the release history showing 2.0.0 and 2.0.1 both on 2026-09-08 and 2.1.0 on 2026-10-01.
The audience is an iOS developer who has either written their own month grid and wants to stop, or who needs something the system does not offer, mainly range selection and non-Gregorian calendars.
The repository is a Swift package with `Sources/`, `Tests/`, an `Example/` project, a `Package.swift` and a `KDCalendar.podspec`.
One thing to notice at the top of the file list: there are `AGENTS.md` and `CLAUDE.md` files, along with `.claude/`, `.codex/`, `.cursor/` and `.agents/` directories, plus a `.beads/` issue tracker directory and an `.orchestra/` directory. This is a repository that has been developed with AI coding assistants configured at the project level.
Two entry points, one core
The library ships two front ends over a shared core, and the choice is made at the product level.
For UIKit there is `CalendarView`, which is a `UIView`. For SwiftUI there is `KDCalendarView` with a two-way selection binding, so the selected days flow into your state and back out of it. The optional `KDCalendarEventKit` product is a third piece, and it is the one that adds system calendar events.
That last point has a design implication worth spelling out. EventKit access requires user permission, and the documentation keeps it in a separate product so that an app which does not want to prompt for calendar access does not have to. You add `KDCalendar` for the calendar, and `KDCalendarEventKit` as well if you want system calendar events.
The UIKit setup is deliberately small.
import KDCalendar
let calendarView = CalendarView(frame: .zero)
calendarView.dataSource = self
calendarView.delegate = self
calendarView.direction = .horizontal
calendarView.selectionMode = .singleTwo properties drive everything. The `direction` picks horizontal or vertical paging, and the `selectionMode` picks the selection behaviour, and both are enums with a `.single` default you then change.
The memory management model is the detail to notice. The documentation states that the view holds both the data source and the delegate weakly, so you keep them alive yourself, and that a view controller that owns the calendar is the usual arrangement.
That is a deliberate choice, and it is the right one for a view that a controller hosts, but it is a source of retain-cycle bugs if you assign the view's delegate to something owned by the view. The example project is there to show the intended shape.
The data source defines which months exist
There is no `startDate` property to set. The range comes from the data source, and that has a consequence for how you plan the screen: the view cannot show a month until the data source is attached and has answered.
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 data source returns the first and last selectable day. Every month those days touch is shown, and days before the start or after the end are greyed out. Without a data source the view shows the current month, which is the fallback for a view you want to display before the controller is fully set up.
Note the sample's own date arithmetic. Both endpoints are computed with `Calendar.current` and a month offset, one backwards and one forwards, so the visible range follows the device calendar rather than a hardcoded date.
The delegate is where events and permissions come back, and only two of its 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]?) { }
}`canSelectDate` is the mechanism for disabling days, which is how you grey out a fully booked date or a closed holiday, and it is the reason the view does not need a separate disabled-days data source. The `withEvents` parameters carry the events for the tapped day, and in the long press callback the array is optional, so the delegate has to handle the case where EventKit is not linked in.
The scrolling callback receives the first day of the month, which is a convenient trigger for lazy-loading the events for that month.
Any calendar, with the week starting where the locale says
The calendar system is a property, and this is the feature that sets the library apart from a grid you draw yourself.
By default the view uses the user's current calendar and time zone. Gregorian, Persian, Hebrew or anything else goes through `Style.calendar`, and the week starts on the day the locale says. The example in the documentation sets a Persian calendar explicitly.
var style = CalendarView.Style()
style.calendar = Calendar(identifier: .persian)
style.firstWeekday = .automatic // the calendar's, or style.locale's if it has no locale
calendarView.style = styleThe `.automatic` value for `firstWeekday` resolves to the calendar's own first weekday, or the style's locale's if the calendar has no locale of its own. The style also has a `locale` property you can set directly, and the calendar used for all date arithmetic is `calendarView.calendar`, which is `style.calendar` and falls back to the style's locale when the calendar has no locale of its own.
The default is `Calendar.autoupdatingCurrent`, so the view follows the device when its time zone changes, and every date the view computes or hands out is the start of a day in that calendar. That is a specific convention rather than a vague one, and it is the kind of thing that causes off-by-one-day bugs when you compare a date from the view with a `Date()` you built yourself.
The practical consequence for a product with a non-Gregorian requirement, a Persian calendar app or a Hebrew calendar scheduling screen, is that you set two properties instead of writing a custom grid, a custom month arithmetic and a custom week layout.
The time zone side is worth being careful about. Because the calendar is autoupdating and the dates are day starts, a date handed to you is midnight in the device's time zone at the moment it was created. If your server expects UTC day boundaries, you have to convert, and the documentation does not provide a helper for that.
Range selection across months, and the selection that survives a reload
Selection is the feature that separates this from a grid you write in an afternoon, and it comes in three modes.
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 through the delegate. 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, after which the delegate receives `didSelectRange`. A third tap starts a new range, and tapping a selected day clears it. The older `multipleSelectionEnable` flag still works and maps onto `.single` and `.multiple`, so existing code keeps compiling.
Two behaviours are easy to miss. `selectDate` follows the same rules as a tap, including in range mode where `canSelectDate` can veto it, so programmatic selection is not a back door around your own rules. And `clearAllSelectedDates()` produces no delegate callbacks, which is what you want when you are resetting state, and what you do not want if you were relying on the delegate to mirror selections.
The selection is kept as days, not as indices. When `reloadData()` or a new style rebuilds the grid, the same days stay selected wherever they land, and days that are no longer in range are deselected and reported with `didDeselectDate`.
That last sentence is the one to design around. Your delegate has to handle deselections that are not user gestures, because a data source change can silently drop selected days out of the selectable range. If your app treats a deselection as a user action that clears a field, this will surprise you.
Styling by mutating a value, with system colours by default
`CalendarView.Style` is a struct, and the documentation makes the point that change a property and the view follows. That is a value-type configuration model, and it has a real consequence: you build a style, assign it, and the view renders from it.
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.weekdayCasing = .uppercase // the weekday labels: .capitalized or .uppercase
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, which is the documented break: if you set a fixed colour, you lose the automatic dark mode behaviour for that element.
The default calendar system colours being dynamic is what makes the component drop into a modern iOS app without a design pass. That is also why the per-day style hook exists, documented as a way to style single days and to return nil, which is the mechanism for holidays and disabled days.
There is also a way to replace the month title, through a `title(forMonth:)` function on the data source.
func title(forMonth month: Date) -> String? { month.formatted(.dateTime.month(.wide)) }Returning nil falls back to the built-in title, so you can override only the months you care about.
One thing the style does not cover is the overall layout of the grid beyond the cell shape, so a design that needs a different day cell layout, a week-number column or a dense month view is a fork, not a configuration.
Installation, the CocoaPods cut-off and the example project
Installation goes through Swift Package Manager, and the two documented routes are the Xcode UI and the manifest.
In Xcode you choose File, then Add Package Dependencies, and enter the repository URL.
https://github.com/mmick66/CalendarViewOr in `Package.swift` you add the package dependency.
.package(url: "https://github.com/mmick66/CalendarView", from: "2.1.0")Then you add the `KDCalendar` product to your target, and `KDCalendarEventKit` as well if you want system calendar events.
The CocoaPods route exists but is closed. The documentation states that 2.0.1 is the final CocoaPods release, that later fixes ship through Swift Package Manager only, and that Swift Package Manager is preferred.
pod 'KDCalendar', '~> 2.0' # the calendar
pod 'KDCalendar/EventKit', '~> 2.0' # plus system eventsThat is a genuine upgrade decision for an existing project. A team on CocoaPods cannot receive the 2.1.0 fixes, and the 2.0 to 2.1 boundary is where the new release work lands, so staying put means shipping a slightly older calendar for the life of the app.
The example project is the other part of the first-use story. The `Example/` directory holds `KDCalendarDemo.xcodeproj` and `KDCalendarDemo`, and the repository's own demo image shows the example app with the classic style, the default style, the SwiftUI wrapper and dark mode, which is the fastest way to see what the component looks like before writing code against it.
The licence is MIT, with a LICENSE file at the top of the repository. The last push was on 2026-10-05, two days before the 2.1.0 tag was published on 2026-10-01, and the project is not archived, so the 2.x line is where new work is going.
Editorial conclusion
KDCalendar suits an iOS team that needs a month grid with real selection behaviour rather than a date picker, and it suits an app that must show a non-Gregorian calendar, since the calendar identifier and locale are properties you set rather than a fork you maintain. It does not suit a project that needs week or day views, since it is a month calendar only, and it does not suit a team still on CocoaPods who expects bug fixes, because 2.0.1 is the last CocoaPods release. Verify first that the 2.0 line to 2.1.0 changes do not affect your call sites, that the date semantics of your app match the start-of-day convention the view uses, and that the data source and delegate ownership model, which holds both weakly, is compatible with how your view controllers are structured.
Frequently asked questions
How do I install KDCalendar in an iOS project?
Add the package through Swift Package Manager, either from Xcode with File, Add Package Dependencies, or in Package.swift with .package(url: "https://github.com/mmick66/CalendarView", from: "2.1.0"). Then add the KDCalendar product to your target, and KDCalendarEventKit as well if you want system calendar events.
Can KDCalendar show a non-Gregorian calendar?
Yes. The view uses the user's current calendar and time zone by default, and you can set Gregorian, Persian, Hebrew or any other calendar through style.calendar, with the week starting on the day the locale says.
How does range selection work in KDCalendar?
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, after which the delegate receives didSelectRange. A third tap starts a new range and tapping a selected day clears it.
Is KDCalendar still available through CocoaPods?
The podspec is still in the repository and the KDCalendar and KDCalendar/EventKit subspecs exist, but the documentation states that 2.0.1 is the final CocoaPods release and later fixes ship through Swift Package Manager only.
How do I style the KDCalendar view?
CalendarView.Style is a struct, so you set the properties you need and assign it to the view, or mutate the view's style in place. The defaults are dynamic system colours and Dynamic Type scaled fonts, and there is a per-day style hook for holidays and disabled days.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/mmick66-calendarview)Was this useful?