https://github.com/wikipediabrown/napkin
Swift 6.2 framework for clean-architecture apps as a tree of isolated, composable units. Modeled on Uber's RIBs, rebuilt around Swift Concurrency.
https://github.com/wikipediabrown/napkin
architecture clean-architecture ios macos ribs spm structured-concurrency swift swift-6 swift-actors swift-concurrency swift-package-manager swiftui
Last synced: 28 days ago
JSON representation
Swift 6.2 framework for clean-architecture apps as a tree of isolated, composable units. Modeled on Uber's RIBs, rebuilt around Swift Concurrency.
- Host: GitHub
- URL: https://github.com/wikipediabrown/napkin
- Owner: WikipediaBrown
- License: apache-2.0
- Created: 2021-04-03T00:07:38.000Z (about 5 years ago)
- Default Branch: develop
- Last Pushed: 2026-06-03T06:00:59.000Z (29 days ago)
- Last Synced: 2026-06-03T06:24:28.585Z (29 days ago)
- Topics: architecture, clean-architecture, ios, macos, ribs, spm, structured-concurrency, swift, swift-6, swift-actors, swift-concurrency, swift-package-manager, swiftui
- Language: HTML
- Homepage: https://getnapkin.to/
- Size: 1.77 MB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 1
-
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
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Agents: AGENTS.md
Awesome Lists containing this project
README
# napkin
[](https://github.com/WikipediaBrown/napkin/actions/workflows/Tests.yml)
[](https://github.com/WikipediaBrown/napkin/actions/workflows/Release.yml)
[](https://github.com/WikipediaBrown/napkin/releases/latest)
[](https://swiftpackageindex.com/WikipediaBrown/napkin)
[](https://swiftpackageindex.com/WikipediaBrown/napkin)
[](https://github.com/WikipediaBrown/napkin/blob/main/LICENSE.md)
[](https://getnapkin.to/documentation/napkin/)
[](https://deepwiki.com/WikipediaBrown/napkin)
napkin 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.
## Table of Contents
- [Supported Platforms](#supported-platforms)
- [Installation](#installation)
- [Architecture Overview](#architecture-overview)
- [Concurrency Model](#concurrency-model)
- [Core Components](#core-components)
- [Builder](#builder)
- [Component & Dependency](#component--dependency)
- [Interactor](#interactor)
- [Interactor Lifecycle](#interactor-lifecycle)
- [Router](#router)
- [Presenter (Optional)](#presenter-optional)
- [ViewControllable](#viewcontrollable)
- [Routing & Navigation](#routing--navigation)
- [Launching the App](#launching-the-app)
- [SwiftUI Integration](#swiftui-integration)
- [Testing](#testing)
- [Tooling](#tooling)
- [Versioning](#versioning)
- [Contributing](#contributing)
- [Author](#author)
- [License](#license)
## Supported Platforms
- iOS 26.0+
- macOS 26.0+
These 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.
## Installation
Add napkin via [Swift Package Manager](https://swift.org/package-manager/):
1. In Xcode, navigate to **File** > **Add Package Dependencies...**
2. Paste the repository URL: `https://github.com/WikipediaBrown/napkin.git`
3. Click **Add Package**.
> **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).
## Architecture Overview
napkin structures your app as a tree of units called "napkins." Each napkin encapsulates a feature and consists of:
```mermaid
flowchart LR
subgraph napkin[" "]
direction LR
B([Builder]):::builder --> R([Router]):::core
B --> I([Interactor]):::core
B -.-> P([Presenter]):::optional
R --> I
R --> C([Child Routers]):::children
P -.-> V([View]):::optional
end
classDef core fill:#4a90d9,stroke:#2c5aa0,color:#fff
classDef builder fill:#50c878,stroke:#3a9a5c,color:#fff
classDef optional fill:#f5f5f5,stroke:#999,color:#666,stroke-dasharray: 5 5
classDef children fill:#ffb347,stroke:#cc8a2e,color:#fff
```
| Component | Required | Role |
|-----------|----------|------|
| **Builder** | Yes | Constructs the napkin, wires dependencies |
| **Component** | Yes | Provides dependencies to this napkin and its children |
| **Interactor** | Yes | Business logic, state management, lifecycle |
| **Router** | Yes | Manages the napkin tree (attach/detach children) |
| **Presenter** | No | Transforms business data into view-friendly formats |
| **View** | No | UIKit view controller or SwiftUI hosting controller |
Data flows down the tree. Events flow up via listener protocols.
## Concurrency Model
napkin 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.
| Layer | Isolation |
|-------|-----------|
| `Interactable` (protocol) + per-feature `final actor` | `actor` |
| `InteractorLifecycle` (helper) | `final class @unchecked Sendable` (Mutex-protected) |
| `Router` / `ViewableRouter` / `LaunchRouter` | `@MainActor` |
| `Presenter` (`@Observable`) | `@MainActor` |
| `ViewControllable` | `@MainActor` |
| `Builder` / `Component` | non-isolated, `Sendable` |
Crossings between layers are explicit `await` points:
- Interactor → Router: `await router?.routeToProfile()`
- Interactor → Presenter: `await presenter.presentUser(user)`
- View → Interactor (events): `dispatch { await listener?.didTapLogout() }`
Combine has been removed. View-state changes flow through `@Observable` properties on the Presenter; lifecycle-bound subscriptions use `Interactor.task { for await … in Observations { … } }`.
### Why protocol composition instead of class inheritance?
Swift 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`.
### Divergence from Uber RIBs-iOS
Uber'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.
Both 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/).
## Core Components
### Builder
The **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.
When 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`:
```swift
protocol HomeDependency: Dependency {
var userService: UserServiceProtocol { get }
}
protocol HomeBuildable: Buildable {
@MainActor func build(withListener listener: HomeListener) async -> HomeRouting
}
nonisolated final class HomeBuilder: Builder, HomeBuildable {
@MainActor
func build(withListener listener: HomeListener) async -> HomeRouting {
let component = HomeComponent(dependency: dependency)
let viewController = HomeViewController()
let interactor = HomeInteractor(
presenter: viewController,
userService: component.userService
)
let router = HomeRouter(interactor: interactor, viewController: viewController)
await interactor.wire(router: router, listener: listener)
return router
}
}
```
For napkins without views, `build()` does not need `@MainActor`:
```swift
protocol AnalyticsBuildable: Buildable {
func build(withListener listener: AnalyticsListener) async -> AnalyticsRouting
}
```
### Component & Dependency
A **Dependency** protocol declares what a napkin requires from its parent. A **Component** provides those dependencies and can create new ones for its children.
Use `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.
```swift
protocol HomeDependency: Dependency {
var analyticsService: AnalyticsServiceProtocol { get }
var userSession: UserSession { get }
}
nonisolated final class HomeComponent: Component {
// Passed through from parent
var analyticsService: AnalyticsServiceProtocol {
dependency.analyticsService
}
// Created once, shared within this scope
var userService: UserServiceProtocol {
shared { UserService(session: dependency.userSession) }
}
// New instance each time
var viewModel: HomeViewModel {
HomeViewModel(service: userService)
}
}
```
The root napkin uses `EmptyDependency`:
```swift
nonisolated final class AppComponent: Component, HomeDependency {
var analyticsService: AnalyticsServiceProtocol {
shared { AnalyticsService() }
}
var userSession: UserSession {
shared { UserSession() }
}
}
```
### Interactor
The **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`.
Interactors communicate:
- **Up** to parent napkins via `weak var listener` (a `Sendable` protocol the parent implements)
- **Down** to navigation via `weak var router` (an `@MainActor` routing protocol the router implements)
Listener and routing methods are `async` because they cross isolation boundaries.
```swift
protocol HomeListener: AnyObject, Sendable {
func homeDidRequestLogout() async
}
@MainActor
protocol HomeRouting: ViewableRouting {
func routeToProfile() async
}
protocol HomePresentable: Presentable, Sendable {
@MainActor var listener: HomePresentableListener? { get set }
func presentUser(_ user: User) async
}
final actor HomeInteractor: PresentableInteractable, HomePresentableListener {
nonisolated let lifecycle = InteractorLifecycle()
nonisolated let presenter: HomePresentable
weak var router: HomeRouting?
weak var listener: HomeListener?
private let userService: UserServiceProtocol
init(presenter: HomePresentable, userService: UserServiceProtocol) {
self.presenter = presenter
self.userService = userService
}
func wire(router: HomeRouting?, listener: HomeListener?) {
self.router = router
self.listener = listener
}
func didBecomeActive() async {
// Lifecycle-bound subscription: cancelled automatically on willResignActive.
task {
for await user in Observations({ userService.currentUser }) {
await presenter.presentUser(user)
}
}
}
func willResignActive() async {
// Tasks started via `task { }` are cancelled automatically here.
}
// MARK: - HomePresentableListener
func didTapProfile() async {
await router?.routeToProfile()
}
func didTapLogout() async {
await listener?.homeDidRequestLogout()
}
}
```
`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.
Use `PresentableInteractable` when the interactor communicates with a view through a presentable protocol. Use plain `Interactable` for napkins without views.
### Interactor Lifecycle
An interactor's parent router drives it between two states. You override two callbacks; the lifecycle handles the transitions, the bound tasks, and teardown.
```mermaid
stateDiagram-v2
direction LR
[*] --> Inactive
Inactive --> Active: activate() → didBecomeActive()
Active --> Inactive: deactivate() → willResignActive() → bound tasks cancelled
```
- **`activate()` / `deactivate()`** are called by the parent router on `attachChild` / `detachChild` — you never call them yourself, and both are idempotent.
- **`didBecomeActive()`** is where you start observation; **`willResignActive()`** is where you flush state or notify the listener.
- 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.
- A read-only view of the state is available through `isActive` and the `isActiveStream` `AsyncStream`.
The 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).
### Router
The **Router** is `@MainActor`, manages the napkin tree, owns the interactor, maintains a list of children, and coordinates navigation.
- `attachChild(_:)` `async` — adds a child router, activates its interactor, and loads it
- `detachChild(_:)` `async` — deactivates the child's interactor and removes it
- `didLoad()` `async open` — called once when the router is first loaded; attach permanent children here
Use `Router` for napkins without views. Use `ViewableRouter` when the napkin has a view controller.
```swift
@MainActor
protocol HomeRouting: ViewableRouting {
func routeToProfile() async
func routeBackFromProfile() async
}
@MainActor
final class HomeRouter: ViewableRouter,
HomeRouting {
private let profileBuilder: ProfileBuildable
private var profileRouter: ProfileRouting?
init(interactor: HomeInteractor,
viewController: HomeViewControllable,
profileBuilder: ProfileBuildable) {
self.profileBuilder = profileBuilder
super.init(interactor: interactor, viewController: viewController)
}
func routeToProfile() async {
guard profileRouter == nil else { return }
let router = await profileBuilder.build(withListener: interactor)
profileRouter = router
await attachChild(router)
viewController.uiviewController.present(
router.viewControllable.uiviewController,
animated: true
)
}
func routeBackFromProfile() async {
guard let router = profileRouter else { return }
profileRouter = nil
viewController.uiviewController.dismiss(animated: true)
await detachChild(router)
}
}
```
Because 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:
1. Build the child (`async @MainActor` for view-owning napkins)
2. `await attachChild(router)` — activates the interactor on its own actor, loads the router on the main actor
3. Present — manipulates the view hierarchy directly on `@MainActor`
For detaching, the order is reversed: dismiss the view, then `await detachChild(router)`.
### Presenter (Optional)
The **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`:
```swift
protocol HomePresentable: Presentable, Sendable {
func presentUser(_ user: User) async
}
@MainActor
final class HomePresenter: Presenter, HomePresentable {
var displayName: String = ""
func presentUser(_ user: User) async {
displayName = "\(user.firstName) \(user.lastName)"
}
}
```
The interactor calls `await presenter.presentUser(user)` from its actor; the await is the boundary crossing onto the main actor.
In 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`.
### ViewControllable
`ViewControllable` is the only `@MainActor`-isolated protocol in napkin. It provides access to the underlying platform view controller:
```swift
// UIKit — UIViewController subclasses conform automatically
final class HomeViewController: UIViewController, HomeViewControllable {
// uiviewController returns self via default implementation
}
// SwiftUI — use a UIHostingController
final class HomeHostingController: UIHostingController, HomeViewControllable {
init() {
super.init(rootView: HomeView())
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
```
Define a feature-specific protocol extending `ViewControllable` for methods the router or presenter needs:
```swift
@MainActor protocol HomeViewControllable: ViewControllable {
func displayUserName(_ name: String)
}
```
## Routing & Navigation
Routing 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.
**Modal presentation:**
```swift
func routeToSettings() async {
guard settingsRouter == nil else { return }
let router = await settingsBuilder.build(withListener: interactor)
settingsRouter = router
await attachChild(router)
viewController.uiviewController.present(
router.viewControllable.uiviewController,
animated: true
)
}
```
**Push onto a navigation stack:**
```swift
func routeToDetail(id: String) async {
guard detailRouter == nil else { return }
let router = await detailBuilder.build(withListener: interactor, id: id)
detailRouter = router
await attachChild(router)
let nav = viewController.uiviewController as! UINavigationController
nav.pushViewController(
router.viewControllable.uiviewController,
animated: true
)
}
```
**Embed a child view:**
```swift
func attachDashboard() async {
let router = await dashboardBuilder.build(withListener: interactor)
dashboardRouter = router
await attachChild(router)
let parent = viewController.uiviewController
let child = router.viewControllable.uiviewController
parent.addChild(child)
parent.view.addSubview(child.view)
child.didMove(toParent: parent)
}
```
**Viewless napkin (no UI):**
```swift
func attachAnalytics() async {
guard analyticsRouter == nil else { return }
let router = await analyticsBuilder.build(withListener: interactor)
analyticsRouter = router
await attachChild(router)
}
```
## Launching the App
Use `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:
```swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private var launchRouter: LaunchRouting?
func scene(_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
self.window = window
let component = AppComponent()
let builder = RootBuilder(dependency: component)
Task { @MainActor in
let router = await builder.build(withListener: AppListener())
self.launchRouter = router
await router.launch(from: window)
}
}
}
```
The root router subclasses `LaunchRouter`:
```swift
@MainActor
final class RootRouter: LaunchRouter,
RootRouting {
private let homeBuilder: HomeBuildable
init(interactor: RootInteractor,
viewController: RootViewControllable,
homeBuilder: HomeBuildable) {
self.homeBuilder = homeBuilder
super.init(interactor: interactor, viewController: viewController)
}
override func didLoad() async {
await super.didLoad()
await routeToHome()
}
func routeToHome() async {
let router = await homeBuilder.build(withListener: interactor)
await attachChild(router)
}
}
```
## SwiftUI Integration
The 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:
```swift
protocol HomePresentableListener: AnyObject, Sendable {
func didTapProfile() async
}
protocol HomePresentable: Presentable, Sendable {
@MainActor var listener: HomePresentableListener? { get set }
}
struct HomeView: View {
weak var listener: HomePresentableListener?
var body: some View {
Button("Profile") {
dispatch { [listener] in await listener?.didTapProfile() }
}
}
}
@MainActor protocol HomeViewControllable: ViewControllable {}
@MainActor
final class HomeViewController: UIHostingController, HomePresentable {
weak var listener: HomePresentableListener? {
didSet { rootView.listener = listener }
}
init() {
super.init(rootView: HomeView())
}
@MainActor required dynamic init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension HomeViewController: HomeViewControllable {}
```
The builder constructs the view controller, hands it to the interactor as the presenter, and wires the tree — no placeholder, no cycle:
```swift
@MainActor
func build(withListener listener: HomeListener) async -> HomeRouting {
let component = HomeComponent(dependency: dependency)
let viewController = HomeViewController()
let interactor = HomeInteractor(presenter: viewController,
userService: component.userService)
let router = HomeRouter(interactor: interactor, viewController: viewController)
await interactor.wire(router: router, listener: listener)
return router
}
```
(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).)
When 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.
Forward 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:
```swift
protocol HomePresentableListener: AnyObject, Sendable {
func didTapProfile() async
}
struct HomeView: View {
@Bindable var presenter: HomePresenter
weak var listener: HomePresentableListener?
var body: some View {
Button("Profile") {
dispatch { [listener] in await listener?.didTapProfile() }
}
}
}
```
## Testing
napkin 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:
```swift
import Testing
@testable import YourApp
@Suite("HomeInteractor")
struct HomeInteractorTests {
@Test func didTapLogout_notifiesListener() async {
let listener = MockHomeListener()
let presenter = await MockHomePresentable()
let interactor = HomeInteractor(presenter: presenter, userService: MockUserService())
await interactor.wire(router: nil, listener: listener)
await interactor.activate()
await interactor.didTapLogout()
#expect(await listener.logoutCalled)
}
}
final actor MockHomeListener: HomeListener {
private(set) var logoutCalled = false
func homeDidRequestLogout() async { logoutCalled = true }
}
@MainActor
final class MockHomePresentable: HomePresentable {
weak var listener: HomePresentableListener?
var lastUser: User?
func presentUser(_ user: User) async { lastUser = user }
}
```
Run tests via SwiftPM:
```bash
swift test
```
Or via Xcode (Command+U after opening `Package.swift`), or via fastlane:
```bash
bundle install
bundle exec fastlane unit_test
```
### Runnable example app
**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).
Left: LoggedOutNapkin · Right: LoggedInNapkin
Both 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:
```bash
open Examples/RibHouse/RibHouse.xcodeproj
```
Walkthrough:
## Tooling
### Xcode Templates
napkin includes Xcode templates for creating napkin components from the **File** > **New File...** menu.
#### Install
```bash
git clone https://github.com/WikipediaBrown/napkin.git
bash napkin/Tools/InstallXcodeTemplates.sh
```
#### Available Templates
| Template | Description |
|----------|-------------|
| **napkin** | Builder, Interactor, Router (+ optional ViewController) |
| **Launch napkin** | Root napkin for app launch |
| **napkin Unit Tests** | Interactor and Router test files |
| **Component Extension** | Component extension for child dependencies |
| **Service Manager** | Service manager pattern |
## Versioning
napkin 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.
Notable 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`.
## Contributing
Send a pull request or create an issue. Commits must be signed:
```bash
git config commit.gpgsign true
```
## Author
Wikipedia Brown
## License
napkin is available under the Apache 2.0 license. See the LICENSE file for more info.
Made with 🌲🌲🌲 in Cascadia