Cookbook
Supporting iOS 15 and 16
Undra's Swift output is iOS 17 by default, because a generated store is @Observable. Lower the deployment target and the same store becomes an ObservableObject: the core, the wire and the commands do not change, only how SwiftUI is told to redraw.
Turn it on#
[ios]
deployment_target = "15.0" # 15.0 or later; below 17.0 the stores are ObservableObjectsRun undra bindgen. A new app starts there with undra init todo --ios-deployment-target 15.0. [bindings] swift_observation = "observable-object" (or --swift-observation) picks the shape on any floor; "observation" with a target below 17.0 is an error that names both settings. Below iOS 16 the wire Duration is the runtime's UndraDuration (exact nanoseconds), because Swift.Duration is iOS 16.
Your views#
| iOS 17+ (observation) | iOS 15 and 16 (observable-object) | |
|---|---|---|
| owning view | @State private var todos = try! Todos() | @StateObject private var todos = try! Todos() |
| child view | let todos: Todos | @ObservedObject var todos: Todos |
| environment | .environment(todos) | .environmentObject(todos) |
| outside SwiftUI | withObservationTracking | todos.$visible.sink { … } |
| connection state | core.connection.state | core.connectionObject with @ObservedObject |
Reading a signal is still a plain property read and a command is still a method call. The $property publishers are a gain for UIKit and Combine code on every floor.
What degrades#
- Granularity. Observation redraws a view when a property it read changes; an
ObservableObjectredraws every view that observes the store when any of its properties changes. SwiftUI coalesces the events of one drain into one update. Split a big store, or observe a smaller one. - A big list is copied on every patch.
@Publishedhas no in-place accessor, so a keyed patch to a list copies the array once, where Observation mutates it in place. Measured on an Apple M5 Pro (release build, 32-byte rows): about 5 µs for 1,000 rows, 48 µs for 10,000 and 0.5 ms for 100,000, against 0.2 µs at any size under Observation. It is linear in the list and paid once per applied patch, not per op (the mirror merges a frame's patches into one apply, so 100 ops cost about what 1 does). It is invisible for lists of thousands; for 100,000 rows that change every frame, page the list or observe a smaller store. - One name. A store member called
objectWillChangeis E0051 in this mode, becauseObservableObjectdeclares it. - SQLite change counts. The built-in
SQLiteDbAdaptercounts changes withsqlite3_changes, a Cint(the 64-bit one needs iOS 15.4): a single statement that changes more than 2,147,483,647 rows reports an undefined count. - Nothing else. The mirror, keyed patches,
no_coalesceand the error channel are the same code in both modes.
Raising the floor later#
Set deployment_target = "17.0", run undra bindgen and make the mechanical edits in the table. No core change, no wire change. The repository's examples/ios15-sample is an iOS 15.0 app over a store, a keyed list and a query; docs/IOS_15_16.md says how CI proves the floor.