UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Day 47: Why aren't updates in DetailView shared with other views?

Forums > 100 Days of SwiftUI

I'm refactoring the habit tracking app I built on Day 47.

The app has two sections displayed in a TabView: the list of habits (ListView) and a dashboard that displays trends for all habits (ProgressView). Users can add new habits by tapping a button in ListView, which makes a form appear as a sheet. Tapping one of the rows in ListView shows a DetailView that displays information about the habit, plus a stepper for updating its progress.

So far, I've structured the data this way:

struct Habit: Identifiable {
    let id = UUID()
    var name: String
    var goal: String
    var metric: String
    var frequency: String
    var progress: Int 
}

class HabitList: ObservableObject {
    @Published var list = [Habit]()
}

I initialized HabitList as an @StateObject and passed it to MainView as an environment object, since multiple views need access to it. I then used @EnvironmentObject to refer to that instance in each of the views that need it. (This tutorial was helpful for figuring out which property wrappers to use.)

Everything is working fine, except the DetailView. Using the stepper updates the progress property within the view, but those changes aren't shared with ListView or other views.

I tried converting Habit to a class, but my code wouldn't compile.

Two questions:

  1. How should I structure data for this app? Multiple views will need access to both Habit and HabitList.
  2. Am I using the right property wrappers?

Here is my code so far.

Thanks in advance for your suggestions!

2      

I cannot see where you update your habit list values.

When you add a habit, you append the new habit to the habit list with whatever the selected value for progress is at that time.

However, when you select a new progress value, it is not saved back into the appropriate habit in the habit list. I guess that this should be a update function in the ProgressView. Unless I am missing something in your design.

2      

AddHabitView is the source of truth for each habit's properties. New habits are added via this method:

func saveAndExit() {
        habits.list.append(Habit(name: name, goal: goal, metric: metric, frequency: frequency, progress: progress))
        self.presentationMode.wrappedValue.dismiss()
    }

In DetailView, I have a stepper that should update the progress property on ListView and any other views that display information about each habit.

The problem is that it isn't.

Here is the code for DetailView. Note that ProgressBar contains a stepper with a @Binding property.

struct DetailView: View {
    @EnvironmentObject var habits: HabitList
    @State private var progress = 0
    var body: some View {
        VStack {
            Text("\(progress)% Complete")
                .font(.largeTitle)
            ProgressBar(value: $progress)
        }
        .navigationBarTitle("Habit Progress", displayMode: .inline)
    }
}

I have also tried this, but run into the same problem:

struct DetailView: View {
    @EnvironmentObject var habits: HabitList
    @State var habit: Habit
    var body: some View {
        VStack {
            Text("\(habit.progress)% Complete")
                .font(.largeTitle)
            ProgressBar(value: $habit.progress)
        }
        .navigationBarTitle("Habit Progress", displayMode: .inline)
    }
}

My guess is the problem in both cases is that I'm using an @State property wrapper, which can only update values locally, and writing Habit as a struct, which can only be read by other views, not updated.

So, to reiterate: How should I structure the data for this app?

As I mentioned, I tried writing Habit as a class, which would allow me to use @StateObject to share updates across views. However, my code wouldn't compile.

I've been stuck on this for days. It's really frustrating!

2      

You are right that @State is only local. The @Binding allows for the @State variable to be both monitored and updated.

Your habits is an @EnvironmentObject so persists across the app.

Having updated the progress status inProgressBar, the latest value is not saved back into 'habits'.

Add an onChange to the ProgressBar.

VStack {
   Text("\(progress)% Complete")
        .font(.largeTitle)
    ProgressBar(value: $progress)
    .onChange(of: progress) {
    // code to handle the update of habots
    }
}

See here for some more details. You could even link the update to an extension on Binding.

2      

Thanks for your suggestions and the link. I wrote an extension on Binding, but for some reason, it still isn't working.

ListView still shows zero progress, even when the property has been updated in DetailView.

Here's my code:

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

struct DetailView: View {
    @EnvironmentObject var habits: HabitList
    @State var habit: Habit
    var body: some View {
        VStack {
            Text("\(habit.progress)% Complete")
                .font(.largeTitle)
            ProgressBar(value: $habit.progress.onChange(updateProgress))
        }
        .navigationBarTitle("Habit Progress", displayMode: .inline)
    }

    func updateProgress(_ newValue: Int) {
        habit.progress = newValue
    }
}

What am I missing?

2      

BUILD THE ULTIMATE PORTFOLIO APP Most Swift tutorials help you solve one specific problem, but in my Ultimate Portfolio App series I show you how to get all the best practices into a single app: architecture, testing, performance, accessibility, localization, project organization, and so much more, all while building a SwiftUI app that works on iOS, macOS and watchOS.

Get it on Hacking with Swift+

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.