Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to observe array's single elements in SwiftUI

Tags:

swift

swiftui

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?

like image 582
Ramy Al Zuhouri Avatar asked Oct 11 '19 15:10

Ramy Al Zuhouri


Video Answer


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.

like image 151
Al___ Avatar answered Sep 28 '22 08:09

Al___