GitHub-Like Contribution Chart in SwiftUI


Greetings, traveler!

In this tutorial, we’ll create a GitHub-like contribution chart in SwiftUI for the habit-tracker app. This chart visualizes user progress over a given period, with each day represented by a colored block indicating the percentage of tasks completed. We’ll also add a date picker to allow users to select different months and years. By the end of this tutorial, you’ll have a fully functional contribution chart that dynamically updates based on the selected date.

Note

By the way, you can find another tutorial about SwiftUI Week Calendar View here.

Data Model

We’ll create a TrackerItem struct to represent the tracked habit, including the completions dictionary and the total number of actions expected for each day.

struct TrackerItem {
    var completions: [Date: Int]
    var total: Int
}

ViewModel

Next, we’ll create a TrackerViewModel class to manage our data and handle date selection. This class will use the Observable macro to ensure SwiftUI updates the view when data changes.

import Observation
import Foundation

@Observable
final class ContributionChartViewModel {
    
    private var trackerItem: TrackerItem

    init(trackerItem: TrackerItem) {
        self.trackerItem = trackerItem
    }

    func percentageFor(date: Date) -> Double {
        let completed = trackerItem.completions[date.startOfDay] ?? .zero
        return Double(completed) / Double(trackerItem.total)
    }
    
    func weeksInMonth(startOfMonth: Date) -> Int {
        let calendar = Calendar.current
        let range = calendar.range(of: .weekOfMonth, in: .month, for: startOfMonth)
        return range?.count ?? .zero
    }
    
    func dayFor(week: Int, dayOfWeek: Int, startOfMonth: Date, daysInMonth: Int) -> Date? {
        let calendar = Calendar.current
        let firstWeekday = calendar.firstWeekday
        let startDayOffset = (calendar.component(.weekday, from: startOfMonth) - firstWeekday + 7) % 7
        let dayIndex = week * 7 + dayOfWeek - startDayOffset
        guard dayIndex >= 0, dayIndex < daysInMonth else { return nil }
        return calendar.date(byAdding: .day, value: dayIndex, to: startOfMonth)
    }
    
}

extension Date {
    var startOfDay: Date {
        Calendar.current.startOfDay(for: self)
    }
}

View

We’ll create the ContributionChartView to display the contribution blocks. Each block represents a day, and its color indicates the percentage of the task completed that day.

struct ContributionChartView: View {
    
    @Binding private var date: Date
    @State private var viewModel: ContributionChartViewModel
    
    init(
        date: Binding<Date>,
        viewModel: ContributionChartViewModel
    ) {
        _date = date
        self.viewModel = viewModel
    }
    
    var body: some View {
        let calendar = Calendar.current
        let startOfMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: date)) ?? .now
        let daysInMonth = calendar.range(of: .day, in: .month, for: startOfMonth)?.count ?? .zero
        
        VStack(alignment: .center) {
            ForEach(0..<viewModel.weeksInMonth(startOfMonth: startOfMonth), id: \.self) { week in
                HStack {
                    ForEach(0..<7, id: \.self) { dayOfWeek in
                        if let day = viewModel.dayFor(
                            week: week,
                            dayOfWeek: dayOfWeek,
                            startOfMonth: startOfMonth,
                            daysInMonth: daysInMonth
                        ) {
                            ContributionBlockView(
                                date: day,
                                viewModel: viewModel
                            )
                        } else {
                            Rectangle()
                                .fill(Color.clear)
                                .frame(width: 38, height: 38)
                        }
                    }
                }
            }
        }
    }
    
}

private struct ContributionBlockView: View {
    let date: Date
    var viewModel: ContributionChartViewModel
    
    var body: some View {
        let percentage = viewModel.percentageFor(date: date)
        let color = Color.accentColor.opacity(percentage)
        
        Rectangle()
            .fill(color)
            .frame(width: 38, height: 38)
            .cornerRadius(8)
            .overlay(
                Text("\(Calendar.current.component(.day, from: date))")
                    .foregroundColor(.white)
            )
    }
}

Month and Year Picker

We’ll add a Custom Date Picker to allow users to select specific year and month. The selected date will be passed on to ContributionChartView.

import SwiftUI

struct MonthYearPicker: View {
    
    @Binding var date: Date
    
    @State private var selectedMonthIndex: Int
    @State private var selectedYear: Int
    
    private let months = Calendar.current.monthSymbols
    private let years = Array(2020...2100)
    
    init(date: Binding<Date>) {
        _date = date
        self.selectedMonthIndex = Calendar.current.component(.month, from: date.wrappedValue) - 1
        self.selectedYear = Calendar.current.component(.year, from: date.wrappedValue)
    }

    var body: some View {
        HStack {
            Picker("", selection: $selectedMonthIndex) {
                ForEach(0..<months.count, id: \.self) { index in
                    Text(months[index]).tag(index)
                }
            }
            .labelsHidden()
            .pickerStyle(.menu)
            .onChange(of: selectedMonthIndex) { _, _ in
                setDate()
            }

            Picker("", selection: $selectedYear) {
                ForEach(years, id: \.self) { year in
                    Text(String(year)).tag(year)
                }
            }
            .labelsHidden()
            .pickerStyle(.menu)
            .onChange(of: selectedYear) { _, _ in
                setDate()
            }
        }
    }
    
    private func setDate() {
        let components = DateComponents(
            year: selectedYear,
            month: selectedMonthIndex + 1
        )
        date = Calendar.current.date(from: components) ?? .now
    }
}

Conclusion

With these steps, you’ve created a GitHub-like contribution chart in SwiftUI. The date picker allows users to select months and years, and the chart updates to reflect the chosen period.