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?
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: