A flexible and highly customizable SwiftUI calendar library inspired by Android's Jetpack Compose calendar implementations. MagicCalendar provides a comprehensive solution for displaying calendars with support for various selection modes, custom themes, event management, and complete customization flexibility.
✨ Flexible Date Selection
- Single date selection
- Multiple date selection
- Date range selection
- No selection mode
🎨 Customizable Themes
- Built-in themes (Default, Dark, Minimal, Colorful)
- Fully customizable color schemes
- Typography customization
- Spacing and sizing controls
📅 Event Management
- Add, remove, and display events
- Color-coded event indicators
- Multiple events per date
- Event types and categories
⚙️ Configuration Options
- Configurable first day of week
- Date range restrictions
- Past/future selection control
- Weekend highlighting
🛠 Custom Day Views
- Complete day cell customization
- Custom shapes and layouts
- Animation support
- Interaction handling
Add MagicCalendar to your project using Swift Package Manager:
- In Xcode, go to File → Add Package Dependencies
- Enter the repository URL
- Select the version you want to use
- Click Add Package
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/yourusername/MagicCalendar.git", from: "1.0.0")
]import SwiftUI
import MagicCalendar
struct ContentView: View {
var body: some View {
CalendarView()
}
}import SwiftUI
import MagicCalendar
struct ContentView: View {
@State private var selectedDate: Date? = nil
@State private var selectedDates: Set<Date> = []
@State private var events: [Date: [CalendarEvent]] = [:]
var body: some View {
CalendarView(
selectedDate: $selectedDate,
selectedDates: $selectedDates,
events: $events
)
}
}CalendarView(
selectedDate: $selectedDate,
selectedDates: $selectedDates,
events: $events,
theme: .dark
)@State private var selectedDate: Date? = nil
@State private var selectedDates: Set<Date> = []
@State private var events: [Date: [CalendarEvent]] = [:]
var body: some View {
CalendarView(
selectedDate: $selectedDate,
selectedDates: $selectedDates,
events: $events,
configuration: CalendarConfiguration(selectionMode: .multiple)
)
Text("Selected: \(selectedDates.count) dates")
}@State private var selectedDate: Date? = nil
@State private var selectedDates: Set<Date> = []
@State private var events: [Date: [CalendarEvent]] = [:]
var body: some View {
VStack {
CalendarView(
selectedDate: $selectedDate,
selectedDates: $selectedDates,
events: $events,
configuration: CalendarConfiguration(selectionMode: .range)
)
if selectedDates.count >= 2 {
let dates = selectedDates.sorted()
let range = dates.first!...dates.last!
Text("Selected range: \(DateFormatter.localizedString(from: range.lowerBound, dateStyle: .medium, timeStyle: .none)) - \(DateFormatter.localizedString(from: range.upperBound, dateStyle: .medium, timeStyle: .none))")
}
}
}@State private var selectedDate: Date? = nil
@State private var selectedDates: Set<Date> = []
@State private var events: [Date: [CalendarEvent]] = [:]
var body: some View {
CalendarView(
selectedDate: $selectedDate,
selectedDates: $selectedDates,
events: $events
)
.onAppear {
addSampleEvents()
}
.onChange(of: selectedDate) { date in
if let date = date {
addEventToDate(date)
}
}
}
private func addSampleEvents() {
let event = CalendarEvent(
title: "Meeting",
date: Date(),
color: .blue,
type: .meeting
)
let normalizedDate = Calendar.current.startOfDay(for: Date())
events[normalizedDate] = [event]
}
private func addEventToDate(_ date: Date) {
let newEvent = CalendarEvent(
title: "New Event",
date: date,
color: .green,
type: .event
)
let normalizedDate = Calendar.current.startOfDay(for: date)
if events[normalizedDate] == nil {
events[normalizedDate] = []
}
events[normalizedDate]?.append(newEvent)
}let configuration = CalendarConfiguration(
selectionMode: .single,
firstDayOfWeek: .monday,
allowPastSelection: false,
minimumDate: Date(),
maximumDate: Calendar.current.date(byAdding: .year, value: 1, to: Date())
)
@StateObject private var viewModel = CalendarViewModel(configuration: configuration)let customTheme = CalendarTheme(
colors: CalendarTheme.Colors(
primary: .purple,
selectedBackground: .purple,
selectedForeground: .white,
todayBackground: .orange,
weekendForeground: .red
),
typography: CalendarTheme.Typography(
headerFont: .title.bold(),
dayFont: .callout
),
spacing: CalendarTheme.Spacing(
daySpacing: 4,
weekSpacing: 12
),
sizing: CalendarTheme.Sizing(
daySize: 50,
cornerRadius: 12
)
)
CalendarView()
.theme(customTheme)CalendarView(viewModel: viewModel)
.customDayView { day, theme in
ZStack {
Circle()
.fill(day.isSelected ? theme.colors.selectedBackground : Color.clear)
Text(String(day.day))
.font(theme.typography.dayFont)
.foregroundColor(day.isSelected ? theme.colors.selectedForeground : theme.colors.onBackground)
}
.frame(width: theme.sizing.daySize, height: theme.sizing.daySize)
}The main calendar view component.
public struct CalendarView: View {
public init(
selectedDate: Binding<Date?>,
selectedDates: Binding<Set<Date>>,
events: Binding<[Date: [CalendarEvent]]>,
configuration: CalendarConfiguration = CalendarConfiguration(),
theme: CalendarTheme = .default,
customDayView: ((CalendarDay, CalendarTheme) -> AnyView)? = nil
)
}Manages calendar state, navigation, and date selection.
@MainActor
public class CalendarViewModel: ObservableObject {
public func nextMonth()
public func previousMonth()
public func navigate(to date: Date)
public func goToToday()
public func selectDate(_ date: Date)
public func addEvent(_ event: CalendarEvent, to date: Date)
public func removeEvent(_ event: CalendarEvent, from date: Date)
public func events(for date: Date) -> [CalendarEvent]
}Configuration options for calendar behavior.
public struct CalendarConfiguration {
public let selectionMode: CalendarSelectionMode
public let firstDayOfWeek: WeekDay
public let allowPastSelection: Bool
public let allowFutureSelection: Bool
public let minimumDate: Date?
public let maximumDate: Date?
}Comprehensive theming system.
public struct CalendarTheme {
public let colors: Colors
public let typography: Typography
public let spacing: Spacing
public let sizing: Sizing
}Event model for calendar events.
public struct CalendarEvent: Identifiable, Equatable, Hashable {
public let title: String
public let date: Date
public let color: EventColor
public let type: EventType
}Standard iOS-style calendar with blue accents.
Dark mode compatible theme with appropriate contrast.
Clean, minimal design with subtle styling.
Vibrant theme with purple and pink accents.
.event- General events.reminder- Reminders.birthday- Birthday events.holiday- Holiday events.meeting- Meeting events
.red,.blue,.green,.orange,.purple,.pink,.yellow,.gray
.single- Select one date at a time.multiple- Select multiple individual dates.range- Select a continuous range of dates.none- No selection allowed
The package includes comprehensive examples demonstrating various use cases:
BasicCalendarExample- Simple calendar setupDarkThemeCalendarExample- Dark theme usageMultiSelectionCalendarExample- Multiple date selectionRangeSelectionCalendarExample- Date range selectionEventCalendarExample- Calendar with eventsCustomDayViewCalendarExample- Custom day cell renderingMondayFirstCalendarExample- Monday as first day of weekLimitedDateRangeCalendarExample- Restricted date selection
- iOS 16.0+
- macOS 13.0+
- Swift 6.0+
Contributions are welcome! Please feel free to submit a Pull Request.
MagicCalendar is available under the MIT license. See the LICENSE file for more info.
Inspired by Android's Jetpack Compose calendar implementations, adapted for SwiftUI with additional features and customization options.