Framework
napkin
A Swift 6.2 framework for building app architecture as a tree of small, isolated, composable units — napkins — modeled on Uber’s RIBs but rebuilt from the ground up around Swift Concurrency.
Overview
A napkin is one node in your application tree. Every napkin is built from a small, fixed set of rings:
| Ring | Isolation | Role |
|---|
Builder | Sendable class | Constructs the napkin: builds the component, instantiates interactor and router, wires the listener. |
Component | Sendable class | The DI container. Provides services to this napkin and conforms to its children’s Dependency protocols. |
Interactable | final actor | The unit’s brain. Holds business state, drives the lifecycle, calls the presenter and router. |
Routing | @MainActor class | Owns the subtree. Attaches and detaches child routers; in viewable napkins it also owns a view controller. |
Presentable | @MainActor class | View-state holder. Optional — view controllers can conform directly when there is no separate state to hold. |
ViewControllable | @MainActor class | The platform view. A UIHostingController wrapping a SwiftUI View, or any UIViewController / NSViewController. |
Why protocol composition over inheritance
Earlier RIBs frameworks shipped an open class PresentableInteractor<P> that subclasses extended. Swift Concurrency removes that option. Per SE-0306, actors cannot be subclassed; per SE-0316, only Apple-blessed types like MainActor may be @globalActor. A @MainActor open class base would put business logic on the main actor, violating the dependency rule of clean architecture.
napkin’s answer is protocol composition. Interactable is a protocol that refines Actor; conforming types are final actor. A protocol extension on Interactable provides default implementations of every lifecycle method, forwarding each call to InteractorLifecycle — the shared object that owns the active-state machine and its concurrency contract (see The Interactor Lifecycle). The result behaves exactly like a base class — overrides, inherited defaults, polymorphic dispatch — without the inheritance.
See Protocol Composition Over Inheritance for the full reasoning, including the primitives table and links to Apple’s evolution proposals.
The actor-isolation map
Each ring lives in a specific isolation domain, and crossings are explicit:
![The napkin actor-isolation map: Sendable Builder/Component, @MainActor Router/Presenter/ViewController, and final actor Interactor with arrows showing creates/injects/owns/await/dispatch crossings.]()
Interactor → Presenter crosses actor → @MainActor: await presenter.update(...).
Interactor → Router crosses actor → @MainActor: await router?.routeToProfile().
View → Interactor crosses @MainActor sync → actor: dispatch { await listener?.didTap() }.
Interactor → Listener stays in the listener’s actor: await listener?.fooDidFinish().
See Cross-Isolation Patterns for the full set of directional patterns and when each is appropriate.
Where to start
A runnable end-to-end reference, Napkin’s Rib House, lives at Examples/RibHouse/ in the repository. The tracked RibHouse.xcodeproj opens directly.
Topics
Getting Started
Getting Started
A 30,000ft tour of napkin: what a napkin is, the rings each one is built from, and the direction data and events flow through the tree.
Tutorial: Building a Login Flow
A walkthrough of Napkin’s Rib House (Examples/RibHouse): two child napkins, a service held by the parent, dependencies declared from the top down.
Defining a Feature
Build a single feature, file by file. We’ll define a Counter napkin that displays a tappable count and exposes a “Done” button to its parent.
Migrating From v0.x
A real, line-by-line migration of a hypothetical v0.x feature to napkin v2.0.0. We’ll convert a HomeInteractor that subclasses PresentableInteractor<HomePresentable> and uses Combine, alongside its router, presenter, and listener protocols.
Streaming State Down the Tree
How to replace Combine’s CurrentValueSubject, PassthroughSubject, and @Published with actor-owned AsyncStreams and Observations sequences, consumed through lifecycle-bound task(priority:_:) loops. The state recipe mirrors the shape of the framework’s own isActiveStream.
Tutorials
Architecture Reference
The Lifecycle
Defining a Napkin
Routing
Presenting
Dependency Injection
View Events