On iOS 26 an app icon is no longer a flattened PNG: it is a small bundle of layers the system lights, tints and renders as Liquid Glass. Icon Composer draws them by hand; this skill generates the same document from the brand artwork, so the icon is rebuilt when the brand changes rather than redrawn from memory.
# apple-app-icon — the icon as a build artefact, not a drawing
On iOS 26 an app icon is no longer a flattened PNG: it is a small bundle of
**layers** the system lights, tints and renders as Liquid Glass. Icon Composer
draws them by hand; this skill generates the same document from the brand
artwork, so the icon is *rebuilt* when the brand changes rather than redrawn
from memory.
Reach for it when the icon and the app disagree — different mark, different
gradient, or nine PNGs whose source nobody can find.
## Workflow
### 1. Find the real artwork before drawing anything
Two attempts went the wrong way on one project before the file turned up:
rebuilding the mark from the app's own button geometry (beautiful, and a
different mark), then tracing it off a 1024 PNG by measuring pixels (the right
mark, at second-hand accuracy).
```bash
find ~ -maxdepth 7 \( -iname "*logo*" -o -iname "*icon*" \) \
\( -iname "*.svg" -o -iname "*.ai" -o -iname "*.sketch" \) 2>/dev/null
mdfind -name "logo" # useless if Spotlight is off — check `mdutil -s /`
```
Look in abandoned web projects and old zips; a Figma export usually survives
somewhere even when the designer's archive link is dead. **What you want is one
artboard, already composed**: the mark on its tile, with the background
gradient in a `<linearGradient>`. That file is the source of truth — copy it
into the repo (`Resources/logo-source.svg`) so the icon has a provenance.
### 2. Generate
```bash
python3 "${SKILL_DIR}/scripts/make_icon.py" \
--source Resources/logo-source.svg \
--icon Resources/App.icon --render
```
It reads the stops, the viewBox and the paths out of the artwork, splits the
paths into layers, writes `icon.json`, and exports all six appearances through
`ictool`. Nothing is retyped — a hex copied into a script is a second source of
truth that drifts on the next export.
### 3. Look at all six, not at one
```
Default · Dark · TintedLight · TintedDark · ClearLight · ClearDark
```
The dark and tinted appearances are where a mark falls apart: the system
re-colours the layers, and thin white lines with dark gaps — the shape of most
marks — are exactly what a translucent treatment fills in. Rendering is three
seconds; guessing costs a release.
### 4. Then see it among other icons
```bash
xcrun simctl install <UDID> path/to/App.app
xcrun simctl io <UDID> screenshot home.png # after `axe button home`
```
A tile that looks confident at 512 points can be mush at 60 next to Photos and
Safari. That is the only test that matters.
## Wiring it into the project
```yaml
# project.yml (XcodeGen)
sources:
- path: Resources/App.icon
settings:
base:
ASSETCATALOG_COMPILER_APPICON_NAME: App # the .icon's name, no extension
```
Keep the old `AppIcon.appiconset` beside it while the deployment target is
below 26 — older systems fall back to it.
## Gotchas
Each of these cost a render, and none is documented — the full set, with the
measurements, is in `references/icon-json.md`.
- **Icon Composer fills contours and ignores strokes.** `<circle fill="none"
stroke="…">` arrives as a filled disc: the first build of one icon came out
as a white blob. Hand it outlines — a ring is an annulus with `evenodd`, an
arc is a band with rounded ends.
- **Groups are drawn front to back.** The background written first covers
everything; it goes **last**.
- **`fill` belongs to the layer.** On a group it is accepted and silently
ignored — `ictool` returns 0 either way.
- **`fill-specializations` does nothing from JSON** (Icon Composer 1.6). On a
group it parses and changes nothing; on a layer the document stops loading.
Per-appearance colours are a thing the GUI writes — if dark or monochrome
must differ, open the `.icon` and set them there, then stop regenerating it.
- **A colour is `display-p3:r,g,b,a`, all four components.** Three fail with
"Expected four comma separated color components"; a bare hex with "missing
':' delimiter", which names the delimiter and not the prefix.
- **`linear-gradient` takes exactly two colours.** A three-stop brand gradient
has to be drawn as a background *layer* — at the cost that the system can no
longer dim it for dark. Two colours it can dim; three it cannot touch. Pick
which matters more and say so in a comment.
- **The artboard is already composed.** Scaling it to "72% of the canvas, so it
breathes" shrinks the mark twice — the designer's margin plus yours. Scale
1:1; use ~0.9 only if watchOS is claimed, because a circular mask eats the
corners of a square's margin.
- **Do not claim platforms you do not ship.** `supported-platforms` with
`circles: [watchOS]` puts a round preview in front of you that no user will
ever see, and tempts you into re-composing the icon for it.
## Don't
- **Don't redraw the mark from the app's own code.** The button inside the app
and the icon on the home screen are two different objects; making the icon
"match the code" produces a mark nobody recognises.
- **Don't trace a PNG when a vector exists.** Measuring pixels gets the radii
right and the end caps wrong.
- **Don't ship without looking at Dark.** It is the appearance most likely to
be ugly and the one nobody checks.
- **Don't leave the probes in the repo.** Scripts written to interrogate the
format belong in the commit message or in `references/`, not in `scripts/`.