{"id":32149554,"url":"https://github.com/wikipediabrown/napkin","last_synced_at":"2026-06-04T04:00:41.320Z","repository":{"id":43746386,"uuid":"354161706","full_name":"WikipediaBrown/napkin","owner":"WikipediaBrown","description":"Swift 6.2 framework for clean-architecture apps as a tree of isolated, composable units. Modeled on Uber's RIBs, rebuilt around Swift Concurrency.","archived":false,"fork":false,"pushed_at":"2026-06-03T06:00:59.000Z","size":1856,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"develop","last_synced_at":"2026-06-03T06:24:28.585Z","etag":null,"topics":["architecture","clean-architecture","ios","macos","ribs","spm","structured-concurrency","swift","swift-6","swift-actors","swift-concurrency","swift-package-manager","swiftui"],"latest_commit_sha":null,"homepage":"https://getnapkin.to/","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WikipediaBrown.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":["WikipediaBrown"]}},"created_at":"2021-04-03T00:07:38.000Z","updated_at":"2026-06-03T05:58:29.000Z","dependencies_parsed_at":"2022-08-22T11:50:22.841Z","dependency_job_id":"493b6dee-da66-45cb-a3ca-313f71b94a75","html_url":"https://github.com/WikipediaBrown/napkin","commit_stats":{"total_commits":34,"total_committers":2,"mean_commits":17.0,"dds":0.2647058823529411,"last_synced_commit":"fe63c44192fcca1e6fff6028b5cdfa68ed7c3c33"},"previous_names":[],"tags_count":72,"template":false,"template_full_name":null,"purl":"pkg:github/WikipediaBrown/napkin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WikipediaBrown%2Fnapkin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WikipediaBrown%2Fnapkin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WikipediaBrown%2Fnapkin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WikipediaBrown%2Fnapkin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WikipediaBrown","download_url":"https://codeload.github.com/WikipediaBrown/napkin/tar.gz/refs/heads/develop","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WikipediaBrown%2Fnapkin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33888302,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-04T02:00:06.755Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","clean-architecture","ios","macos","ribs","spm","structured-concurrency","swift","swift-6","swift-actors","swift-concurrency","swift-package-manager","swiftui"],"created_at":"2025-10-21T10:00:14.013Z","updated_at":"2026-06-04T04:00:41.312Z","avatar_url":"https://github.com/WikipediaBrown.png","language":"HTML","funding_links":["https://github.com/sponsors/WikipediaBrown"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/WikipediaBrown/napkin/develop/Tools/napkin/napkin.xctemplate/TemplateIcon%402x.png\" alt=\"napkin logo\" width=\"128\" height=\"128\"\u003e\n\u003c/p\u003e\n\n# napkin\n\n[![Tests](https://github.com/WikipediaBrown/napkin/actions/workflows/Tests.yml/badge.svg)](https://github.com/WikipediaBrown/napkin/actions/workflows/Tests.yml)\n[![Release](https://github.com/WikipediaBrown/napkin/actions/workflows/Release.yml/badge.svg?branch=main)](https://github.com/WikipediaBrown/napkin/actions/workflows/Release.yml)\n[![Latest Release](https://img.shields.io/github/v/release/WikipediaBrown/napkin?label=release\u0026sort=semver\u0026color=2dbe60)](https://github.com/WikipediaBrown/napkin/releases/latest)\n[![Swift Versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FWikipediaBrown%2Fnapkin%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/WikipediaBrown/napkin)\n[![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FWikipediaBrown%2Fnapkin%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/WikipediaBrown/napkin)\n[![License: Apache 2.0](https://img.shields.io/github/license/WikipediaBrown/napkin?color=blue)](https://github.com/WikipediaBrown/napkin/blob/main/LICENSE.md)\n[![Docs](https://img.shields.io/badge/docs-getnapkin.to-2dbe60)](https://getnapkin.to/documentation/napkin/)\n[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/WikipediaBrown/napkin)\n\nnapkin is a fork of Uber's [RIBs](https://github.com/uber/ribs-ios) rebuilt on Swift 6.2 native concurrency. It structures iOS and macOS applications as a tree of modular units using the Router-Interactor-Builder pattern, with business logic running off the main actor and routing/presentation pinned to it.\n\n## Table of Contents\n\n- [Supported Platforms](#supported-platforms)\n- [Installation](#installation)\n- [Architecture Overview](#architecture-overview)\n- [Concurrency Model](#concurrency-model)\n- [Core Components](#core-components)\n  - [Builder](#builder)\n  - [Component \u0026 Dependency](#component--dependency)\n  - [Interactor](#interactor)\n  - [Interactor Lifecycle](#interactor-lifecycle)\n  - [Router](#router)\n  - [Presenter (Optional)](#presenter-optional)\n  - [ViewControllable](#viewcontrollable)\n- [Routing \u0026 Navigation](#routing--navigation)\n- [Launching the App](#launching-the-app)\n- [SwiftUI Integration](#swiftui-integration)\n- [Testing](#testing)\n- [Tooling](#tooling)\n- [Versioning](#versioning)\n- [Contributing](#contributing)\n- [Author](#author)\n- [License](#license)\n\n## Supported Platforms\n\n- iOS 26.0+\n- macOS 26.0+\n\nThese are deliberate support targets, not the hard compiler minimum. napkin's sources type-check down to iOS 18 / macOS 15 (bounded by `Mutex` from the `Synchronization` module). The project intentionally tracks only the current OS generation so the actor model and `isolated deinit`-based teardown (SE-0371, Swift 6.2) run on a single current Swift runtime instead of a back-deployment matrix.\n\n## Installation\n\nAdd napkin via [Swift Package Manager](https://swift.org/package-manager/):\n\n1. In Xcode, navigate to **File** \u003e **Add Package Dependencies...**\n2. Paste the repository URL: `https://github.com/WikipediaBrown/napkin.git`\n3. Click **Add Package**.\n\n\u003e **Xcode 26 — Default Actor Isolation.** Xcode 26's App template sets the **Default Actor Isolation** build setting to `MainActor`. napkin's `Builder` and `Component` are deliberately `nonisolated` (DI plumbing, off any actor), so in a `MainActor`-default module a `Builder`/`Component` subclass will fail to compile with *\"Main actor-isolated initializer 'init(dependency:)' has different actor isolation from nonisolated overridden declaration.\"* Mark each `Builder`/`Component` subclass `nonisolated` (the bundled Xcode templates already do — see the snippets below), or set the target's **Default Actor Isolation** to `nonisolated`. Routers and view controllers stay `@MainActor`; interactors stay `actor`s. Full explanation in [Getting Started](https://getnapkin.to/documentation/napkin/gettingstarted).\n\n## Architecture Overview\n\nnapkin structures your app as a tree of units called \"napkins.\" Each napkin encapsulates a feature and consists of:\n\n```mermaid\nflowchart LR\n    subgraph napkin[\" \"]\n        direction LR\n        B([Builder]):::builder --\u003e R([Router]):::core\n        B --\u003e I([Interactor]):::core\n        B -.-\u003e P([Presenter]):::optional\n        R --\u003e I\n        R --\u003e C([Child Routers]):::children\n        P -.-\u003e V([View]):::optional\n    end\n\n    classDef core fill:#4a90d9,stroke:#2c5aa0,color:#fff\n    classDef builder fill:#50c878,stroke:#3a9a5c,color:#fff\n    classDef optional fill:#f5f5f5,stroke:#999,color:#666,stroke-dasharray: 5 5\n    classDef children fill:#ffb347,stroke:#cc8a2e,color:#fff\n```\n\n| Component | Required | Role |\n|-----------|----------|------|\n| **Builder** | Yes | Constructs the napkin, wires dependencies |\n| **Component** | Yes | Provides dependencies to this napkin and its children |\n| **Interactor** | Yes | Business logic, state management, lifecycle |\n| **Router** | Yes | Manages the napkin tree (attach/detach children) |\n| **Presenter** | No | Transforms business data into view-friendly formats |\n| **View** | No | UIKit view controller or SwiftUI hosting controller |\n\nData flows down the tree. Events flow up via listener protocols.\n\n## Concurrency Model\n\nnapkin uses Swift 6.2 native concurrency. Business logic in the Interactor runs **off the main actor by construction**; routing and presentation run on the main actor.\n\n| Layer | Isolation |\n|-------|-----------|\n| `Interactable` (protocol) + per-feature `final actor` | `actor` |\n| `InteractorLifecycle` (helper) | `final class @unchecked Sendable` (Mutex-protected) |\n| `Router` / `ViewableRouter` / `LaunchRouter` | `@MainActor` |\n| `Presenter` (`@Observable`) | `@MainActor` |\n| `ViewControllable` | `@MainActor` |\n| `Builder` / `Component` | non-isolated, `Sendable` |\n\nCrossings between layers are explicit `await` points:\n\n- Interactor → Router: `await router?.routeToProfile()`\n- Interactor → Presenter: `await presenter.presentUser(user)`\n- View → Interactor (events): `dispatch { await listener?.didTapLogout() }`\n\nCombine has been removed. View-state changes flow through `@Observable` properties on the Presenter; lifecycle-bound subscriptions use `Interactor.task { for await … in Observations { … } }`.\n\n### Why protocol composition instead of class inheritance?\n\nSwift actors do not support inheritance (SE-0306). Rather than fall back to `@MainActor open class` (which would pin business logic to the main actor) or a custom `@globalActor` (which would serialize all interactors on one executor), napkin uses **protocol composition**: each feature's interactor is its own `final actor` conforming to `Interactable`. The `InteractorLifecycle` class — the only `@unchecked Sendable` type in the framework — owns the mutex-protected lifecycle state and its concurrency contract. Default implementations of `activate` / `deactivate` / `task(_:)` / `isActive` / `isActiveStream` come from a protocol extension that delegates to `lifecycle`.\n\n### Divergence from Uber RIBs-iOS\n\nUber's `RIBs-iOS` PR #49 unifies the framework on `@MainActor` (Interactor included). napkin deliberately keeps the Interactor off the main actor so business logic is not pinned to the main thread. The cost is `await` at every cross-layer call; the benefit is enforced clean-architecture isolation.\n\nBoth frameworks agree the *view-facing* seam belongs on `@MainActor`. napkin's base `Presentable` protocol is annotated `@MainActor`, so every feature's presentable (and any `var listener` it requires) inherits that isolation. The Swift 6 conformance error a RIB-shaped listener seam otherwise hits — *\"Main actor-isolated property 'listener' cannot be used to satisfy nonisolated protocol requirement\"* ([RIBs-iOS #43](https://github.com/uber/ribs-ios/issues/43)) — is therefore structurally impossible in napkin: the requirement and its `@MainActor` view-controller witness are always in the same isolation domain. The child-to-parent listener is a separate seam (an actor-isolated `weak var` behind a `Sendable async` protocol) and never had the problem. Full write-up: [The Swift 6 @MainActor listener-conformance error](https://getnapkin.to/blog/swift-6-mainactor-protocol-conformance/).\n\n## Core Components\n\n### Builder\n\nThe **Builder** constructs a napkin and wires its dependencies. It receives a `Dependency` from its parent and returns a `Router`. `Builder` is `Sendable` and non-isolated.\n\nWhen the napkin has a view, mark `build()` as `@MainActor async` — `@MainActor` because `UIViewController` initialization requires the main actor; `async` because wiring the actor-based interactor (e.g. setting the listener and router) requires `await`:\n\n```swift\nprotocol HomeDependency: Dependency {\n    var userService: UserServiceProtocol { get }\n}\n\nprotocol HomeBuildable: Buildable {\n    @MainActor func build(withListener listener: HomeListener) async -\u003e HomeRouting\n}\n\nnonisolated final class HomeBuilder: Builder\u003cHomeDependency\u003e, HomeBuildable {\n\n    @MainActor\n    func build(withListener listener: HomeListener) async -\u003e HomeRouting {\n        let component = HomeComponent(dependency: dependency)\n        let viewController = HomeViewController()\n        let interactor = HomeInteractor(\n            presenter: viewController,\n            userService: component.userService\n        )\n        let router = HomeRouter(interactor: interactor, viewController: viewController)\n        await interactor.wire(router: router, listener: listener)\n        return router\n    }\n}\n```\n\nFor napkins without views, `build()` does not need `@MainActor`:\n\n```swift\nprotocol AnalyticsBuildable: Buildable {\n    func build(withListener listener: AnalyticsListener) async -\u003e AnalyticsRouting\n}\n```\n\n### Component \u0026 Dependency\n\nA **Dependency** protocol declares what a napkin requires from its parent. A **Component** provides those dependencies and can create new ones for its children.\n\nUse `shared {}` to create a single instance per component scope. Without `shared`, a new instance is created on each access. The `shared()` method is thread-safe.\n\n```swift\nprotocol HomeDependency: Dependency {\n    var analyticsService: AnalyticsServiceProtocol { get }\n    var userSession: UserSession { get }\n}\n\nnonisolated final class HomeComponent: Component\u003cHomeDependency\u003e {\n\n    // Passed through from parent\n    var analyticsService: AnalyticsServiceProtocol {\n        dependency.analyticsService\n    }\n\n    // Created once, shared within this scope\n    var userService: UserServiceProtocol {\n        shared { UserService(session: dependency.userSession) }\n    }\n\n    // New instance each time\n    var viewModel: HomeViewModel {\n        HomeViewModel(service: userService)\n    }\n}\n```\n\nThe root napkin uses `EmptyDependency`:\n\n```swift\nnonisolated final class AppComponent: Component\u003cEmptyDependency\u003e, HomeDependency {\n    var analyticsService: AnalyticsServiceProtocol {\n        shared { AnalyticsService() }\n    }\n    var userSession: UserSession {\n        shared { UserSession() }\n    }\n}\n```\n\n### Interactor\n\nThe **Interactor** contains all business logic. It is a `final actor` conforming to `Interactable` (or `PresentableInteractable` when paired with a view), and holds an `InteractorLifecycle` helper. Lifecycle is driven by its parent router: `didBecomeActive()` when attached, `willResignActive()` when detached. Both are `async`.\n\nInteractors communicate:\n- **Up** to parent napkins via `weak var listener` (a `Sendable` protocol the parent implements)\n- **Down** to navigation via `weak var router` (an `@MainActor` routing protocol the router implements)\n\nListener and routing methods are `async` because they cross isolation boundaries.\n\n```swift\nprotocol HomeListener: AnyObject, Sendable {\n    func homeDidRequestLogout() async\n}\n\n@MainActor\nprotocol HomeRouting: ViewableRouting {\n    func routeToProfile() async\n}\n\nprotocol HomePresentable: Presentable, Sendable {\n    @MainActor var listener: HomePresentableListener? { get set }\n    func presentUser(_ user: User) async\n}\n\nfinal actor HomeInteractor: PresentableInteractable, HomePresentableListener {\n\n    nonisolated let lifecycle = InteractorLifecycle()\n    nonisolated let presenter: HomePresentable\n\n    weak var router: HomeRouting?\n    weak var listener: HomeListener?\n\n    private let userService: UserServiceProtocol\n\n    init(presenter: HomePresentable, userService: UserServiceProtocol) {\n        self.presenter = presenter\n        self.userService = userService\n    }\n\n    func wire(router: HomeRouting?, listener: HomeListener?) {\n        self.router = router\n        self.listener = listener\n    }\n\n    func didBecomeActive() async {\n        // Lifecycle-bound subscription: cancelled automatically on willResignActive.\n        task {\n            for await user in Observations({ userService.currentUser }) {\n                await presenter.presentUser(user)\n            }\n        }\n    }\n\n    func willResignActive() async {\n        // Tasks started via `task { }` are cancelled automatically here.\n    }\n\n    // MARK: - HomePresentableListener\n\n    func didTapProfile() async {\n        await router?.routeToProfile()\n    }\n\n    func didTapLogout() async {\n        await listener?.homeDidRequestLogout()\n    }\n}\n```\n\n`didBecomeActive` / `willResignActive` are protocol default-implementation methods, so there is no `override` and no `super` call. Subscriptions started with `task { }` on the lifecycle are cancelled automatically when the interactor deactivates.\n\nUse `PresentableInteractable` when the interactor communicates with a view through a presentable protocol. Use plain `Interactable` for napkins without views.\n\n### Interactor Lifecycle\n\nAn interactor's parent router drives it between two states. You override two callbacks; the lifecycle handles the transitions, the bound tasks, and teardown.\n\n```mermaid\nstateDiagram-v2\n    direction LR\n    [*] --\u003e Inactive\n    Inactive --\u003e Active: activate() → didBecomeActive()\n    Active --\u003e Inactive: deactivate() → willResignActive() → bound tasks cancelled\n```\n\n- **`activate()` / `deactivate()`** are called by the parent router on `attachChild` / `detachChild` — you never call them yourself, and both are idempotent.\n- **`didBecomeActive()`** is where you start observation; **`willResignActive()`** is where you flush state or notify the listener.\n- Work spawned with **`task { }`** is bound to the active scope and **cancelled automatically on deactivate** — napkin's replacement for `disposeOnDeactivate` from Uber's [RIBs](https://github.com/uber/ribs-ios). No manual teardown.\n- A read-only view of the state is available through `isActive` and the `isActiveStream` `AsyncStream\u003cBool\u003e`.\n\nThe full contract — the non-recursive `Mutex` guarding lifecycle state, the exact `deactivate()` ordering, and the `deinit` backstop that makes a runtime leak detector unnecessary — lives in the [lifecycle guide](https://getnapkin.to/documentation/napkin/lifecycle) and the [`InteractorLifecycle` reference](https://getnapkin.to/documentation/napkin/interactorlifecycle).\n\n### Router\n\nThe **Router** is `@MainActor`, manages the napkin tree, owns the interactor, maintains a list of children, and coordinates navigation.\n\n- `attachChild(_:)` `async` — adds a child router, activates its interactor, and loads it\n- `detachChild(_:)` `async` — deactivates the child's interactor and removes it\n- `didLoad()` `async open` — called once when the router is first loaded; attach permanent children here\n\nUse `Router\u003cInteractorType\u003e` for napkins without views. Use `ViewableRouter\u003cInteractorType, ViewControllerType\u003e` when the napkin has a view controller.\n\n```swift\n@MainActor\nprotocol HomeRouting: ViewableRouting {\n    func routeToProfile() async\n    func routeBackFromProfile() async\n}\n\n@MainActor\nfinal class HomeRouter: ViewableRouter\u003cHomeInteractor, HomeViewControllable\u003e,\n                        HomeRouting {\n\n    private let profileBuilder: ProfileBuildable\n    private var profileRouter: ProfileRouting?\n\n    init(interactor: HomeInteractor,\n         viewController: HomeViewControllable,\n         profileBuilder: ProfileBuildable) {\n        self.profileBuilder = profileBuilder\n        super.init(interactor: interactor, viewController: viewController)\n    }\n\n    func routeToProfile() async {\n        guard profileRouter == nil else { return }\n\n        let router = await profileBuilder.build(withListener: interactor)\n        profileRouter = router\n        await attachChild(router)\n        viewController.uiviewController.present(\n            router.viewControllable.uiviewController,\n            animated: true\n        )\n    }\n\n    func routeBackFromProfile() async {\n        guard let router = profileRouter else { return }\n        profileRouter = nil\n\n        viewController.uiviewController.dismiss(animated: true)\n        await detachChild(router)\n    }\n}\n```\n\nBecause the router is already on the main actor, there are no `Task { @MainActor in }` hops. `attachChild` / `detachChild` are `async` and serialize with the interactor's actor when activating / deactivating. The pattern is:\n\n1. Build the child (`async @MainActor` for view-owning napkins)\n2. `await attachChild(router)` — activates the interactor on its own actor, loads the router on the main actor\n3. Present — manipulates the view hierarchy directly on `@MainActor`\n\nFor detaching, the order is reversed: dismiss the view, then `await detachChild(router)`.\n\n### Presenter (Optional)\n\nThe **Presenter** transforms business data into view-friendly formats. It sits between the interactor and the view controller. `Presenter` is `@MainActor` and `@Observable`, so SwiftUI views can read its stored properties directly via `@Bindable`:\n\n```swift\nprotocol HomePresentable: Presentable, Sendable {\n    func presentUser(_ user: User) async\n}\n\n@MainActor\nfinal class HomePresenter: Presenter\u003cHomeViewControllable\u003e, HomePresentable {\n\n    var displayName: String = \"\"\n\n    func presentUser(_ user: User) async {\n        displayName = \"\\(user.firstName) \\(user.lastName)\"\n    }\n}\n```\n\nThe interactor calls `await presenter.presentUser(user)` from its actor; the await is the boundary crossing onto the main actor.\n\nIn many cases you won't need a separate `Presenter` class. The simpler pattern — used by the included templates — is to make the view controller conform to the feature-specific `Presentable` protocol directly. The interactor declares `nonisolated let presenter: HomePresentable`, calls `await presenter.presentUser(user)` to send data, and the view controller forwards user events back to the interactor via a `PresentableListener` protocol whose methods are `async`.\n\n### ViewControllable\n\n`ViewControllable` is the only `@MainActor`-isolated protocol in napkin. It provides access to the underlying platform view controller:\n\n```swift\n// UIKit — UIViewController subclasses conform automatically\nfinal class HomeViewController: UIViewController, HomeViewControllable {\n    // uiviewController returns self via default implementation\n}\n\n// SwiftUI — use a UIHostingController\nfinal class HomeHostingController: UIHostingController\u003cHomeView\u003e, HomeViewControllable {\n    init() {\n        super.init(rootView: HomeView())\n    }\n\n    required init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n```\n\nDefine a feature-specific protocol extending `ViewControllable` for methods the router or presenter needs:\n\n```swift\n@MainActor protocol HomeViewControllable: ViewControllable {\n    func displayUserName(_ name: String)\n}\n```\n\n## Routing \u0026 Navigation\n\nRouting separates the **logical tree** (attach/detach) from the **visual tree** (present/dismiss). The router is `@MainActor`, so view manipulation runs inline; `attachChild` / `detachChild` are `async` because they activate or deactivate the child interactor on its own actor.\n\n**Modal presentation:**\n\n```swift\nfunc routeToSettings() async {\n    guard settingsRouter == nil else { return }\n\n    let router = await settingsBuilder.build(withListener: interactor)\n    settingsRouter = router\n    await attachChild(router)\n    viewController.uiviewController.present(\n        router.viewControllable.uiviewController,\n        animated: true\n    )\n}\n```\n\n**Push onto a navigation stack:**\n\n```swift\nfunc routeToDetail(id: String) async {\n    guard detailRouter == nil else { return }\n\n    let router = await detailBuilder.build(withListener: interactor, id: id)\n    detailRouter = router\n    await attachChild(router)\n\n    let nav = viewController.uiviewController as! UINavigationController\n    nav.pushViewController(\n        router.viewControllable.uiviewController,\n        animated: true\n    )\n}\n```\n\n**Embed a child view:**\n\n```swift\nfunc attachDashboard() async {\n    let router = await dashboardBuilder.build(withListener: interactor)\n    dashboardRouter = router\n    await attachChild(router)\n\n    let parent = viewController.uiviewController\n    let child = router.viewControllable.uiviewController\n    parent.addChild(child)\n    parent.view.addSubview(child.view)\n    child.didMove(toParent: parent)\n}\n```\n\n**Viewless napkin (no UI):**\n\n```swift\nfunc attachAnalytics() async {\n    guard analyticsRouter == nil else { return }\n    let router = await analyticsBuilder.build(withListener: interactor)\n    analyticsRouter = router\n    await attachChild(router)\n}\n```\n\n## Launching the App\n\nUse `LaunchRouter` as the root of the napkin tree. Its `launch(from:)` method is `async`: it sets the root view controller on the window, activates the interactor, and `await`s `load()`. Hop into a `Task { @MainActor in }` from the synchronous scene callback:\n\n```swift\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n\n    var window: UIWindow?\n    private var launchRouter: LaunchRouting?\n\n    func scene(_ scene: UIScene,\n               willConnectTo session: UISceneSession,\n               options connectionOptions: UIScene.ConnectionOptions) {\n\n        guard let windowScene = scene as? UIWindowScene else { return }\n        let window = UIWindow(windowScene: windowScene)\n        self.window = window\n\n        let component = AppComponent()\n        let builder = RootBuilder(dependency: component)\n\n        Task { @MainActor in\n            let router = await builder.build(withListener: AppListener())\n            self.launchRouter = router\n            await router.launch(from: window)\n        }\n    }\n}\n```\n\nThe root router subclasses `LaunchRouter`:\n\n```swift\n@MainActor\nfinal class RootRouter: LaunchRouter\u003cRootInteractor, RootViewControllable\u003e,\n                        RootRouting {\n\n    private let homeBuilder: HomeBuildable\n\n    init(interactor: RootInteractor,\n         viewController: RootViewControllable,\n         homeBuilder: HomeBuildable) {\n        self.homeBuilder = homeBuilder\n        super.init(interactor: interactor, viewController: viewController)\n    }\n\n    override func didLoad() async {\n        await super.didLoad()\n        await routeToHome()\n    }\n\n    func routeToHome() async {\n        let router = await homeBuilder.build(withListener: interactor)\n        await attachChild(router)\n    }\n}\n```\n\n## SwiftUI Integration\n\nThe simplest viewful pattern — the one the example app and the Xcode templates use — is to make the `UIHostingController` (or `NSHostingController` on macOS) conform to the feature's `Presentable` protocol directly. No separate `Presenter` class, and no presenter⟷view-controller construction cycle. The view holds a `weak` reference to the listener and forwards user events through it:\n\n```swift\nprotocol HomePresentableListener: AnyObject, Sendable {\n    func didTapProfile() async\n}\n\nprotocol HomePresentable: Presentable, Sendable {\n    @MainActor var listener: HomePresentableListener? { get set }\n}\n\nstruct HomeView: View {\n    weak var listener: HomePresentableListener?\n\n    var body: some View {\n        Button(\"Profile\") {\n            dispatch { [listener] in await listener?.didTapProfile() }\n        }\n    }\n}\n\n@MainActor protocol HomeViewControllable: ViewControllable {}\n\n@MainActor\nfinal class HomeViewController: UIHostingController\u003cHomeView\u003e, HomePresentable {\n\n    weak var listener: HomePresentableListener? {\n        didSet { rootView.listener = listener }\n    }\n\n    init() {\n        super.init(rootView: HomeView())\n    }\n\n    @MainActor required dynamic init?(coder: NSCoder) {\n        fatalError(\"init(coder:) has not been implemented\")\n    }\n}\n\nextension HomeViewController: HomeViewControllable {}\n```\n\nThe builder constructs the view controller, hands it to the interactor as the presenter, and wires the tree — no placeholder, no cycle:\n\n```swift\n@MainActor\nfunc build(withListener listener: HomeListener) async -\u003e HomeRouting {\n    let component = HomeComponent(dependency: dependency)\n    let viewController = HomeViewController()\n    let interactor = HomeInteractor(presenter: viewController,\n                                    userService: component.userService)\n    let router = HomeRouter(interactor: interactor, viewController: viewController)\n    await interactor.wire(router: router, listener: listener)\n    return router\n}\n```\n\n(To render formatted state, give the interactor a `nonisolated let presenter` and call `await presenter.presentUser(...)` — the separate `@Observable` `Presenter` shown in [Core Components](#core-components).)\n\nWhen you *do* want a separate `@Observable` presenter holding formatted view-state, use the `Presenter` base class as shown in [Core Components](#core-components) — it's parameterized over the `ViewControllable` protocol, which is what keeps that construction acyclic.\n\nForward user actions to the interactor with `dispatch { await listener?.didTapX() }`. The `dispatch` helper is `@MainActor` and spawns an unstructured `Task` to call the actor-isolated listener — it's the bridge from a synchronous SwiftUI button handler to the async listener method:\n\n```swift\nprotocol HomePresentableListener: AnyObject, Sendable {\n    func didTapProfile() async\n}\n\nstruct HomeView: View {\n    @Bindable var presenter: HomePresenter\n    weak var listener: HomePresentableListener?\n\n    var body: some View {\n        Button(\"Profile\") {\n            dispatch { [listener] in await listener?.didTapProfile() }\n        }\n    }\n}\n```\n\n## Testing\n\nnapkin uses Swift Testing. Interactor tests are `async`; assertions about actor-isolated state require `await`. Mocks for `Sendable` listener and `@MainActor` presentable protocols can be plain `final class`es with the appropriate isolation:\n\n```swift\nimport Testing\n@testable import YourApp\n\n@Suite(\"HomeInteractor\")\nstruct HomeInteractorTests {\n\n    @Test func didTapLogout_notifiesListener() async {\n        let listener = MockHomeListener()\n        let presenter = await MockHomePresentable()\n        let interactor = HomeInteractor(presenter: presenter, userService: MockUserService())\n        await interactor.wire(router: nil, listener: listener)\n        await interactor.activate()\n\n        await interactor.didTapLogout()\n\n        #expect(await listener.logoutCalled)\n    }\n}\n\nfinal actor MockHomeListener: HomeListener {\n    private(set) var logoutCalled = false\n    func homeDidRequestLogout() async { logoutCalled = true }\n}\n\n@MainActor\nfinal class MockHomePresentable: HomePresentable {\n    weak var listener: HomePresentableListener?\n    var lastUser: User?\n    func presentUser(_ user: User) async { lastUser = user }\n}\n```\n\nRun tests via SwiftPM:\n\n```bash\nswift test\n```\n\nOr via Xcode (Command+U after opening `Package.swift`), or via fastlane:\n\n```bash\nbundle install\nbundle exec fastlane unit_test\n```\n\n### Runnable example app\n\n**Napkin's Rib House** under [`Examples/RibHouse`](Examples/RibHouse) is a runnable iOS app demonstrating the framework end-to-end: a headless `LaunchNapkin` holds an `AuthService`, swapping between a `LoggedOutNapkin` (Login button) and a `LoggedInNapkin` (user name + barbecue list).\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"Sources/napkin/napkin.docc/Resources/rib-house-logged-out.png\" alt=\"LoggedOut napkin: paper-cream background with kicker '§ 00 · WELCOME', large serif headline 'Step inside the smokehouse', a lede, and an ink LOGIN button.\" width=\"300\"\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"Sources/napkin/napkin.docc/Resources/rib-house-logged-in.png\" alt=\"LoggedIn napkin: dark green-black background, '§ ∞ · SIGNED IN' kicker, italic 'Smokey Joe' wordmark, a numbered list of barbecue foods, and an outlined LOGOUT button.\" width=\"300\"\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cem\u003eLeft:\u003c/em\u003e \u003ccode\u003eLoggedOutNapkin\u003c/code\u003e \u0026nbsp;·\u0026nbsp; \u003cem\u003eRight:\u003c/em\u003e \u003ccode\u003eLoggedInNapkin\u003c/code\u003e\u003c/p\u003e\n\nBoth screenshots are the **reference images** from the example's snapshot tests (`Examples/RibHouse/SnapshotTests/__Snapshots__/`) — any visual regression in either view flips the test red. The `.xcodeproj` is tracked, so just:\n\n```bash\nopen Examples/RibHouse/RibHouse.xcodeproj\n```\n\nWalkthrough: \u003chttps://getnapkin.to/documentation/napkin/tutorialbuildingaloginflow\u003e\n\n## Tooling\n\n### Xcode Templates\n\nnapkin includes Xcode templates for creating napkin components from the **File** \u003e **New File...** menu.\n\n#### Install\n\n```bash\ngit clone https://github.com/WikipediaBrown/napkin.git\nbash napkin/Tools/InstallXcodeTemplates.sh\n```\n\n#### Available Templates\n\n| Template | Description |\n|----------|-------------|\n| **napkin** | Builder, Interactor, Router (+ optional ViewController) |\n| **Launch napkin** | Root napkin for app launch |\n| **napkin Unit Tests** | Interactor and Router test files |\n| **Component Extension** | Component extension for child dependencies |\n| **Service Manager** | Service manager pattern |\n\n## Versioning\n\nnapkin releases [new versions on GitHub](https://github.com/WikipediaBrown/napkin/releases) automatically when a pull request is merged from `develop` to `main`. The default is a patch bump; for a minor or major release, trigger the **Release** workflow manually from the Actions tab and choose the bump type.\n\nNotable changes are documented in [CHANGELOG.md](CHANGELOG.md). The current major version is `2.x` (Swift 6.2 native concurrency); see the changelog for migration notes from `0.x` / `1.x`.\n\n## Contributing\n\nSend a pull request or create an issue. Commits must be signed:\n\n```bash\ngit config commit.gpgsign true\n```\n\n## Author\n\nWikipedia Brown\n\n## License\n\nnapkin is available under the Apache 2.0 license. See the LICENSE file for more info.\n\n\u003cp align=\"center\"\u003eMade with 🌲🌲🌲 in Cascadia\u003c/p\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwikipediabrown%2Fnapkin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwikipediabrown%2Fnapkin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwikipediabrown%2Fnapkin/lists"}