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.
If you enjoyed this article, please feel free to follow me on my social media: