Swift 6.2 introduces a new Observations type to stream state changes from an Observable type. Let’s see an example of why that can be useful.
What’s My Problem?
I like to keep my app’s scene state in an Observable
model. That state might include various scene-specific settings along with the navigation state of the window (presented sheets, navigation path, etc.). That gives me a single object to save/restore to maintain state across app launches.
Let’s see an example with a search query and navigation path:
@Observable final class SceneModel: Codable {
var query: String = ""
var path: [Item.ID] = []
The type is Codable
so I can persist it as JSON data:
nonisolated private enum CodingKeys: String, CodingKey {
case query
case path
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
query = try container.decode(String.self, forKey: .query)
path = try container.decode([Item.ID].self, forKey: .path)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(query, forKey: .query)
try container.encode(path, forKey: .path)
}
}
I’ve added a property to compute the JSON data and to restore the model from JSON data:
var jsonData: Data? {
get {
try JSONEncoder().encode(self)
}
set {
guard let data = newValue,
let model = try? JSONDecoder()
.decode(SceneModel.self, from: data) else {
return
}
self.query = model.query
self.path = model.path
}
}
Since @SceneStorage
cannot persist arbitrary data I have it save and restore the encoded scene model JSON data in the root view of my scene.
struct RootView: View {
@State private var sceneModel = SceneModel()
@SceneStorage("SceneModel") private var sceneData: Data?
var body: some View {
NavigationSplitView {
...
}
.task {
// Save and restore state
}
}
}
The task runs when the view appears so I can use that to restore my scene model state from any JSON data the scene storage provides:
.task {
// Restore scene state
if let sceneData {
sceneModel.jsonData = sceneData
}
}
Saving the scene state is tricky. Before iOS 26, the best I could do was check the scene phase and save the state when moving to the background:
@Environment(\.scenePhase) private var phase
var body: some View {
NavigationSplitView {
...
}
.task { ... }
.onChange(of: phase) { _, newValue in
// Save scene state
if newValue == .background {
sceneData = sceneModel.jsonData
}
}
}
}
That’s not ideal. SwiftUI makes no promise that my scene will always transition to a background state before termination. For example, on macOS a scene only seems to enter a background state when you hide the window. There’s a risk with this approach that I lose changes to my scene state.
Using ObservableObject
I think I first saw this approach to saving and restoring navigation state in the Apple WWDC22 session video The SwiftUI cookbook for navigation. At that time, SwiftUI used ObservableObject
and @Published
properties.
That meant you could rely on Combine to publish changes to the observable object as an async sequence:
var objectWillChangeSequence:
AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>> {
objectWillChange
.buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest)
.values
}
You can then save the state any time the model changes:
.task {
// Restore scene state
if let sceneData {
sceneModel.jsonData = sceneData
}
// Save scene state
for await _ in sceneModel.objectWillChangeSequence {
sceneData = sceneModel.jsonData
}
}
I’ve been waiting since iOS 17 to be able to do something similar with Observable
types.
What Changed in iOS 26
Starting in iOS 26 and Swift 6.2 you can stream state changes to an Observable type as an AsyncSequence. That’s exactly what I need to save my scene state any time it changes.
The Observations
type takes a closure where you compute the value you want to observe changes for:
// iOS 26 required
let values = Observations {
sceneModel.jsonData
}
In my case, I can use the computed JSON data to observe changes to any of my scene models properties.
The updates are transactional so multiple synchronous changes arrive as a single updated value. From the WWDC session video:
The tracking for an update begins when any of the observable properties in the closure have their willSet called. And it ends at the next await where the code suspends
That allows me to rewrite my task to both restore and save the scene state without needing ro rely on the scene phase:
.task {
// Restore scene state
if let sceneData {
sceneModel.jsonData = sceneData
}
// Save scene state
let values = Observations {
sceneModel.jsonData
}
for await value in values {
sceneData = value
}
}
Each time my scene model changes I get a new value from the for-await async sequence that I persist using the @SceneStorage
.
Note:
- The async sequence starts with the initial value of the scene model. That’s not a problem in my case but it’s worth being aware of.