Skip to content

State for native mobile UI

State that
feels
simple

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
0.8.1
Kotlin
designed
Dependencies
none

Install Cog

Swift ships today. Kotlin is designed and not yet built.

In Xcode, File › Add Package Dependencies… and paste

https://github.com/skeswa/cog.git

Or 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.

Watch state change

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.

temperature60°
zip94110
advicecoat
citySan Francisco
headlineSan Francisco: coat
AdviceLabelcoat
BannerSan Francisco: coat
CityTitleSan Francisco
waiting for a write

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.

State lives in the runtime

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.

Read the Swift design →
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)
  }
}

First-class side effects

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.

The mechanismNotificationsMechanism.swift
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.

In the graph

App state

alertsEnabledCog
true
messageCountCog
0
At the boundary

NotificationsMechanism

scope(alertsEnabled)
scope open
watch(messageCount)
registered
Outside the graph

Notifier

notifications sent
0
last
nothing yet
Scope open. The watch is registered and waiting.
Turn lognewest first
  1. AssemblyThe gate read true; the watch went live.

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 →

Minimal overhead * All four versions ran one after another in the same test session on an Apple M5 Pro with 48 GB of memory. The computer ran macOS 26.5.1, Xcode 26.6 (17F113), and Swift 6.3.3. A test tool counted every request for temporary memory. The full benchmark report explains the setup, limits, and results.

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µs115ms820× slower59µs2.4× faster1,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.9M2,657M680× more1.2M3.1× fewer40M10× 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.124,432370× more746.2× more2,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.536B378MB700,000× more6,035B11× more185KB345× more

Read the docs

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.

Released under the MIT License.