A Swift library and CLI for working with Apple .icon bundles and Android adaptive icons.
IconKit reads and writes the .icon bundle format introduced with Icon Composer β Apple's structured icon format containing multiple image layers (front, middle, back) at various sizes, enabling dynamic rendering effects like parallax and lighting. It also supports Android adaptive icons (XML format with PNG/WebP assets). Use it to generate icons from SF Symbols, inspect bundle structure, add environment ribbons to existing icons, manipulate layers programmatically, or validate round-trip fidelity.
- π£ 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.
brew tap rozd/tap
brew install iconkitswift build -c release
# Binary is at .build/release/iconkitStamp 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.5Ribbon options
| 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 |
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).
Create a new .icon bundle from any SF Symbol:
iconkit generate sf \
--symbol "shippingbox.fill" \
--background "#4A90D9" \
--foreground "#FFFFFF" \
--size 0.8 \
--output AppIcon.iconGenerate options
| 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 |
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 missing
Use --json for machine-readable output (raw icon.json, pretty-printed):
iconkit inspect --json AppIcon.iconRead a bundle and write it back to verify nothing is lost:
iconkit test --input AppIcon.icon --output AppIcon.copy.iconStamp 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).
| 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) |
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- 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.
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")
]
)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()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)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)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)// 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
)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.
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.