An equivalent to computed properties using @Published in Swift Combine?
Create a new publisher subscribed to the property you want to track.
@Published var speed: Double = 88
lazy var canTimeTravel: AnyPublisher<Bool,Never> = {
$speed
.map({ $0 >= 88 })
.eraseToAnyPublisher()
}()
You will then be able to observe it much like your @Published
property.
private var subscriptions = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
sourceOfTruthObject.$canTimeTravel.sink { [weak self] (canTimeTravel) in
// Do something…
})
.store(in: &subscriptions)
}
Not directly related but useful nonetheless, you can track multiple properties that way with combineLatest
.
@Published var threshold: Int = 60
@Published var heartData = [Int]()
/** This publisher "observes" both `threshold` and `heartData`
and derives a value from them.
It should be updated whenever one of those values changes. */
lazy var status: AnyPublisher<Status,Never> = {
$threshold
.combineLatest($heartData)
.map({ threshold, heartData in
// Computing a "status" with the two values
Status.status(heartData: heartData, threshold: threshold)
})
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}()
Published computed properties in SwiftUI model objects
There are multiple issues here to address.
First, it's important to understand that SwiftUI updates the view's body when it detects a change, either in a @State
property, or from an ObservableObject
(via @ObservedObject
and @EnvironmentObject
property wrappers).
In the latter case, this is done either via a @Published
property, or manually with objectWillChange.send()
. objectWillChange
is an ObservableObjectPublisher
publisher available on any ObservableObject
.
This is a long way of saying that IF the change in a computed property is caused together with a change of any @Published
property - for example, when another element is added from somewhere:
elements.append(Talies())
then there's no need to do anything else - SwiftUI will recompute the view that observes it, and will read the new value of the computed property cumulativeCount
.
Of course, if the .count
property of one of the Tallies
objects changes, this would NOT cause a change in elements
, because Tallies
is a reference-type.
The best approach given your simplified example is actually to make it a value-type - a struct
:
struct Tallies: Identifiable {
let id = UUID()
var count = 0
}
Now, a change in any of the Tallies
objects would cause a change in elements
, which will cause the view that "observes" it to get the now-new value of the computed property. Again, no extra work needed.
If you insist, however, that Tallies
cannot be a value-type for whatever reason, then you'd need to listen to any changes in Tallies
by subscribing to their .objectWillChange
publishers:
class GroupOfTallies: Identifiable, ObservableObject {
let id = UUID()
@Published var elements: [Tallies] = [] {
didSet {
cancellables = [] // cancel the previous subscription
elements.publisher
.flatMap { $0.objectWillChange }
.sink(receiveValue: self.objectWillChange.send)
.store(in: &cancellables)
}
}
private var cancellables = Set<AnyCancellable>
var cumulativeCount: Int {
return elements.reduce(0) { $0 + $1.count } // no changes here
}
}
The above will subscribe a change in the elements
array (to account for additions and removals) by:
- converting the array into a
Sequence
publisher of each array element - then flatMap again each array element, which is a
Tallies
object, into itsobjectWillChange
publisher - then for any output, call
objectWillChange.send()
, to notify of the view that observes it of its own changes.
Computed property from @Published array of objects not updating SwiftUI view
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)
}
}
Swift binding to a computed property
Make it a @Published
property and update it when project
is changed
class MyViewModel: ObservableObject {
@Published var project: Project {
didSet {
isProjectValid = project.name != "" && project.duration > 0
}
}
@Published var isProjectValid: Bool
//...
}
SwiftUI: ObservableObject with a computed property
No need to declare your computed values as Published, as they are depending on a Published value. When the published value changed, they get recalculated.
class SelectedDate: ObservableObject {
@Published var selectedMonth: Date = Date()
var startDateOfMonth2: Date {
let components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
let startOfMonth = Calendar.current.date(from: components)!
print(startOfMonth)
return startOfMonth
}
var endDateOfMonth2: Date {
var components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
components.month = (components.month ?? 0) + 1
let endOfMonth = Calendar.current.date(from: components)!
print(endOfMonth)
return endOfMonth
}
}
When you print endDateOfMonth2
on click, you will see that it is changing.
Add @Published behaviour for computed property
Updated:
With the EnclosingSelf subscript, one can do it!
Works like a charm!
import Combine
import Foundation
class LocalSettings: ObservableObject {
static var shared = LocalSettings()
@Setting(key: "TabSelection")
var tabSelection: Int = 0
}
@propertyWrapper
struct Setting<T> {
private let key: String
private let defaultValue: T
init(wrappedValue value: T, key: String) {
self.key = key
self.defaultValue = value
}
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
public static subscript<EnclosingSelf: ObservableObject>(
_enclosingInstance object: EnclosingSelf,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<EnclosingSelf, T>,
storage storageKeyPath: ReferenceWritableKeyPath<EnclosingSelf, Setting<T>>
) -> T {
get {
return object[keyPath: storageKeyPath].wrappedValue
}
set {
(object.objectWillChange as? ObservableObjectPublisher)?.send()
UserDefaults.standard.set(newValue, forKey: object[keyPath: storageKeyPath].key)
}
}
}
@Published for a computed property (or best workaround)
In this case there's no reason to use @Published
for the isInialized
property since it's derived from two other Published properties.
var isInitialized: Bool {
return self.account.initialized && self.project.initialized;
}
Related Topics
What Is an Example of Drawing Custom Nodes with Vertices in Swift Scenekit
Resize a Sklabelnode Font Size to Fit
Swiftui Onhover Doesn't Register Mouse Leaving the Element If Mouse Moves Too Fast
How to Move a Rotated Scnnode in Scenekit
How to Nskeyedunarchiver.Unarchiveobject
Saving Array to Realm in Swift
Generate an Integer Binary Representation Using Swift
How to Make Alphabetically Section Headers in Table View with a Mutable Data Source
How to Connect Aksequencer to a Akcallbackinstrument
Avspeechutterance - Swift - Initializing with a Phrase
How to Install the Alamofire 4.0 in Xcode 8.0
How to Convert Any to Int in Swift
How to Use Swift Package Manager with an Existing MACos Project
How to View Value of Swift "Let" Constant in Xcode 6 Debugger
Scene Created in Sprite Kit Level Editor Is Not Working