Migrating Swift Combine Publisher / Observable to Observation

Cory D. Wiles
2 min readDec 11, 2023

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.

--

--

Cory D. Wiles

I code stuff in Swift. I also raise children, workout, and make a perfect old fashioned.