App state
- alertsEnabledCog
- true
- messageCountCog
- 0
State for native mobile UI
Cog keeps your app's state in one graph. Declare each fact once, then derive the rest. After a change, Cog settles the values that depend on it and notifies the views that read them.
Swift ships today. Kotlin is designed and not yet built.
In Xcode, File › Add Package Dependencies… and paste
https://github.com/skeswa/cog.gitOr add it to Package.swift
.package(
url: "https://github.com/skeswa/cog.git",
.upToNextMinor(from: "0.8.1")
)Pin to a minor version. Before 1.0 a minor release may carry listed breaking changes; a patch release never does.
There is nothing to add to a build yet
Cog for Kotlin is designed down to its turn, lifetime, and async semantics, and none of it is built. No Gradle coordinate exists yet; this panel will carry one the day it does. Until then the design is the deliverable, and it is complete enough to read and argue with.
Watch releases to hear about the first Kotlin artifact.
This eight-node graph shows how an update moves through dependent state. Change a source to see which derived values run and which views redraw.
Writing the same value does not rerun a selector. When the temperature crosses 70°, advice changes. Cog then reruns headline and redraws two views; the third view is unchanged.
A Cog declaration identifies a value; the runtime stores it. Your app, each test, and each preview can use the same declarations while keeping separate state.
adviceCog runs on its first read. It runs again only after the temperature value it read changes.
let _temperatureCog = Cog<Int>.Manual(60)
let adviceCog = Cog<String> { c in
c[_temperatureCog] > 70 ? "shorts" : "coat"
}
struct AdviceLabel: View {
@Environment(\.cogs) private var cogs
var body: some View {
let advice = cogs[adviceCog]
Text(advice)
}
}Everything above stays inside the graph. A side effect is the work that leaves it — posting a notification, writing a file, calling a service. Cog gives that work one home: a mechanism, registered once when the app assembles. Run a turn below and watch it cross the boundary.
struct NotificationsMechanism: Mechanism { let notifier: Notifier func operate(_ m: MechanismController) { m.scope(alertsEnabledCog) { s in s.watch(messageCountCog, initial: .skip) { _, count in notifier.send("Message \(count)") } } }}Cogs.assemble(mechanisms:) runs operate once, and it is the only place that can. The gate reads true, so the body below it is live and its watch is registered.
The gate is ordinary state, so the scope's lifetime is ordinary state too. When alertsEnabled settles false the scope tears down and its watch unregisters; when it settles true again the body runs from scratch. Nothing survives that cycle, which is why a mechanism never unregisters anything by hand. Read the Mechanism model →
Four versions ran the same 11-step shopping test and produced the same results. Lower is better. Handwritten caching wins two rows but makes developers manage saved results by hand; Cog records what each calculation reads and updates the right values automatically. Read the full benchmark report →
| CogThis test app has 12 single values, 5 groups of values such as one per product, and 18 values that Cog calculates. One setup process starts everything when the app opens. | Plain @ObservableUses Apple's built-in @Observable system. It saves no calculated results, so it calculates every result again when the app reads it. This is a simple baseline, not a finished app. | Cached @ObservableStarts with plain @Observable and adds seven handwritten caches. It takes 89 lines across 19 methods to decide when each saved result is out of date. | swift-state-graphUses swift-state-graph 0.28.0, another state-management library. We added code for product-specific values, loading data in the background, and deleting old data so it could run the same shopping test. | |
|---|---|---|---|---|
| time to finish one updateHow long one shopping update takes. The update changes four pieces of shopping data, then updates a chain of 23 values used to calculate prices. The number shown is the median time across repeated runs. A screen that refreshes 120 times per second has 8.3 milliseconds between frames. One millisecond (ms) equals 1,000 microseconds (µs). | 140µs | 115ms820× slower | 59µs2.4× faster | 1,569µs11× slower |
| work done by the processorHow many basic commands the processor completes. The benchmark records retired CPU instructions: the basic commands completed during the timed update. The number shown is the median count. M means million. This count does not say how long each command took. | 3.9M | 2,657M680× more | 1.2M3.1× fewer | 40M10× more |
| requests for temporary memoryHow many temporary memory blocks the update requests. This is the number of malloc calls during the timed update. Each call asks the memory allocator for a new block. These counters cover the whole process, so the test runs only after all background work has stopped. | 12 | 4,432370× more | 746.2× more | 2,602220× more |
| temporary memory requestedHow much temporary memory those requests add up to. This is the total number of bytes requested by the malloc calls, not the most memory held at one time. Each version requested and freed the same number of blocks during the timed update. B, KB, and MB mean bytes, kilobytes, and megabytes. | 536B | 378MB700,000× more | 6,035B11× more | 185KB345× more |
The handbook shows how to build an app with Cog, using code from the example apps. When you want the reasoning behind a rule, the design overview explains the tradeoffs and marks any decision that remains open.