An equivalent to computed properties using @Published in Swift Combine?
Solution 1:
You don't need to do anything for computed properties that are based on @Published
properties. You can just use it like this:
class UserManager: ObservableObject {
@Published
var currentUser: User?
var userIsLoggedIn: Bool {
currentUser != nil
}
}
What happens in the @Published
property wrapper of currentUser
is that it will call objectWillChange.send()
of the ObservedObject
on changes. SwiftUI views don't care about which properties of @ObservedObject
s have changed, it will just recalculate the view and redraw if necessary.
Working example:
class UserManager: ObservableObject {
@Published
var currentUser: String?
var userIsLoggedIn: Bool {
currentUser != nil
}
func logOut() {
currentUser = nil
}
func logIn() {
currentUser = "Demo"
}
}
And a SwiftUI demo view:
struct ContentView: View {
@ObservedObject
var userManager = UserManager()
var body: some View {
VStack( spacing: 50) {
if userManager.userIsLoggedIn {
Text( "Logged in")
Button(action: userManager.logOut) {
Text("Log out")
}
} else {
Text( "Logged out")
Button(action: userManager.logIn) {
Text("Log in")
}
}
}
}
}
Solution 2:
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()
}()
Solution 3:
You could declare a PassthroughSubject in your ObservableObject:
class ReactiveUserManager1: ObservableObject {
//The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
var objectWillChange = PassthroughSubject<Void,Never>()
[...]
}
And in the didSet (willSet could be better) of your @Published var you will use a method called send()
class ReactiveUserManager1: ObservableObject {
//The PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.
var objectWillChange = PassthroughSubject<Void,Never>()
@Published private(set) var currentUser: User? {
willSet {
userIsLoggedIn = currentUser != nil
objectWillChange.send()
}
[...]
}
You can check it in the WWDC Data Flow Talk
Solution 4:
How about using downstream?
lazy var userIsLoggedInPublisher: AnyPublisher = $currentUser
.map{$0 != nil}
.eraseToAnyPublisher()
In this way, the subscription will get element from upstream, then you can use sink
or assign
to do the didSet
idea.