rozd/icon-kit
**A Swift library and CLI for working with Apple `.icon` bundles and Android adaptive icons.**
β¨ Features
- π£ SF Symbol Icons β generate
.iconbundles 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
.iconbundles 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
.iconbundle, inspect or modify it, write it back out without data loss. - π§© Full Document Model β typed Swift structs for every part of the
.iconformat: 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
iconkitcommand-line tool directly, or embed theIconKitlibrary in your own Swift code.
π CLI Usage
Install
Homebrew
brew tap rozd/tap
brew install iconkitBuild from source
swift build -c release
# Binary is at .build/release/iconkitAdd a ribbon
Stamp an environment label onto an existing .icon bundle:
iconkit ribbon top \
--text "UAT" \
--input AppIcon.icon \
--output AppIcon.uat.iconCustomize 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-qaThe 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.iconAppIcon.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 missingUse --json for machine-readable output (raw icon.json, pretty-printed):
iconkit inspect --json AppIcon.iconValidate 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.iconThe 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 archiveExample: Android adaptive icon
- uses: rozd/icon-kit@v1
with:
placement: bottom
text: DEV
input: app/src/main/resThe 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