How to Build a Habit Tracker in SwiftUI

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

  1. Open Xcode and create a new project.
  2. Select the “App” template under the iOS tab.
  3. Enter “HabitTracker” as the product name, choose SwiftUI for the interface, and ensure “Use Core Data” is not selected.
  4. 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 of DayOfWeek enum cases representing on which days a user wants to perform the habit.
  • completionDates: An array of Date 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 colored Rectangle, 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 and description 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 using sort.

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.