Habit tracking is a powerful method for building and maintaining positive routines. SwiftUI, Apple’s modern declarative UI framework, makes building a habit tracker app a straightforward and enjoyable process. In this guide, we’ll walk through creating a simple yet effective habit tracker app using SwiftUI, covering data modeling, UI design, and data persistence.
What is a Habit Tracker?
A habit tracker is an app or tool that helps you monitor and manage your habits. It typically allows you to define habits you want to track, record your progress, and view statistics related to your habits over time. The goal is to provide insights into your habits, helping you stay motivated and consistent.
Why Build a Habit Tracker?
- Improved Consistency: Encourages you to maintain your habits.
- Increased Awareness: Helps you monitor progress and identify patterns.
- Motivation Boost: Provides visual feedback on accomplishments.
Prerequisites
- Basic knowledge of Swift and SwiftUI.
- Xcode installed on your macOS machine.
Step 1: Setting Up the Project
- Open Xcode and create a new project.
- Select the “App” template under the iOS tab.
- Enter “HabitTracker” as the product name, choose SwiftUI for the interface, and ensure “Use Core Data” is not selected.
- Click “Create” and choose a location to save your project.
Step 2: Defining the Data Model
First, let’s define a data model for a habit. Create a new Swift file named Habit.swift
and add the following code:
import Foundation
struct Habit: Identifiable, Codable {
var id = UUID()
var name: String
var description: String
var frequency: [DayOfWeek] // Days of the week to perform the habit
var completionDates: [Date] // Dates when the habit was completed
var color: String //String representation of a color
init(id: UUID = UUID(), name: String, description: String, frequency: [DayOfWeek], completionDates: [Date], color: String) {
self.id = id
self.name = name
self.description = description
self.frequency = frequency
self.completionDates = completionDates
self.color = color
}
func isCompletedToday() -> Bool {
guard let today = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month, .day], from: Date())) else {
return false
}
return completionDates.contains { completedDate in
let completedDateOnly = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month, .day], from: completedDate))
return completedDateOnly == today
}
}
}
enum DayOfWeek: String, CaseIterable, Codable {
case sunday = "Sun"
case monday = "Mon"
case tuesday = "Tue"
case wednesday = "Wed"
case thursday = "Thu"
case friday = "Fri"
case saturday = "Sat"
}
In this model:
id
: A unique identifier for each habit.name
: The name of the habit (e.g., “Drink Water”).description
: A brief description of the habit.frequency
: An array ofDayOfWeek
enum cases representing on which days a user wants to perform the habit.completionDates
: An array ofDate
objects representing the dates when the habit was completed.color
: String representation of the color for habit representation in the UI.DayOfWeek
: Enum of days of week
Step 3: Creating a Habit List View
Create the main view that will display the list of habits. Replace the contents of ContentView.swift
with the following code:
import SwiftUI
struct ContentView: View {
@State private var habits: [Habit] = []
@State private var isAddingHabit = false
var body: some View {
NavigationView {
List {
ForEach(habits) { habit in
HabitRow(habit: habit, onToggleCompletion: {
toggleCompletion(habit: habit)
})
}
.onDelete(perform: deleteHabit)
}
.navigationTitle("Habit Tracker")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
isAddingHabit = true
} label: {
Image(systemName: "plus")
}
}
}
.sheet(isPresented: $isAddingHabit) {
AddHabitView(habits: $habits, isPresented: $isAddingHabit)
}
.onAppear {
loadHabits()
}
}
}
// Function to toggle completion
func toggleCompletion(habit: Habit) {
if let index = habits.firstIndex(where: { $0.id == habit.id }) {
var habitCopy = habits[index]
if habitCopy.isCompletedToday() {
// Remove the completion date for today
habitCopy.completionDates.removeAll { date in
let dateOnly = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month, .day], from: date))
let today = Calendar.current.date(from: Calendar.current.dateComponents([.year, .month, .day], from: Date()))
return dateOnly == today
}
} else {
// Add the completion date for today
habitCopy.completionDates.append(Date())
}
habits[index] = habitCopy
saveHabits()
}
}
func deleteHabit(at offsets: IndexSet) {
habits.remove(atOffsets: offsets)
saveHabits()
}
// Load habits from UserDefaults
func loadHabits() {
if let data = UserDefaults.standard.data(forKey: "Habits") {
if let decodedHabits = try? JSONDecoder().decode([Habit].self, from: data) {
habits = decodedHabits
return
}
}
habits = []
}
// Save habits to UserDefaults
func saveHabits() {
if let encodedData = try? JSONEncoder().encode(habits) {
UserDefaults.standard.set(encodedData, forKey: "Habits")
}
}
}
Here, ContentView
:
- Displays a list of habits.
- Includes a button to add new habits.
- Uses
@State
to manage the list of habits and the state of the habit creation sheet.
Step 4: Creating a Habit Row View
Now, let’s create a view for each habit row. Create a new Swift file named HabitRow.swift
and add the following code:
import SwiftUI
struct HabitRow: View {
let habit: Habit
let onToggleCompletion: () -> Void
var body: some View {
HStack {
Rectangle()
.fill(Color(hex: habit.color) ?? .blue)
.frame(width: 10)
VStack(alignment: .leading) {
Text(habit.name)
.font(.headline)
Text(habit.description)
.font(.subheadline)
.foregroundColor(.gray)
}
Spacer()
Button(action: {
onToggleCompletion()
}) {
Image(systemName: habit.isCompletedToday() ? "checkmark.circle.fill" : "circle")
.foregroundColor(habit.isCompletedToday() ? Color(hex: habit.color) ?? .blue : .gray)
}
}
}
}
//Extension to use hexcodes in Color
extension Color {
init?(hex: String) {
var formattedHex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
if formattedHex.count == 6 {
formattedHex = "FF" + formattedHex
} else if formattedHex.count != 8 {
return nil
}
var rgbValue: UInt64 = 0
Scanner(string: formattedHex).scanHexInt64(&rgbValue)
let red = Double((rgbValue & 0xFF0000) >> 16) / 255.0
let green = Double((rgbValue & 0x00FF00) >> 8) / 255.0
let blue = Double(rgbValue & 0x0000FF) / 255.0
self.init(red: red, green: green, blue: blue)
}
}
In this view:
- The Habit Row will show the name and the description of the habit
HStack
to display elements horizontally: a coloredRectangle
, the habit description (name, description), and a completion button.- Display status of habit(completed today) and based on that mark the image accordingly
- The
Color Extension
converts the string hexcodes for representation in Color
Step 5: Creating an Add Habit View
Create a view to add new habits. Create a new Swift file named AddHabitView.swift
and add the following code:
import SwiftUI
struct AddHabitView: View {
@Binding var habits: [Habit]
@Binding var isPresented: Bool
@State private var name: String = ""
@State private var description: String = ""
@State private var selectedDays: [DayOfWeek] = []
@State private var selectedColor: String = "007AFF" // Default blue color
let availableColors = ["007AFF", "34C759", "5856D6", "FF9500", "FF3B30"] // Example color palette
var body: some View {
NavigationView {
Form {
Section(header: Text("Habit Details")) {
TextField("Name", text: $name)
TextField("Description", text: $description)
}
Section(header: Text("Frequency")) {
ForEach(DayOfWeek.allCases, id: \.self) { day in
Button(action: {
toggleDay(day)
}) {
HStack {
Text(day.rawValue)
Spacer()
if selectedDays.contains(day) {
Image(systemName: "checkmark")
}
}
}
.foregroundColor(.primary)
}
}
Section(header: Text("Color")) {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(availableColors, id: \.self) { color in
Circle()
.fill(Color(hex: color) ?? .gray)
.frame(width: 30, height: 30)
.overlay(
Circle()
.strokeBorder(.gray, lineWidth: selectedColor == color ? 3 : 0)
)
.onTapGesture {
selectedColor = color
}
}
}
}
}
}
.navigationTitle("Add New Habit")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
isPresented = false
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveHabit()
isPresented = false
}
}
}
}
}
private func toggleDay(_ day: DayOfWeek) {
if selectedDays.contains(day) {
selectedDays.removeAll { $0 == day }
} else {
selectedDays.append(day)
}
}
private func saveHabit() {
let newHabit = Habit(name: name, description: description, frequency: selectedDays, completionDates: [], color: selectedColor)
habits.append(newHabit)
// Sort habits array to keep consistency
habits.sort { $0.name < $1.name }
saveHabitsToUserDefaults()
}
private func saveHabitsToUserDefaults() {
if let encodedData = try? JSONEncoder().encode(habits) {
UserDefaults.standard.set(encodedData, forKey: "Habits")
}
}
}
In this view:
- The Add Habit Row has 3 sections: "Habit Details", "Frequency", and "Color"
- In the first section: the view will take the habit details like:
name
anddescription
of the habit - In the second section: you can mark the days of the week for a given habit using
DaysOfWeek
enums, stored in an array:selectedDays
- In the third section: color pallete is represented from an array and user has an option to select a particular color, selected color will have a 3-pixel border stroke(selected state)
- When the form is submitted the details are stored in
saveHabit()
to persist for offline usage, its sorted alphabetically usingsort
.
Step 6: Running the App
Now, run your app in the Xcode simulator or on your physical device. You should see a list of habits (initially empty) and an option to add new habits using the "+" button.
Conclusion
Congratulations! You’ve built a basic habit tracker app using SwiftUI. This foundation can be extended with features like progress tracking, reminders, streak counters, and more detailed analytics. SwiftUI’s declarative syntax and powerful features make it an excellent choice for creating modern and engaging habit tracking apps.