numi
A modern SwiftGen replacement
numi

Type-safe code from your
Apple resources.

A blazingly fast Rust CLI that turns asset catalogs, localization files, fonts, and file lists into generated accessors — with generation hooks, workspace orchestration, and incremental caching. Swift and Objective-C built in, or write your own Minijinja templates.

View on GitHub
Written in
Rust · 2024
License
MIT
Runtime
Zero
Quickstart

Three commands. Generated code you can trust.

  1. Step 01 1
    Scaffold
    $ numi init

    Drops a starter numi.toml into your project root, preconfigured for common asset layouts.

  2. Step 02 2
    Generate
    $ numi generate

    Parses your resources, renders templates, and writes output files — only when they changed.

  3. Step 03 3
    Verify
    $ numi check

    Exits non-zero if committed files are stale. Wire it into CI and never merge drift again.

Why numi

Everything you need to take generated code seriously.

01

Deterministic outputs

Byte-stable generation. Unchanged inputs never rewrite files — commit the output and trust it.

02

CI-safe with numi check

Exit 0 when fresh, exit 2 when stale. Drop it into any workflow and fail the build on drift.

03

Swift + Obj-C built-ins

Ship-ready templates for SwiftUI assets, localization, file catalogs, and fonts in both languages.

04

Minijinja templates

Bring your own templates with a stable context schema. Full access to jobs, inputs, and metadata.

05

Workspace orchestration

One manifest at the repo root, lean per-module configs that inherit shared defaults. Auto-detects the nearest ancestor workspace.

06

Cached incremental parses

Asset catalogs and strings files are parsed once, cached on disk, and skipped when untouched.

07

Generation hooks

Run formatters or scripts before and after generation. Per-job or workspace-wide, with full environment metadata.

08

Five input types

Asset catalogs, .strings, .xcstrings, file lists, and fonts. Parse once, render to any template.

09

Rich CLI tooling

Dump template context for debugging, locate and print resolved configs, and interactive terminal status.

Config

One manifest in. Type-safe code out.

Declare jobs in numi.toml. Point at inputs, pick a built-in template, and commit the generated file. That's the whole loop.

numi.toml
version = 1

[defaults]
access_level = "internal"

[jobs.assets]
output = "Generated/Assets.swift"

[[jobs.assets.inputs]]
type = "xcassets"
path = "Resources/Assets.xcassets"

[jobs.assets.template.builtin]
language = "swift"
name = "swiftui-assets"

[jobs.l10n]
output = "Generated/L10n.swift"

[[jobs.l10n.inputs]]
type = "xcstrings"
path = "Resources/Localization"

[jobs.l10n.template.builtin]
language = "swift"
name = "l10n"
// Generated by numi. DO NOT EDIT.
import SwiftUI

public enum Asset {
  public enum Image {
    public static let appIcon = ImageResource(name: "AppIcon", bundle: .module)
    public static let heroBanner = ImageResource(name: "HeroBanner", bundle: .module)
    public static let emptyState = ImageResource(name: "EmptyState", bundle: .module)
  }

  public enum Color {
    public static let brandPrimary = ColorResource(name: "BrandPrimary", bundle: .module)
    public static let surface = ColorResource(name: "Surface", bundle: .module)
    public static let onSurface = ColorResource(name: "OnSurface", bundle: .module)
  }
}
Built-in templates

Ship-ready. Or bring your own.

Every built-in is a Minijinja template backed by a stable context schema. Fork one, or write your own — the same data is available either way.

swiftui-assets Swift

Strongly-typed accessors for image and color resources, ready to drop into SwiftUI views.

Input .xcassets
l10n Swift · Obj-C

Generates localization enums with argument-preserving helpers for parameterized strings.

Input .strings · .xcstrings
files Swift · Obj-C

Emits Bundle.url helpers for arbitrary resource files — fonts, fixtures, seed data, anything.

Input file lists
assets Obj-C

Objective-C asset accessors for image and color resources with UIKit-friendly naming.

Input .xcassets
Workspace & hooks

One repo. Many modules. Shared defaults.

A root workspace manifest orchestrates member configs, shares defaults for access levels, bundle modes, template paths, and hooks. Members stay lean — they inherit what they don't override.

numi.toml (workspace root)
version = 1

[workspace]
members = ["AppUI", "Core"]

[workspace.defaults]
access_level = "internal"

[workspace.defaults.bundle]
mode = "module"

[workspace.defaults.hooks.post_generate]
command = ["swiftformat"]

[workspace.defaults.jobs.assets.template.builtin]
language = "swift"
AppUI/numi.toml (member)
version = 1

[jobs.assets]
output = "Generated/Assets.swift"

[[jobs.assets.inputs]]
type = "xcassets"
path = "Resources/Assets.xcassets"

[jobs.assets.template.builtin]
name = "swiftui-assets"

Auto-detection

Run numi generate from any member directory and it auto-prefers the nearest ancestor workspace. No flags needed.

Generation hooks

Run formatters or scripts before and after generation. Hooks receive NUMI_JOB_NAME, NUMI_OUTPUT_PATH, and NUMI_WRITE_OUTCOME via env vars.

Inherited defaults

Workspace defaults cascade into members — access levels, bundle modes, template paths, and hooks. Job-level config overrides inherited values for that phase.

CLI

Every command you need.

Generate, verify, debug, and inspect — all from the terminal. Run numi --help for the full reference.

$ numi generate

Generate outputs for one config or workspace

$ numi check

Check whether generated outputs are up to date

$ numi init

Write a starter numi.toml in the current directory

$ numi config locate

Print the resolved config path

$ numi config print

Print the resolved manifest with defaults materialized

$ numi dump-context --job <name>

Print the template context for one job

CI-safe

Gate drift at the pull request.

numi check exits 0 when committed files are up to date and 2 when they're stale. Drop it into any workflow and your CI will fail the build before reviewers do.

  • Deterministic output — no timestamps, no ordering noise.
  • Workspace mode checks every manifest in one pass.
  • No-op writes: unchanged files never get rewritten.
.github/workflows/verify.yml
name: verify

on: [push, pull_request]

jobs:
  numi-check:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - run: cargo install numi
      - name: Verify generated files are up to date
        run: numi check --workspace

Generate once. Trust forever.

numi is MIT licensed, on Homebrew and crates.io. One command to install, one to try.