← Skills

/apple-app-icon

Build an iOS 26+ layered app icon (.icon / Icon Composer) from a designer's SVG, generated by script rather than drawn by hand, and check every appearance without launching the app. Use when the user says or when an app still ships a folder of flattened PNGs

install: npx skills add fortunto2/solo-factory/apple-app-icon
phase: utility

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/`.
Sources