§ 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.7
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, and @Observable — with Observations { } as the documented streaming recipe. 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 root LaunchNapkin holds an AuthService, then swaps a LoggedOutNapkin for a LoggedInNapkin when login succeeds — routing follows the auth stream, not the tap. Snapshots and condensed code below.

  1. 01·LoggedOutNapkin/LoggedOutNapkinView.swift

    struct LoggedOutNapkinView: View {
        weak var listener: LoggedOutNapkinPresentableListener?
    
        var body: some View {
            Button("Login") {
                dispatch { await listener?.didTapLogin() }
            }
        }
    }
  2. 02·LoggedOutNapkin/LoggedOutNapkinInteractor.swift

    final actor LoggedOutNapkinInteractor: LoggedOutNapkinPresentableListener {
        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 didBecomeActive() async {
            task { // routing follows auth state, not taps
                for await user in await authService.userStream() {
                    if let user { await router?.attachLoggedIn(user: user) }
                    else { await router?.attachLoggedOut() }
                }
            }
        }
    
        func loggedOutDidTapLogin() async {
            _ = try? await authService.login() // no routing — the stream reacts
        }
    }
  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.7")