Passing Data Up the View Hierarchy

I'm working on a calculator app but I'm having trouble displaying the results of a function. When I press a button in ButtonView, a function runs to calculate the result but that result does not display in CalculationCircleView.

Main View:

struct ScoreCalculatorView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        VStack {
            Spacer()
            HStack {
                CalculationCircleView(calculation: viewModel.grossScore, label: "Score")
                Spacer()
                CalculationCircleView(calculation: viewModel.plusMinus, label: "+/-")
            }
            .padding(.horizontal, 30)
            Spacer()
            LazyVGrid(columns: viewModel.columns, spacing: 20) {
                ForEach(0..<4, id: \.self) { index in
                    ZStack {
                        DataCellsView(labels: viewModel.scoreLabels[index])
                        TextField("\(viewModel.scoreLabels[index])", text: $viewModel.scoreData[index])
                            .font(.largeTitle)
                            .fontWeight(.light)
                            .frame(maxWidth: 80)
                            .multilineTextAlignment(.center)
                            .keyboardType(.numberPad)
                    }
                }
            }
            .frame(width: 250)
            Spacer()
            ButtonView(data: viewModel.scoreData)
                .padding(.bottom, 20)
        }
    }
}

CalculationCircleView:

struct CalculationCircleView: View {
    @StateObject private var viewModel = ViewModel()
    @State var result = ""
    let calculation: Double
    let label: String
    var body: some View {
        VStack {
            ZStack {
                Circle()
                    .stroke(lineWidth: 8)
                    .foregroundColor(.green)
                    .frame(height: 150)
                Text("\(viewModel.grossScore, specifier: "%.1f")")
                    .font(.largeTitle)
                    .fontWeight(.light)
            }
            Text(label)
        }
    }
}

ButtonView:

struct ButtonView: View {
    @StateObject private var viewModel = ViewModel()
    let data: [String]
    var body: some View {
        Button(action: {
            print("***** Score before func: \(viewModel.grossScore)")
            viewModel.calculateGrossScore(data: data)
            print("***** Score after func: \(viewModel.grossScore)")
//            viewModel.calculatePlusMinus(data: data)
//            viewModel.calculateDifferential(data: data)
        }, label: {
            ZStack {
                RoundedRectangle(cornerRadius: 4)
                    .frame(width: 200, height: 50)
                    .foregroundColor(.green)
                Text("Calculate")
                    .bold()
                    .foregroundColor(.white)
            }
        })
        .padding(.bottom, 20)
    }
}

I believe that the issue is the variable grossScore is being updated after CalculationCircleView has been called.

Is there anyway to update a view that is higher in the view hierarchy?

Note: There are some other bugs that I have not gotten to yet. Just wanted to focus on this one first.

Project Files: LINK

Answered by Vision Pro Engineer in 753230022

Hey there,

It seems like you're using @StateObject in every single view. You should use @StateObject once, here I would put it in your ContentView. This has to do with the source of truth in SwiftUI. You want there to exist only one source of truth, and then pass this object down to each of your views as an @ObservedObject. This way, the data changed is observed and passed back to the original object (the one with the @StateObject wrapper). Read more about this here: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

So, for example:

Main View

struct ScoreCalculatorView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        VStack {
            Spacer()
            HStack {
                CalculationCircleView(viewModel: viewModel, calculation: viewModel.grossScore, label: "Score")
                Spacer()
                CalculationCircleView(viewModel: viewModel, calculation: viewModel.plusMinus, label: "+/-")
            }
...

Child View

struct CalculationCircleView: View {
    @ObservedObject var viewModel: ViewModel
    @State var result = ""
    let calculation: Double
    let label: String
    var body: some View {

The changes I have made here is first of all, changing @StateObject in your child views to be @ObservedObject, and second of all, passing in the viewModel as an argument when calling your child views in your main view.

Source of truth is very important in SwiftUI, it allows us to set a variable in one view and change it in another, and have those changes be synced across views. By having just one source of truth, we are able to determine the value of a piece of data, refer to it, and change it whenever necessary.

Accepted Answer

Hey there,

It seems like you're using @StateObject in every single view. You should use @StateObject once, here I would put it in your ContentView. This has to do with the source of truth in SwiftUI. You want there to exist only one source of truth, and then pass this object down to each of your views as an @ObservedObject. This way, the data changed is observed and passed back to the original object (the one with the @StateObject wrapper). Read more about this here: https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

So, for example:

Main View

struct ScoreCalculatorView: View {
    @StateObject private var viewModel = ViewModel()
    var body: some View {
        VStack {
            Spacer()
            HStack {
                CalculationCircleView(viewModel: viewModel, calculation: viewModel.grossScore, label: "Score")
                Spacer()
                CalculationCircleView(viewModel: viewModel, calculation: viewModel.plusMinus, label: "+/-")
            }
...

Child View

struct CalculationCircleView: View {
    @ObservedObject var viewModel: ViewModel
    @State var result = ""
    let calculation: Double
    let label: String
    var body: some View {

The changes I have made here is first of all, changing @StateObject in your child views to be @ObservedObject, and second of all, passing in the viewModel as an argument when calling your child views in your main view.

Source of truth is very important in SwiftUI, it allows us to set a variable in one view and change it in another, and have those changes be synced across views. By having just one source of truth, we are able to determine the value of a piece of data, refer to it, and change it whenever necessary.

Awesome thanks! It didn't feel right to keep using @StateObject but I wasn't too sure. Thanks!

Passing Data Up the View Hierarchy
 
 
Q