Building a Calendar UI with SwiftUI

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:

  1. Open Xcode and select “Create a new Xcode project.”
  2. Choose the “App” template under the iOS tab (or macOS, watchOS, etc.).
  3. 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: A LazyVGrid that 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.