Search code examples
iosswiftuiobservable

List view doesn't update


I have a code that adds work to a register client. The problem is that when I register work for the client and the sheet is being dismissed the list view doesn't update. I have to go out from the view and back in for it to display the latest registered work.

All variables I have is either @State or @ObservedObject and to my understanding the view should refresh whenever those are updated. I will provide my code for the WorkListView and the AddWorkView.

struct WorkListView: View {
    @State var client: Client
    @ObservedObject var clientData = ClientData()
    @State private var showingAddWork = false
    @State private var additionalInfo: [String] = []
    
    
    let dateFormatterStart: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMM d, HH:mm"
        return formatter
    }()
    let dateFormatterEnd: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter
    }()
    func calculateHoursAndMinutes(startDate: Date, endDate: Date) -> String {
        let interval = endDate.timeIntervalSince(startDate)
        let hours = Int(interval) / 3600
        let minutes = Int(interval) / 60 % 60
        return "\(hours) hour(s) and \(minutes) minute(s)"
    }
    var body: some View {
        
        
        List {
            Section {
                VStack(alignment: .leading) {
                    Text("Company")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    TextField("Company", text: $client.company)
                        .fontWeight(.semibold)
                        .autocorrectionDisabled(true)
                }
                
                VStack(alignment: .leading) {
                    Text("E-mail")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    TextField("E-mail", text: $client.eMail)
                        .fontWeight(.semibold)
                        .keyboardType(.emailAddress)
                }
                VStack(alignment: .leading) {
                    Text("Phone")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    TextField("Phone", text: $client.phone)
                        .fontWeight(.semibold)
                        .keyboardType(.numberPad)
                }
                VStack(alignment: .leading) {
                    Text("Price")
                        .font(.footnote)
                        .foregroundColor(.secondary)
                    TextField("Price", text: $client.price)
                }
                
            }
            
            Section(header: Text("Performed Work")) {
                ForEach(client.work.indices) { work in
                        VStack(alignment: .leading) {
                            Text("\(self.dateFormatterStart.string(from: self.client.work[work].startDate)) to \(self.dateFormatterEnd.string(from: self.client.work[work].endDate))\nDuration: \(self.calculateHoursAndMinutes(startDate: self.client.work[work].startDate, endDate: self.client.work[work].endDate))\n\(self.client.work[work].title)")
                            
                        }

                }
                
                .onDelete { indexSet in
                    guard let workIndex = indexSet.first else { return }
                    self.clientData.deleteWork(for: self.client, work: self.client.work[workIndex])
                }

            }
        }
        
        .navigationBarTitle(client.name)
        .navigationBarItems(trailing: Button(action: {
            self.showingAddWork = true
        }) {
            Image(systemName: "plus")
        })
        .sheet(isPresented: $showingAddWork) {
            AddWorkView(client: self.client, clientData: self.clientData, isPresented: $showingAddWork)
            
        }
    }
}

And here is the View for adding the work

struct AddWorkView: View {
    @State var client: Client
    @ObservedObject var clientData = ClientData()
    @Binding var isPresented: Bool
    @State private var title = ""
    @State private var startDate = Date()
    @State private var endDate = Date()
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Work title", text: $title)
                DatePicker("Start date", selection: $startDate)
                    .environment(\.locale, Locale(identifier: "sv-SE"))
                DatePicker("End date", selection: $endDate)
                    .environment(\.locale, Locale(identifier: "sv-SE"))
                
                Button(action: {
                    let work = Work(title: self.title, startDate: self.startDate, endDate: self.endDate)
                    
                    self.clientData.addWork(for: self.client, work: work)
                    self.isPresented = false
                }) {
                    Text("Add work")
                }
            }
            .navigationBarTitle("Add work")
            .navigationBarItems(trailing: Button(action: {
                self.isPresented = false
            }) {
                Text("Cancel")
            })
        }
    }
        
}

EDIT: The code I have for adding clients is similar and that code makes the Added client appear directly. Here is the code for adding clients:

struct ContentView: View {
    @ObservedObject var clientData = ClientData()
    @State private var showingAddClient = false
    @State private var showingAddWork = false
      
    var body: some View {
        NavigationView {
            List {
                ForEach(clientData.clients) { client in
                    NavigationLink(destination: WorkListView(client: client, clientData: self.clientData)) {
                        VStack(alignment: .leading) {
                            Text(client.name)
                                .font(.headline)
                            Text(client.company)
                                .foregroundColor(.secondary)
                        }
                    }
                }
                .onDelete { indices in
                    indices.forEach { self.clientData.clients.remove(at: $0) }
                }
            }
            .navigationBarTitle("Clients")
            .navigationBarItems(leading: EditButton(), trailing: Button(action: {
                self.showingAddClient.toggle()
            }) {
                Image(systemName: "plus")
            })
            .sheet(isPresented: $showingAddClient) {
                AddClientView(clientData: self.clientData, isPresented: self.$showingAddClient)
            }
        }
    }
}
struct AddClientView: View {
    @ObservedObject var clientData = ClientData()
    @Binding var isPresented: Bool
    @State private var name = ""
    @State private var company = ""
    @State private var eMail = ""
    @State private var phone = ""
    @State private var price = ""
    @State private var additionalInfo = ""
    
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Client name", text: $name)
                    .autocorrectionDisabled(true)
                TextField("Comapny", text: $company)
                    .autocorrectionDisabled(true)
                TextField("E-mail", text: $eMail)
                    .keyboardType(.emailAddress)
                TextField("Phone", text: $phone)
                    .keyboardType(.numberPad)
                TextField("Price", text: $price)
                    .keyboardType(.numberPad)
                Button(action: {
                    let client = Client(name: self.name, company: self.company, eMail: self.eMail, phone: self.phone, price: self.price)
                    
                    do {
                        try self.clientData.addClient(client)
                        self.isPresented = false
                    } catch {
                        // Handle the error
                    }
                }) {
                    Text("Add client")
                }
            }
            .navigationBarTitle("Add client")
            .navigationBarItems(trailing: Button(action: {
                self.isPresented = false
            }) {
                Text("Cancel")
            })
            
        }
    }
}

Here is the Client Class code:

class Client: ObservableObject, Codable, Hashable, Identifiable, Equatable {
    var id = UUID()
    @Published var name: String
    @Published var company: String
    @Published  var eMail: String
    @Published  var phone: String
    @Published  var price: String
    @Published  var work: [Work]
    
    init(name: String, company: String, eMail: String, phone: String, price: String) {
        self.name = name
        self.company = company
        self.eMail = eMail
        self.phone = phone
        self.price = price
        self.work = []
    }
    
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        company = try container.decode(String.self, forKey: .company)
        eMail = try container.decode(String.self, forKey: .eMail)
        phone = try container.decode(String.self, forKey: .phone)
        price = try container.decode(String.self, forKey: .price)
        work = try container.decode([Work].self, forKey: .work)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(company, forKey: .company)
        try container.encode(eMail, forKey: .eMail)
        try container.encode(phone, forKey: .phone)
        try container.encode(price, forKey: .price)
        try container.encode(work, forKey: .work)
    }
    
    enum CodingKeys: String, CodingKey {
        case name
        case company
        case eMail
        case phone
        case price
        case work
    }
    
    static func == (lhs: Client, rhs: Client) -> Bool {
        return lhs.id == rhs.id
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

What am I missing here?


Solution

  • Here is a minimal reproducible example of your problem:

    ContentView

    struct ContentView: View {
        @ObservedObject var clientData = ClientData()
        var body: some View {
            NavigationView {
                List {
                    ForEach(clientData.clients) { client in
                        NavigationLink(destination: WorkListView(client: `client, clientData: clientData)) {`
                                Text(client.name)
                            }
                        }
                    }
                }
            }
        }
    

    WorkListView

    struct WorkListView: View {
        @State var client: Client
        @ObservedObject var clientData = ClientData()
        @State private var showSheet = false
        var body: some View {
            List {
        Section {
            Text(client.name)
        }
        Section(header: Text("Performed Work")) {
            ForEach(client.work.indices) { work in
                VStack  {
                    Text(client.work[work].content)
                }
            }
        }
            }
            .onTapGesture {
                showSheet.toggle()
            }
            .sheet(isPresented: $showSheet, content: {
                AddWorkView(client: client, showSheet: $showSheet)
            })
            .padding()
        }
    }
    

    AddWorkView

    struct AddWorkView: View {
        var work = Work()
        @State var client: Client
        @ObservedObject var clientData = ClientData()
        @Binding var showSheet: Bool
        var body: some View {
            Form {
                Text(work.content)
                Button("Add") {
                    clientData.addWork(for: client, work: work)
                    showSheet = false
                }
            }
        }
    }
    

    ClientData

    class ClientData: ObservableObject {
        let clients = [Client(), Client()]
        
        func addWork(for client: Client, work: Work) {
            client.work.append(work)
        }
    }
    

    Client

    class Client: ObservableObject,
                                Identifiable {
        let name = "Foo"
        @Published var work: [Work]
        
        init() {
            self.work = []
        }
    
        func addWork(_ work: Work) {
            self.work.append(work)
        }
    }
    

    Work

    struct Work: Identifiable {
        var id = UUID()
        
        let content = "Bar"
    }
    

    To fix the problem you are having

    The problem that you've got is that when we want to share a property between multiple views, as here, we must instantiate it as a StateObject rather than just reference it's state.

    So, in WorkListView replace

    @State var client: Client
    

    with

    @StateObject var client: Client
    

    Then rather than using ForEach to iterate over the indices of the client work array as so

    ForEach(client.work.indices) { work in
        Text(client.work[work].content
    }
    

    instead bind use it to iterate over the elements of the collection

    ForEach(client.work) { work in
        Text(work.content)
    }
    

    A few extra points to note:

    1. If you are dependency injecting a reference to a class into a subview you do not need to also instantiate the same class in the subview.
    2. Please look at @EnvironmentalObject property wrapper, or delegate pattern to look at how to pass reference to objects between views.
    3. Try and work out which property wrapper gives you the behaviour that you need and only use that. There are lots of extraneous @State and duplications in your code.