SwiftUI provides a declarative way to build user interfaces across all Apple platforms. One common UI component is a calendar, which can range from simple date pickers to fully-featured calendar applications. In this comprehensive guide, we’ll walk through building a calendar UI using SwiftUI, covering everything from basic layouts to advanced interactions.
Why Build a Calendar UI?
- Scheduling: Enables users to schedule events and appointments.
- Date Selection: Simplifies date selection in forms or settings.
- Visualization: Provides a visual representation of dates and events.
Prerequisites
Before you start, ensure you have:
- Xcode installed
- Basic knowledge of Swift and SwiftUI
Step 1: Setting Up the Project
Create a new Xcode project:
- Open Xcode and select “Create a new Xcode project.”
- Choose the “App” template under the iOS tab (or macOS, watchOS, etc.).
- Name your project “CalendarUI” and select SwiftUI as the interface.
Step 2: Creating the Basic Calendar Structure
Let’s start by building the basic structure of the calendar, including the header with the month and year, and the days of the week.
Create a Model to Manage Date Information
First, create a new Swift file named CalendarModel.swift and define a class to handle calendar-related logic:
import Foundation
class CalendarModel: ObservableObject {
@Published var currentDate: Date = Date()
let calendar: Calendar = .current
func nextMonth() {
guard let nextMonth = calendar.date(byAdding: .month, value: 1, to: currentDate) else {
return
}
currentDate = nextMonth
}
func previousMonth() {
guard let previousMonth = calendar.date(byAdding: .month, value: -1, to: currentDate) else {
return
}
currentDate = previousMonth
}
func getDaysInMonth() -> [Date] {
guard let range = calendar.range(of: .day, in: .month, for: currentDate) else {
return []
}
return range.compactMap { day -> Date? in
return calendar.date(bySetting: .day, value: day, of: currentDate)
}
}
func extractDate(date: Date, format: String) -> String {
let formatter = DateFormatter()
formatter.dateFormat = format
return formatter.string(from: date)
}
}
This class will manage the current date, handle month navigation, and extract date-related information.
Build the SwiftUI View
Open ContentView.swift and modify it to use the CalendarModel. This includes the calendar header, day labels, and the grid of days.
import SwiftUI
struct ContentView: View {
@ObservedObject var calendarModel = CalendarModel()
var body: some View {
VStack {
CalendarHeader(calendarModel: calendarModel)
DayLabels()
CalendarGrid(calendarModel: calendarModel)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CalendarHeader: View {
@ObservedObject var calendarModel: CalendarModel
var body: some View {
HStack {
Button(action: {
calendarModel.previousMonth()
}) {
Image(systemName: "chevron.left")
}
Spacer()
Text("(calendarModel.extractDate(date: calendarModel.currentDate, format: "MMMM yyyy"))")
.font(.title)
Spacer()
Button(action: {
calendarModel.nextMonth()
}) {
Image(systemName: "chevron.right")
}
}
.padding()
}
}
struct DayLabels: View {
let dayStrings = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
var body: some View {
HStack {
ForEach(dayStrings, id: .self) { day in
Text(day)
.frame(maxWidth: .infinity)
.padding(.vertical, 5)
}
}
.padding(.horizontal)
}
}
struct CalendarGrid: View {
@ObservedObject var calendarModel: CalendarModel
var body: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7)) {
ForEach(calendarModel.getDaysInMonth(), id: .self) { day in
DayView(day: day, calendarModel: calendarModel)
}
}
.padding(.horizontal)
}
}
struct DayView: View {
let day: Date
@ObservedObject var calendarModel: CalendarModel
var body: some View {
Text("(calendarModel.calendar.component(.day, from: day))")
.padding(8)
.frame(maxWidth: .infinity)
.background(isToday(day) ? Color.blue : Color.clear)
.foregroundColor(isToday(day) ? .white : .black)
.clipShape(Circle())
}
private func isToday(_ date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDateInToday(date)
}
}
Explanation of the Code
CalendarHeader: Displays the current month and year and includes buttons for navigating to the next and previous months.DayLabels: Displays the abbreviated days of the week.CalendarGrid: ALazyVGridthat displays the days of the month. It adapts to fit the available width, creating a grid layout.DayView: Represents a single day in the calendar. Highlights the current day.
Step 3: Implementing Date Selection
Now, let’s add the ability to select dates and highlight the selected date.
Update CalendarModel
Add a new property to CalendarModel to store the selected date and a method to set it:
@Published var selectedDate: Date?
func selectDate(date: Date) {
selectedDate = date
}
func isDateSelected(date: Date) -> Bool {
guard let selectedDate = selectedDate else {
return false
}
return calendar.isDate(date, inSameDayAs: selectedDate)
}
Modify DayView
Update DayView to handle selection and highlight the selected date:
struct DayView: View {
let day: Date
@ObservedObject var calendarModel: CalendarModel
var body: some View {
Text("(calendarModel.calendar.component(.day, from: day))")
.padding(8)
.frame(maxWidth: .infinity)
.background(
ZStack {
if isToday(day) {
Color.blue
}
if calendarModel.isDateSelected(date: day) {
Color.gray.opacity(0.5)
}
}
)
.foregroundColor(
(isToday(day) || calendarModel.isDateSelected(date: day)) ? .white : .black
)
.clipShape(Circle())
.onTapGesture {
calendarModel.selectDate(date: day)
}
}
private func isToday(_ date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDateInToday(date)
}
}
Step 4: Adding Events
To make the calendar more functional, let’s add the ability to display events. First, we need an event model and sample data.
Create an Event Model
Create a new Swift file called Event.swift and define an Event struct:
import Foundation
struct Event: Identifiable {
let id = UUID()
let title: String
let date: Date
let description: String
}
Add Sample Event Data
Modify the CalendarModel to include an array of events:
@Published var events: [Event] = [
Event(title: "Meeting with John", date: Date(), description: "Discuss project updates"),
Event(title: "Doctor's Appointment", date: Calendar.current.date(byAdding: .day, value: 2, to: Date())!, description: "Checkup"),
Event(title: "Birthday Party", date: Calendar.current.date(byAdding: .day, value: 5, to: Date())!, description: "Celebrate Jane's birthday")
]
Display Events in DayView
Modify DayView to show a dot indicator if there are events on that day:
struct DayView: View {
let day: Date
@ObservedObject var calendarModel: CalendarModel
var body: some View {
ZStack {
Text("(calendarModel.calendar.component(.day, from: day))")
.padding(8)
.frame(maxWidth: .infinity)
.background(
ZStack {
if isToday(day) {
Color.blue
}
if calendarModel.isDateSelected(date: day) {
Color.gray.opacity(0.5)
}
}
)
.foregroundColor(
(isToday(day) || calendarModel.isDateSelected(date: day)) ? .white : .black
)
.clipShape(Circle())
.onTapGesture {
calendarModel.selectDate(date: day)
}
if hasEvents(on: day) {
VStack {
Spacer()
Circle()
.fill(Color.red)
.frame(width: 5, height: 5)
}
}
}
}
private func isToday(_ date: Date) -> Bool {
let calendar = Calendar.current
return calendar.isDateInToday(date)
}
private func hasEvents(on date: Date) -> Bool {
return calendarModel.events.contains { event in
return Calendar.current.isDate(event.date, inSameDayAs: date)
}
}
}
Create a View to Display Event Details
Create a new view called EventListView.swift to display the events on the selected day:
import SwiftUI
struct EventListView: View {
@ObservedObject var calendarModel: CalendarModel
var body: some View {
List {
ForEach(getEventsForSelectedDate()) { event in
VStack(alignment: .leading) {
Text(event.title).font(.headline)
Text(event.description).font(.subheadline)
}
}
}
}
private func getEventsForSelectedDate() -> [Event] {
guard let selectedDate = calendarModel.selectedDate else {
return []
}
return calendarModel.events.filter { event in
return Calendar.current.isDate(event.date, inSameDayAs: selectedDate)
}
}
}
Finally, add this view to ContentView:
struct ContentView: View {
@ObservedObject var calendarModel = CalendarModel()
var body: some View {
VStack {
CalendarHeader(calendarModel: calendarModel)
DayLabels()
CalendarGrid(calendarModel: calendarModel)
if calendarModel.selectedDate != nil {
EventListView(calendarModel: calendarModel)
}
}
}
}
Step 5: Enhancements and Customizations
Here are some additional enhancements you can add:
- Localization: Support multiple languages by localizing date formats and day names.
- Theming: Allow users to customize the calendar’s appearance with different themes.
- Navigation: Add gestures for navigating between months and years.
- Event Creation: Implement functionality for users to create and edit events directly from the calendar.
- Integration: Integrate with system calendars and reminders.
Conclusion
Building a calendar UI in SwiftUI is a great way to learn about layout management, data binding, and user interactions. By following the steps outlined in this guide, you can create a functional and visually appealing calendar component that meets your specific needs. With the flexibility of SwiftUI, the possibilities for customization and enhancement are virtually endless.