§ 00·The framework
Apps as a tree of small, isolated, composable units.
napkin is a Swift 6.2 framework for clean-architecture iOS & macOS apps, modeled on Uber's RIBs and rebuilt around Swift Concurrency. Business logic lives in final actor interactors. Routing and presentation are @MainActor. Crossings are explicit.
1final actor HomeInteractor: PresentableInteractable {
2 nonisolated let lifecycle = InteractorLifecycle()
3 nonisolated let presenter: HomePresentable
4
5 init(presenter: HomePresentable) {
6 self.presenter = presenter
7 }
8
9 func didBecomeActive() async {
10 task {
11 for await user in users.stream() {
12 await presenter.update(user)
13 }
14 }
15 }
16}
Specifications
Four moves that distinguish napkin from every framework that came before it — including its parent.
-
Clean isolation, by the compiler
Business logic lives in
final actorinteractors — off the main thread, by construction. Routing and presentation are@MainActor. Swift enforces the dependency rule, not convention. -
No Combine. No RxSwift.
@Observablefor state.AsyncStreamfor events. Structured concurrency for everything that used to need a subscription bag. Combine is removed; Rx never arrives. -
iOS 26 · macOS 26 · Swift 6.2
Built on
isolated deinit,MutexfromSynchronization,@Observable, andObservations { }. Concrete bets on the modern stack — no polyfills, no compromises. -
Modeled on Uber's RIBs
Builder, Component, Interactor, Router, Presenter. The familiar architecture you can sketch on a napkin — rebuilt around
actorand friends, with composition where inheritance once lived.
The isolation map
Three regions. Each ring lives in a specific isolation domain. Every crossing is explicit and named.
constructs the napkin
DI container
owns the subtree
hosts the SwiftUI view
@Observable state
business logic, off main
await router?.routeTo(…)await presenter.update(…)dispatch { await listener?.didTap(…) }
Data flows down the tree. Events flow up through listener protocols. Every cross-region call is an await — an architectural seam, made legible.
§ 04·See it in motion
A real app, the napkin way.
Napkin’s Rib House is a runnable iOS app: a headless LaunchNapkin holds an AuthService, then swaps a LoggedOutNapkin for a LoggedInNapkin on tap. Snapshots and code march in lockstep below.
-
01·LoggedOutNapkin/LoggedOutNapkinView.swift
struct LoggedOutNapkinView: View { weak var listener: PresentableListener? var body: some View { Button("Login") { dispatch { await listener?.didTapLogin() } } } } -
02·LoggedOutNapkin/LoggedOutNapkinInteractor.swift
final actor LoggedOutNapkinInteractor: PresentableListener { weak var listener: LoggedOutNapkinListener? // View talks down to me; I talk up to my parent. func didTapLogin() async { await listener?.loggedOutDidTapLogin() } } -
03·LaunchNapkin/LaunchNapkinInteractor.swift
final actor LaunchNapkinInteractor: LoggedOutNapkinListener { nonisolated let authService: AuthService weak var router: LaunchNapkinRouting? func loggedOutDidTapLogin() async { let user = try await authService.login() await router?.attachLoggedIn(user: user) } } -
04·LoggedInNapkin/LoggedInNapkinView.swift
struct LoggedInNapkinView: View { let user: User var body: some View { VStack { Text(user.name).italic() ForEach(user.barbecueFoods, id: \.self) { food in Text(food) } } } }
§ ∞·Get started
Sketch your next app on a napkin.
Pull the package, read the docs, run the example. Three commands, one tree, one isolation rule the compiler is willing to enforce.
// Package.swift
.package(url: "https://github.com/WikipediaBrown/napkin.git", from: "2.1.2")