Freshly inserted data through REST-request only displayed after reopening the view

I have this JSON-File coming from a MySQL-Database via PHP:

   {
      "id":1,
      "partner":{
         "id":1,
         "name":"Migros Bank",
         "image":"migrosbank"
      },
      "name":"Testkonto 1",
      "type":"bank",
      "iban":"CH12 1234 1234 1234 1234 1",
      "datapoints":[
         {
            "id":1,
            "depot_id":1,
            "date":"2021-12-28",
            "amount":5811.490234375
         },
         {
            "id":2,
            "depot_id":1,
            "date":"2021-12-29",
            "amount":7736.89013671875
         }
      ]
   }

In SwiftUI I decode it to a custom struct called Depot which consists of one instance of the custom struct Partner and an array of Instances of the custom struct Depotstand:

struct Depot: Hashable, Codable, Identifiable {
    var id: Int
    var partner: Partner? = nil
    var name: String
    var type: String
    var iban: String? = nil
    var datapoints: [Depotstand]? = nil
}

struct Partner: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var image: String

    var imageName: Image {
        Image(image)
    }
}

struct Depotstand: Hashable, Codable, Identifiable {
    var id: Int
    var depot_id: Int
    var date: Date
    var amount: Double
}

I have a custom class called DataService which handles all the REST-stuff etc. which is referenced in every file/view that uses data from the REST-interface as @StateObject as in:

@StateObject private var DATA = DataService()

The structure is like this:

  1. ContentView.swift (does not include DATA) loads Home.swift
  2. Home.swift (includes DATA) loads DATA on the NavigationView wrapper async via .task (which works great) as in:
NavigationView {
    // some stuff here
}
.task {
    do {
        try await DATA.readDepotAll()
        try await DATA.readDepotstandAll()
    } catch {
        print(error.localizedDescription)
    }
}
  1. Home.swift loads DepotList.swift which shows a list of all Depots of a certain category (a click on a list item opens the view DepotDetails.swift which shows a list of all Depotstands aka datapoints)
  2. In DepotDetails.swift is a + button in the toolbar which activates a sheet to insert a new Depotstand aka datapoint via DepotstandAdd.swift (which also works perfectly). But here is where my actual problem starts; the new datapoint can be inserted and when I print out the most current datapoint's value, the correct amount is displayed. But in the list of datapoints, the new added one is not reflected until I go one page back (DepotList.swift) which makes a refresh and shows the new total amount and then again on DepotDetails.swift which then also shows the refreshed data. I would like the data of DepotDetails.swift to be refreshed on completion of the sheet-action.

I attach the following code, DepotList.swift, DepotDetails.swift and DepotstandAdd.swift, as well as the relevant parts of the DataService which receives and posts the data over REST, since the post is already getting too long - sorry.

import SwiftUI

struct DepotList: View {
    
    @StateObject private var DATA = DataService()
    @State private var isDepotAddPresented = false
    
    var type: String
    var depots: [Depot] { DATA.depots.filter{ depot in return depot.type == type.lowercased() } }
    var date: Date { DATA.getMostCurrentDate(depots) }
    
    private func deleteData(indexSet: IndexSet) {
        let id = indexSet.map { self.depots[$0].id }
        let parameters: [String: Any] = ["id": id[0], "type": "depot"]
        //self.DATA.deleteData(parameters: parameters)
    }

    var body: some View {
        VStack {
            VStack {
                if depots.count > 0 {
                    HStack {
                        Spacer()
                        if depots.count > 0 {
                            Text("Daten per: "+DateFormat(datum: date).description)
                                .font(.system(size: 12))
                                .padding(.trailing)
                        }
                    }
                    .padding(.vertical)
                    
                    List {
                        ForEach(depots) { depot in
                            NavigationLink(destination: DepotDetails(depot_id: depot.id)) {
                                DepotListRow(depot_id: depot.id)
                                    .padding(.vertical, 10)
                            }
                            .foregroundColor(.primary)
                        }
                        .onDelete(perform: deleteData)
                    }
                    .listStyle(.plain)
                } else {
                    Spacer()
                    
                    Text("Keine Depots vorhanden")
                        .font(.headline)
                        .padding()
                    
                    NavigationLink(destination: EmptyView() /*DepotAdd(type: type)*/) {
                        HStack {
                            Image(systemName: "plus")
                            Text("Depot hinzufügen")
                        }
                    }
                    
                    Spacer()
                }
            }
            //.padding(.horizontal)
            
            if depots.count > 0 {
                VStack {
                    HStack(alignment: .firstTextBaseline) {
                        Text("Totalbetrag")
                        Spacer()
                        CHFValue(betrag: DATA.getTotal(depots))
                            //.foregroundColor(Color.accentColor)
                    }
                    .font(.headline)
                    .padding()
                    .foregroundColor(Color.primary)
                }
            }
        }
        .navigationTitle(type)
        .navigationBarTitleDisplayMode(.inline)
        .task {
            do {
                try await DATA.readDepotAll()
                try await DATA.readDepotstandAll()
            } catch {
                print(error.localizedDescription)
            }
        }
        .toolbar {
            HStack {
                if depots.count > 0 {
                    EditButton()
                    
                    Button {
                        isDepotAddPresented.toggle()
                    } label: {
                        Image(systemName: "plus")
                    }
                    /*.sheet(isPresented: $isDepotAddPresented) {
                        DepotAdd(type: type)
                    }*/
                    .fullScreenCover(isPresented: $isDepotAddPresented) {
                        NavigationView {
                            //DepotAdd(type: type)
                        }
                    }
                }
            }
        }
    }
}
import SwiftUI

struct DepotDetails: View {
    
    @StateObject private var DATA = DataService()
    
    @State var isPresentedDepotstandAdd = false
    @State var depot_id: Int
    @State var date: Date = Date()
    @State var amount: Double = 0.0
    
    /*var depot: Depot { DATA.depot }
    var sortedDatapoints: [Depotstand] { DATA.sortDatapoints(DATA.depot) }*/
    var kategorien: [Kategorie] { DATA.Kategorien }
    
    //var dates: [String] { DATA.getDepotDates(depot) }
    //var entries: [ChartDataEntry] { DATA.getDepotEntries(depot) }
    
    private func deleteData(indexSet: IndexSet) {
        let id = indexSet.map { DATA.sortedDatapoints[$0].id }
        let parameters: [String: Any] = ["id": id[0]]
        //self.DATA.deleteDepotstand(parameters: parameters)
    }

    var body: some View {
        VStack {
            VStack {
                if let datapoints = DATA.depot.datapoints {
                    if datapoints.count > 0 {
                        CHFValue(betrag: Double(datapoints[0].amount))
                            .font(.system(size: 28, weight: .semibold))
                            .foregroundColor(.accentColor)
                            .padding(.top)
                        
                        LineChart(depot_id: DATA.depot.id)
                            .frame(height: 230)
                            .padding(.horizontal)
                        
                        /*LineChart(entries: entries, dates: dates)
                            .frame(height: 200)
                            .padding(.bottom, 30)
                            .padding(.horizontal)*/
                    }
                }
                
                if DATA.sortedDatapoints.count > 0 {
                    List {
                        ForEach(DATA.sortedDatapoints) { datapoint in
                            NavigationLink(destination: EmptyView() /*DepotstandDetails(depotstand: datapoint)*/) {
                                HStack {
                                    Text(DateFormat(datum: datapoint.date, format: "dd. MMMM yyyy").description)
                                    Spacer()
                                    CHFValue(betrag: Double(datapoint.amount))
                                }
                                .padding(.vertical, 10)
                            }
                            .foregroundColor(.primary)
                        }
                        .onDelete(perform: deleteData)
                    }
                    .listStyle(.plain)
                } else {
                    Spacer()
                    
                    Text("Keine Einträge vorhanden")
                        .font(.headline)
                        .padding()
                    
                    Button {
                        isPresentedDepotstandAdd = true
                    } label: {
                        HStack {
                            Image(systemName: "plus")
                            Text("Depotstand hinzufügen")
                        }
                    }
                }
                
                Spacer()
            }
            .sheet(isPresented: $isPresentedDepotstandAdd) {
                DepotstandAdd(isPresented: $isPresentedDepotstandAdd, depot_id: $depot_id, date: $date, amount: $amount)
            }
        }
        .navigationTitle(DATA.depot.name)
        .navigationBarTitleDisplayMode(.inline)
        .task {
            await DATA.readDepot(id: depot_id)
        }
        .toolbar {
            HStack {
                if let datapoints = DATA.depot.datapoints {
                    if datapoints.count > 0 {
                        EditButton()
                        Button {
                            isPresentedDepotstandAdd = true
                        } label: {
                            Image(systemName: "plus")
                        }
                    }
                }
            }
        }
    }
}
import SwiftUI

struct DepotstandAdd: View {
    
    @StateObject private var DATA = DataService()
    
    @Binding var isPresented: Bool
    @Binding var depot_id: Int
    @Binding var date: Date
    @Binding var amount: Double
    
    var body: some View {
        VStack {
            VStack {
                Text("Neuer Depostand")
                    .font(.system(size: 20, weight: .bold))
                    .padding()
            }
            
            Form {
                Section {
                    VStack(alignment: .leading) {
                        Text("Datum")
                            .font(.callout)
                            .bold()
                            .padding(.top)
                        
                        HStack {
                            Spacer()
                            
                            DatePicker("Datum", selection: $date, in: ...Date(), displayedComponents: .date)
                                .datePickerStyle(.wheel)
                                .labelsHidden()
                            
                            Spacer()
                        }
                    }
                }
                
                Section {
                    VStack(alignment: .leading) {
                        Text("Betrag in CHF")
                            .font(.callout)
                            .bold()
                            .padding(.top)
                        
                        TextField("Bitte Betrag eingeben", value: $amount, format: .number)
                            .keyboardType(.decimalPad)
                            .textFieldStyle(.roundedBorder)
                            .padding(.bottom)
                    }
                }
            }
            
            Spacer()
            
            Button {
                Task {
                    let parameters: [String: Any] = [
                        "depot_id": depot_id,
                        "date": DateFormat(datum: date, format: "yyyy-MM-dd").description,
                        "amount": amount
                    ]
                    //print(DATA.sortedDatapoints.first!.amount)
                    await DATA.createDepotStand(parameters: parameters)
                    await DATA.readDepot(id: depot_id)
                    print(DATA.sortedDatapoints[0].amount)
                    
                    isPresented = false
                }
            } label: {
                HStack {
                    Image(systemName: "plus")
                    Text("Depotstand hinzufügen")
                        .bold()
                }
            }
            .padding(.vertical)
            
            Button {
                isPresented = false
            } label: {
                Text("Abbrechen")
            }
            .padding(.bottom)
        }
    }
}
import Foundation
import SwiftUI

@MainActor
class DataService: ObservableObject {
    
    @Published var partners: [Partner] = []
    @Published var depots: [Depot] = []
    @Published var datapoints: [Depotstand] = []
    @Published var sortedDatapoints: [Depotstand] = []
    @Published var depot: Depot = Depot(id: 0, name: "", type: "")
    
    let prefixURL: String = "https://REST-API-LINK"
    
    let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()
    
    //MARK: - Diverse Daten-Funktionen
     
    func sortDatapoints(_ depot: Depot) /*-> [Depotstand]*/ {
        var sortedDatapoints: [Depotstand] = []
        
        if let datapoints = depot.datapoints {
            sortedDatapoints = datapoints.sorted{
                $0.date.compare($1.date) == .orderedDescending
            }
            sortedDatapoints = sortedDatapoints.sorted{
                $0.id > $1.id
            }
        }
        
        self.sortedDatapoints = sortedDatapoints
        //return sortedDatapoints
    }
    
    
    //MARK: - REST-API Funktionen
    
    func createDepotStand(parameters: [String: Any]) async {
        let encoded = try! JSONSerialization.data(withJSONObject: parameters)
        var request = URLRequest(url: URL(string: prefixURL + "REST-API-LINK")!); request.httpMethod = "POST"; request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        do {
            let (_, _) = try await URLSession.shared.upload(for: request, from: encoded)
        } catch let JsonError { print("createDepotstand failed:", JsonError) }
    }
     
    func readDepotstandAll() async throws {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
        let (data, _) = try await URLSession.shared.data(from: URL(string: prefixURL + "REST-API-LINK")!)
        
        do {
            self.datapoints = try decoder.decode([Depotstand].self, from: data)
        } catch let JsonError {
            print("fetch JSON error: ", JsonError.localizedDescription)
            print("Das Problem: ", JsonError)
        }
    }
    
    func readDepot(id: Int) async {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .formatted(self.dateFormatter)        
        do {
            let (data, _) = try await URLSession.shared.data(from: URL(string: prefixURL + "REST-API-LINK")!)
            self.depot = try decoder.decode(Depot.self, from: data)
            sortDatapoints(self.depot)
        } catch { print("readDepot failed") }
    }
}

Thanks for helping me out!

P.s: To be very clear; there is no error or failure message or something like this - everything just works as expected. I just need to know, how I can get the freshly inserted data to be displayed without having to manually reentering the view to let it doing it itself.

Are you familiar with data dependencies (source of truth) in SwiftUI? Example @State, @StateObject, @ObservedObject, @EnvironmentObject etc.

If anyone of the variables are declared using one of the above, and if their @Published property changes then the views would be refreshed automatically.

IMHO it might make sense to go through https://developer.apple.com/wwdc20/10040

From DepotstandAdd.swift, it looks like you're "adding" the depotstand by telling your server database to add it. If that's correct, then it seems like you will need to also do one of the following 2 things:

  1. Have a mechanism for the server to inform you when the database changes so that you can update your Depot value (in particular, update datapoints in this case), or:

  2. Update datapoints directly in your Swift code when a depotstand is added.

#1 will result in a slight delay before the UI updates, since it involves a round-trip or two to the server to get the updated data. #2 is easier, but there is a small risk that the server update didn't happen, in which case your local data is out of sync with the server. You could do both #1 and #2, to update your local data immediately with what you expect the server to have, then fetch an update from the server and find out what it actually has stored.

Have I understood your question correctly?

Guessing:

  • I think it has something to do with DATA.depot.datapoints. Is Depot a struct or a class?

Just remember:

  • Mutating a class property is not considered as a value change.
  • Mutating a struct property is considered as a value change.

Testing

  • For testing purpose create a temp @Published var temp dataPoints, update it there and see if your view reacts.
  • If so then it is something with Depot and if it is a struct / class.

Possible Workaround

  • You can manually call objectWillChange.send() inside your DataService to indicate the object has changed

I created a TEST-API including all the app-code, so you can see everything in detail and can even post and get data from a test-server to see exactly what's going on. Hope this helps... silvioberger.ch/TEST-API.zip

Since there were no more replies for the last three days, I thought, I could post this update to everyone, instead of just in the direct replies.

I created a TEST-API including all the app-code, so you can see everything in detail and can even post and get data from a test-server to see exactly what's going on. Hope this helps... silvioberger.ch/TEST-API.zip

Any help is greatly appreciated.

Freshly inserted data through REST-request only displayed after reopening the view
 
 
Q