Contents

rozd/icon-kit

**A Swift library and CLI for working with Apple `.icon` bundles and Android adaptive icons.**

✨ Features

  • πŸ”£ SF Symbol Icons β€” generate .icon bundles from any SF Symbol with configurable background, foreground color, size, and offset. Perfect for prototyping and internal tools.
  • πŸŽ€ Ribbon Overlays β€” stamp UAT / QA / Staging labels onto .icon bundles or Android adaptive icons in one command. Configurable placement, colors, font, and size.
  • πŸ€– Android Adaptive Icons β€” read and write Android adaptive icon XML format with PNG and WebP asset support. Ribbon overlays are composited onto foreground layers at each density.
  • πŸ“¦ Round-Trip Safe β€” read an .icon bundle, inspect or modify it, write it back out without data loss.
  • 🧩 Full Document Model β€” typed Swift structs for every part of the .icon format: groups, layers, fills, shadows, blend modes, specializations, and platform targeting.
  • 🎨 Appearance & Idiom Variants β€” first-class support for light/dark/tinted appearances and per-platform (iOS, macOS, watchOS, visionOS) specializations.
  • πŸ–₯️ CLI + Library β€” use the iconkit command-line tool directly, or embed the IconKit library in your own Swift code.

πŸš€ CLI Usage

Install

Homebrew
brew tap rozd/tap
brew install iconkit
Build from source
swift build -c release
# Binary is at .build/release/iconkit

Add a ribbon

Stamp an environment label onto an existing .icon bundle:

iconkit ribbon top \
  --text "UAT" \
  --input AppIcon.icon \
  --output AppIcon.uat.icon

Customize the appearance:

iconkit ribbon topLeft \
  --text "DEV" \
  --input AppIcon.icon \
  --output AppIcon.dev.icon \
  --background "#4A90D9" \
  --foreground "#FFFFFF" \
  --size 0.3 \
  --font-scale 0.5

<details> <summary>Ribbon options</summary>

| Option | Default | Description | |--------|---------|-------------| | <placement> | β€” | top, bottom, topLeft, or topRight | | --text | β€” | Text to render on the ribbon | | --size | 0.24 | Ribbon height as a factor of icon height (0.0–1.0) | | --offset | 0.0 | Offset from edge as a factor of icon height | | --background | #B92636 | Ribbon background color (hex) | | --foreground | #FEFAFA | Text color (hex) | | --font | System | Font family name | | --font-scale | 0.6 | Text size as a factor of ribbon height |

</details>

Add a ribbon to an Android adaptive icon

The same ribbon command works with Android adaptive icons. Pass a res/ directory or an adaptive icon XML file:

# From a res/ directory (auto-discovers XML in mipmap-anydpi-v26/)
iconkit ribbon bottom \
  --text "DEV" \
  --input app/src/main/res \
  --output app/src/debug/res

# From an XML file directly
iconkit ribbon topLeft \
  --text "QA" \
  --input res/mipmap-anydpi-v26/ic_launcher.xml \
  --output res-qa

The ribbon is composited onto every density variant of the foreground layer (mdpi through xxxhdpi). Input format is auto-detected. WebP foreground images are supported (read as WebP, written back as PNG after compositing).

Generate an icon from an SF Symbol

Create a new .icon bundle from any SF Symbol:

iconkit generate sf \
  --symbol "shippingbox.fill" \
  --background "#4A90D9" \
  --foreground "#FFFFFF" \
  --size 0.8 \
  --output AppIcon.icon

<details> <summary>Generate options</summary>

| Option | Default | Description | |--------|---------|-------------| | --symbol | β€” | SF Symbol name (e.g. shippingbox.fill) | | --output | β€” | Path to output .icon bundle | | --background | #007AFF | Icon background color (hex) | | --foreground | #FFFFFF | Symbol color (hex) | | --size | 0.6 | Symbol size as a fraction of icon space (0.0–1.0) | | --offset-x | 0.0 | Horizontal offset as a fraction of icon width | | --offset-y | 0.0 | Vertical offset as a fraction of icon height |

</details>

Inspect a bundle

Examine the structure of an .icon bundle:

iconkit inspect AppIcon.icon
AppIcon.icon
  Fill: automatic-gradient srgb:0.69804,0.65098,0.60392,1.00000
  Platforms: circles [watchOS], squares shared
  Group 1
    Lighting: individual
    Shadow: none (opacity: 0.5)
    Layer "Fitness Art"
      Image: Fitness Art.png
      Glass: true
      Position: scale 2.0, translate (0.0, 0.0)
      Fill specializations:
        [default] solid display-p3:0.05882,0.08235,0.09804,1.00000
        [dark] solid display-p3:0.94902,0.93725,0.87843,1.00000
  Assets: 1 present, 0 missing

Use --json for machine-readable output (raw icon.json, pretty-printed):

iconkit inspect --json AppIcon.icon

Validate round-trip fidelity

Read a bundle and write it back to verify nothing is lost:

iconkit test --input AppIcon.icon --output AppIcon.copy.icon

πŸ€– GitHub Action

Stamp environment ribbons onto your app icons in CI/CD before building:

- uses: rozd/icon-kit@v1
  with:
    text: UAT
    input: App/Assets.xcassets/AppIcon.icon

The action downloads a pre-built iconkit binary from the matching GitHub Release and runs the ribbon command. Currently requires a macOS runner (Linux support for Android-only projects is planned).

Inputs

| Input | Required | Default | Description | |-------|----------|---------|-------------| | text | βœ… | β€” | Text to render on the ribbon | | input | βœ… | β€” | Path to .icon bundle, adaptive icon XML, or Android res/ directory | | output | | same as input | Output path β€” omit for in-place modification | | placement | | bottom | top, bottom, topLeft, or topRight | | size | | 0.24 | Ribbon height as a factor of icon height | | offset | | 0.0 | Offset from edge as a factor of icon height | | background | | #B92636 | Ribbon background color (hex) | | foreground | | #FEFAFA | Text color (hex) | | font | | System | Font family name | | font-scale | | 0.6 | Text size as a factor of ribbon height | | version | | action ref | Pin to a specific IconKit release (e.g. v1.2.3) |

Example: Stamp a UAT build

jobs:
  build:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - uses: rozd/icon-kit@v1
        with:
          placement: topLeft
          text: UAT
          input: App/Assets.xcassets/AppIcon.icon
          background: '#4A90D9'

      - name: Build app
        run: xcodebuild -project App.xcodeproj -scheme App archive

Example: Android adaptive icon

- uses: rozd/icon-kit@v1
  with:
    placement: bottom
    text: DEV
    input: app/src/main/res

The ribbon is composited onto every density variant of the foreground layer.

πŸ“¦ Integration

Swift Package Manager

Add IconKit as a dependency in your Package.swift:

dependencies: [
    .package(url: "https://github.com/rozd/icon-kit", from: "0.1.0")
]

Then add the product to your target:

.target(
    name: "YourTarget",
    dependencies: [
        .product(name: "IconKit", package: "icon-kit")
    ]
)

πŸ› οΈ Library Usage

Read and inspect a bundle

import IconKit

let icon = try IconComposerDescriptorFile(contentsOf: bundleURL)

// Human-readable summary
print(icon.inspectSummary(bundleName: "AppIcon.icon"))

// Check for missing referenced assets
let warnings = icon.validateAssets()

Add a ribbon overlay

var icon = try IconComposerDescriptorFile(contentsOf: bundleURL)

let style = RibbonStyle(
    text: "UAT",
    size: 0.24,
    offset: 0.0,
    background: try parseHexColor("#B92636"),
    foreground: try parseHexColor("#FEFAFA"),
    fontScale: 0.6
)

try icon.applyRibbon(placement: .top, style: style)
try icon.write(to: outputURL)

Add a ribbon to an Android adaptive icon

var icon = try AdaptiveIconFile(contentsOf: resDirURL)

let style = RibbonStyle(
    text: "DEV",
    background: try parseHexColor("#4A90D9"),
    foreground: try parseHexColor("#FFFFFF")
)

try icon.applyRibbon(placement: .bottom, style: style)
try icon.write(to: outputResDirURL)

Generate an icon from an SF Symbol

let style = SFSymbolStyle(
    symbolName: "shippingbox.fill",
    foreground: try parseHexColor("#FFFFFF"),
    size: 0.8
)

let background = try parseHexIconColor("#4A90D9")
let icon = try IconComposerDescriptorFile.sfSymbol(
    style: style,
    background: background
)
try icon.write(to: outputURL)

Work with layers and specializations

// Access layers
for group in icon.document.groups {
    for layer in group.layers {
        print(layer.name ?? "unnamed", layer.imageName ?? "no image")
    }
}

// Resolve a specialization for dark mode on iOS
let fill = resolveSpecialization(
    base: layer.fill,
    specializations: layer.fillSpecializations ?? [],
    appearance: .dark,
    idiom: .iOS
)

βš™οΈ How It Works

An .icon bundle is a directory containing:

AppIcon.icon/
β”œβ”€β”€ icon.json          # Document descriptor (groups, layers, fills, effects)
└── Assets/
    β”œβ”€β”€ Background.svg
    β”œβ”€β”€ Foreground.png
    └── ...

IconKit models the full icon.json structure as typed Swift structs β€” IconDocument, IconGroup, IconLayer, and supporting types like IconFill, IconShadow, IconBlendMode, and Specialization<T>. Every field round-trips cleanly through Codable.

The ribbon feature works by generating a transparent PNG overlay and inserting it as the front-most layer (group index 0), with liquid glass automatically disabled to ensure opaque, true colors.

Android Adaptive Icons

An Android adaptive icon is an XML descriptor referencing foreground and background layers:

res/
β”œβ”€β”€ mipmap-anydpi-v26/
β”‚   └── ic_launcher.xml    # <adaptive-icon> descriptor
β”œβ”€β”€ mipmap-hdpi/
β”‚   β”œβ”€β”€ ic_launcher_foreground.png   # (or .webp)
β”‚   └── ic_launcher_background.png
β”œβ”€β”€ mipmap-xxhdpi/
β”‚   └── ...
└── ...

Since Android adaptive icons only support foreground + background layers (no arbitrary layer stacking), ribbons are composited directly onto the foreground PNG at each density. Both PNG and WebP inputs are supported.

Package Metadata

Repository: rozd/icon-kit

Default branch: main

README: README.md