Migrating Swift Combine Publisher / Observable to Observation
Overview
Apple introduced the new Observeration framework at WWDC ‘23. This was a welcome addition because it addresses some performance issues found with the Observable framework: bottlenecks with SwiftUI view updates, the constraint of only being available to NSObject subclasses and finally being able to provide property observations _without_ special annotations.
Migration
While the migration to using the new framework was relatively straightforward. I did find myself running into a few scenarios to which Apple didn’t have a documentation for. This article won’t go into a comprehensive tutorial, it will hopefully save other developers wasted hours in compilation errors and headaches.
Usage and Migration
Simple view model that can be used to determine if input string for a given TextField is valid. To keep it simple, the length just needs to be greater than or equal to 10 characters.
class TextInputViewModel: ObservableObject {
@Published
var inputText: String = “”
@Published
private (set) var isValid: Bool = false
private var cancellables: Set<AnyCancellable> = []
init() {
$inputText
.debounce(for: 0.15, scheduler: DispatchQueue.main)
.removeDuplicates()
.map{
!$0.count >= 10
}
.assign(to: \.isValid, on: self)
.store(in: &cancellables)
}
}
With the new Observable
macro you have to call the publisher explicitedly.
@Observable
class TextInputViewModel {
var inputText: String = ""
private (set) var isValid: Bool = false
/// @ObservationIgnored won't observe changes
@ObservationIgnored
private var cancellables: Set<AnyCancellable> = []
init() {
self.inputText
.publisher
.debounce(for: 0.15, scheduler: DispatchQueue.main)
.removeDuplicates()
.collect()
.map{
!$0.count >= 10
}
.assign(to: \.isValid, on: self)
.store(in: &cancellables)
}
}
A little bit of a curve ball
Lastly, I ran into another issue with the refactor when it came to Bool
properties. Using the ObservableObject
protocol I could observe the changes directly. For example, let’s say I had a Toggle
option in my UI whose binding value came from a view model’s property: didOptIn
. Something like…
class ViewModel: ObservableObject {
@Published
var didOptIn: Bool = false
@Published
private (set) var isValid: Bool = false
@Published
var email: String = ""
init() {
$didOptIn.map {
return $0 && !email.isEmpty
}
.assign(to: \.isValid, on: self)
.store(in: &cancellables)
}
}
After the migration I got a compile error stating that didOptIn
didn’t conform to Publisher
. The fix required to have a custom PassthroughSubject
property.
@Observable
class ViewModel {
var didOptIn: Bool = false {
didSet {
self.didOptInPublisher.send(didOptIn)
}
}
private (set) var isValid: Bool = false
var email: String = ""
private var didOptInPublisher: PassthroughSubject<Bool, Never> = .init()
init() {
$didOptInPublisher.map {
return $0 && !email.isEmpty
}
.assign(to: \.isValid, on: self)
.store(in: &cancellables)
}
}
Now I’m able to observe changes for didOptIn
to perform validation checks.