WWDC24 SALE: Save 50% on all my Swift books and bundles! >>

SOLVED: Day 47: How do I save the timesCompleted property? Quantify (Habit Tracker)

Forums > 100 Days of SwiftUI

Hello,

I'm working on the Habit Tracker app. How do you save the timesCompleted property? In the ContentView, it just shows as 0 and sure enough, when I enter the ActivityView, timesCompleted is back to 0. I tried many times but nothing is working. Maybe I haven't realised something about SwiftUI yet. Help would be appreciated!

ContentView:

import SwiftUI

struct ContentView: View {
    var activities = Activities()

    var body: some View {
        NavigationStack {
            List {
                ForEach(activities.activities, id: \.id) { activity in
                    NavigationLink {
                        ActivityView(title: activity.title, description: activity.description, timesCompleted: activity.timesCompleted)
                    } label: {
                        VStack(alignment: .leading) {
                            Text(activity.title)
                                .font(.headline.bold())
                            Text("Times completed: \(activity.timesCompleted)")
                                .font(.caption.bold())
                                .foregroundStyle(.secondary)
                        }
                    }
                }
                .listRowBackground(Color.lightBackground)
            }
            .navigationTitle("Quantify")
            .toolbar {
                NavigationLink {
                        AddView(activities: activities)
                } label: {
                    Image(systemName: "plus")
                        .foregroundStyle(.white)
                }
            }
            .scrollContentBackground(.hidden)
            .background(.darkBackground)
        }
        .preferredColorScheme(.dark)
    }
}

ActivityView:

import SwiftUI

struct ActivityView: View {
    var title: String
    var description: String
    @State var timesCompleted: Int

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    Text(description)
                        .padding()

                    Text("Times completed: \(timesCompleted)")
                        .font(.title.bold())

                    Button("Activity completed!") {
                        timesCompleted += 1
                    }
                    .padding()
                    .background(.lightBackground)
                    .foregroundStyle(.white)
                    .clipShape(RoundedRectangle(cornerRadius: 60))
                    .font(.title.bold())

                }
            }
            .navigationTitle(title)
            .frame(maxWidth: .infinity)
            .background(.darkBackground)
        }
    }
}

AddView:

import SwiftUI

struct AddView: View {
    @State private var title = "Untitled activity"
    @State private var description = ""

    @Environment(\.dismiss) var dismiss

    var activities: Activities

    var body: some View {
        NavigationStack {
            Form {
                TextField("Enter description", text: $description)
                    .listRowBackground(Color.lightBackground)
            }
            .navigationBarBackButtonHidden()
            .scrollContentBackground(.hidden)
            .background(.darkBackground)
            .navigationTitle($title)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("Save") {
                        let activity = Activity(title: title, description: description)
                        activities.activities.append(activity)
                        dismiss()
                    }
                }

                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
    }
}

Activity struct:

import Foundation

struct Activity: Identifiable, Codable {
    var title: String
    var description: String
    var id = UUID()
    var timesCompleted = 0
}

Activities class:

import Foundation

@Observable
class Activities {
    var activities = [Activity]() {
        didSet {
            if let encoded = try? JSONEncoder().encode(activities) {
                UserDefaults.standard.set(encoded, forKey: "Activities")
            }
        }
    }

    init() {
        if let savedActivities = UserDefaults.standard.data(forKey: "Activities") {
            if let decodedActivities = try? JSONDecoder().decode([Activity].self, from: savedActivities) {
                activities = decodedActivities
                return
            }
        }

        activities = []
    }
}

Colour-Theme:

import SwiftUI

extension ShapeStyle where Self == Color {
    static var darkBackground: Color {
        Color(red: 0.2, green: 0.4, blue: 0.2)
    }

    static var lightBackground: Color {
        Color(red: 0.3, green: 0.5, blue: 0.3)
    }
}

   

It's a bit tricky with structs. As you udpate var activities = [Activity]() in your observable object. But for that object array is the same, as you didn't add or remove anyting from the array. You will need to get the reference to that array, find the activity you're updating and replace it with a new struct, which has incremented count. After that, as you have replaced that activity, it sees that array has been modified and triggers update.

// Make it to conform to Equatable protocol
struct Activity: Identifiable, Codable, Equatable {
    var title: String
    var description: String
    var id = UUID()
    var timesCompleted = 0
}

Change the below struct as

struct ActivityView: View {
    // Pass the reference to the data
    // Definitely there other ways to do that via Environment e.g.
    var activities: Activities
    // Pass the activity that you have clicked
    var selectedActivity: Activity

    var body: some View {
        NavigationStack {
            ScrollView {
                VStack {
                    Text(selectedActivity.description)
                        .padding()

                    Text("Times completed: \(selectedActivity.timesCompleted)")
                        .font(.title.bold())

                    Button("Activity Completed", action: {
                        // You copy passed activity to a new struct
                        var newActivity = selectedActivity
                        // Adjust count as necessary
                        newActivity.timesCompleted += 1

                        // Find that activity in your list of activities and replace it with new activity
                        // And now observable see that there is a change in the array of var activities = [Activity]()
                        // and triggers the update
                        if let index = activities.activities.firstIndex(of: selectedActivity) {
                            activities.activities[index] = newActivity
                        }
                    })
                    .padding()
                    .background(.lightBackground)
                    .foregroundStyle(.white)
                    .clipShape(RoundedRectangle(cornerRadius: 60))
                    .font(.title.bold())

                }
            }
            .navigationTitle(selectedActivity.title)
            .frame(maxWidth: .infinity)
            .background(.darkBackground)
        }
    }
}

in your List in ContentView udpate init

ActivityView(activities: activities, selectedActivity: activity)

   

Save 50% in my WWDC sale.

SAVE 50% To celebrate WWDC24, all our books and bundles are half price, so you can take your Swift knowledge further without spending big! Get the Swift Power Pack to build your iOS career faster, get the Swift Platform Pack to builds apps for macOS, watchOS, and beyond, or get the Swift Plus Pack to learn advanced design patterns, testing skills, and more.

Save 50% on all our books and bundles!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.