How to observe array's single elements in SwiftUI

Viewed 1438

Let's say that I have a class Employee whose picture property can be observed.

internal class Employee: CustomDebugStringConvertible, Identifiable, ObservableObject {
    internal let name: String
    internal let role: Role
    @Published internal var picture: UIImage?
}

With a class that stores an array of employees. This array might be mutated later so it's a @Published property:

internal class EmployeeStorage: ObservableObject {
    @Published internal private(set) var employees: [Employee]
}

Now I have a list of views that uses the employee storage:

struct EmployeeList: View {
    @ObservedObject var employeeStorage = EmployeeStorage.sharedInstance

    var body: some View {
        // Uses employeeStorage.employee property to draw itself
    }
}

And the list is used in a content view:

struct ContentView: View {
    @ObservedObject var employeeStorage = EmployeeStorage.sharedInstance

    var body: some View {
        // Uses an EmployeeList value
    }
}

Let's say that now the EmployeeStorage object changes mutates employee array: this effectively updates the UI and I can see that the list is updated with the new collection of employees. Problem: what if I want to achieve the same effect when an employee's picture property changes? I thought that this was enough, but in my case (I have a more complex example), the UI is not updated in case that an employee's image changes. How to do it?

1 Answers

There are a couple of problems in your code.

  • Employee Model

    • Your model should be a struct.
    • It does not need to conform to ObservableObject.
    • Why you're using UIImage!? Simply use Image and possibly a URL if you're fetching data from a web Api asynchronously.
    • something like:

    // Employee Model
    struct Employee: Codable, Identifiable {
        let name: String
        let role: Role
        let imageUrl: String
        let picture: Image
    }
    
  • EmployeeStorage

    • I suspect that the way you're using EmployeeStorage as a singleton is problematic. Apple suggests that you better use an @EnvironmentObject property to pass data shared between views and their subviews. It's super simple, yet robust!

That being said, you can modify your code as follows:

// Employees Store
final class EmployeeStorage: ObservableObject {
    @Published var employees: [Employee]
}

// Content View
struct ContentView: View {
    var employeeStorage = EmployeeStorage()

    var body: some View {
        ...
        EmployeeList()
        .environmentObject(employeeStorage) // here you pass the storage to the list
    }
}

// Employee List
struct EmployeeList: View {
    @EnvironmentObject var employeeStorage: EmployeeStorage

    var body: some View {
        NavigationView {
            List {
                ForEach(employeeStorage.employees) { employee in
                        NavigationLink(
                            destination: EmployeeDetail()
                                .environmentObject(employeeStorage) 
                        ) {
                            EmployeeRow(employee: employee)
                        }
                }.navigationBarTitle(Text("Employees"))
            }
        }
    }
}

For more information, you can check this out. It's been done pretty much what you're going to achieve.

Related