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

Latest
2.1.2
Targets
iOS 26 · macOS 26
Swift
6.2 strict
License
Apache‑2.0
HomeInteractor.swift Swift 6.2
 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}
§ 01

Specifications

Four moves that distinguish napkin from every framework that came before it — including its parent.

  1. Clean isolation, by the compiler

    Business logic lives in final actor interactors — off the main thread, by construction. Routing and presentation are @MainActor. Swift enforces the dependency rule, not convention.

  2. No Combine. No RxSwift.

    @Observable for state. AsyncStream for events. Structured concurrency for everything that used to need a subscription bag. Combine is removed; Rx never arrives.

  3. iOS 26 · macOS 26 · Swift 6.2

    Built on isolated deinit, Mutex from Synchronization, @Observable, and Observations { }. Concrete bets on the modern stack — no polyfills, no compromises.

  4. Modeled on Uber's RIBs

    Builder, Component, Interactor, Router, Presenter. The familiar architecture you can sketch on a napkin — rebuilt around actor and friends, with composition where inheritance once lived.

§ 02

The isolation map

Three regions. Each ring lives in a specific isolation domain. Every crossing is explicit and named.

Sendable
Builder
constructs the napkin
Component
DI container
@MainActor
Router
owns the subtree
ViewController
hosts the SwiftUI view
Presenter
@Observable state
final actor
Interactor
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.

  1. 01·LoggedOutNapkin/LoggedOutNapkinView.swift

    struct LoggedOutNapkinView: View {
        weak var listener: PresentableListener?
    
        var body: some View {
            Button("Login") {
                dispatch { await listener?.didTapLogin() }
            }
        }
    }
  2. 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()
        }
    }
  3. 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)
        }
    }
  4. 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")