I have a StateController class:
import Foundation
import Combine
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
@Published var subjects: [Subject]
private let storageController = StorageController()
init() {
self.subjects = storageController.fetchData()
}
// MARK: - Computed properties
// Array with all tasks from subjects, computed property
var allTasks: [Task] {
var all: [Task] = []
for subject in subjects {
all += subject.tasks
}
print("Computed property updated!")
return all
}
var numberofCompletedTasks: Int {
return subjects.map({$0.tasks.map({$0.isCompleted == true})}).count
}
var numberOfHighPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 1})}).count
}
var numberOfMediumPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 2})}).count
}
var numberOfLowPriorityTasks: Int {
return subjects.map({$0.tasks.map({$0.priority == 3})}).count
}
}
And a SwiftUI view:
import SwiftUI
struct SmartList: View {
// MARK: - Properties
let title: String
@EnvironmentObject private var stateController: StateController
// MARK: - View body
var body: some View {
List(stateController.allTasks, id: \.taskID) { task in
TaskView(task: task)
.environmentObject(self.stateController)
}.listStyle(InsetGroupedListStyle())
.navigationTitle(LocalizedStringKey(title))
}
}
When I update "Task" objects inside "subjects" @Published array, for example checking them as complete, SwiftUI should automatically update the view because computed properties are derived from @Published property of an ObservableObject (declared as @EnvironmentObject inside view) but it doesn't work.
How can I bind my SwiftUI view to computed properties derived from a @Published property??
Sadly, SwiftUI automatically updating views that are displaying computed properties when an @EnvironmentObject
/@ObservedObject
changes only works in very limited circumstances. Namely, the @Published
property itself cannot be a reference type, it needs to be a value type (or if it's a reference type, the whole reference needs to be replaced, simply updating a property of said reference type won't trigger an objectWillChange
emission and hence a View
reload).
Because of this, you cannot rely on computed properties with SwiftUI. Instead, you need to make all properties that your view needs stored properties and mark them as @Published
too. Then you need to set up a subscription on the @Published
property, whose value you need for your computed properties and hence update the value of your stored properties each time the value they depend on changes.
class StateController: ObservableObject {
// Array of subjects loaded in init() with StorageController
@Published var subjects: [Subject]
@Published var allTasks: [Task] = []
private let storageController = StorageController()
private var subscriptions = Set<AnyCancellable>()
init() {
self.subjects = storageController.fetchData()
// Closure for calculating the value of `allTasks`
let calculateAllTasks: (([Subject]) -> [Task]) = { subjects in subjects.flatMap { $0.tasks } }
// Subscribe to `$subjects`, so that each time it is updated, pass the new value to `calculateAllTasks` and then assign its output to `allTasks`
self.$subjects.map(calculateAllTasks).assign(to: \.allTasks, on: self).store(in: &subscriptions)
}
}