Skip to content

feat(native): Swift Icon draws shared icon names as SF Symbols - #8302

Merged
cixzhang merged 3 commits into
mainfrom
native/swift-icon
Oct 25, 2026
Merged

cixzhang merged 3 commits into
mainfrom
native/swift-icon

Conversation

@cixzhang

@cixzhang cixzhang commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

User need

An iOS screen built with Astryx needs icons: in list rows, in place menus, in settings rows, beside a status. Today there is no Swift Icon, so an app either draws its own Image(systemName:) with guessed sizes and colors, or has no way to use Astryx's shared icon names at all. The person using the app gets icons that don't match the theme's colors, don't grow with their text size, and may be read aloud by VoiceOver when they are only decoration.

Problem and solution fit

  • Icon view. Draws a shared icon name as the platform's own SF Symbol, in a square box at core's icon size (12, 16, 20, 24 pt) grown with Dynamic Type, in core's icon color token. Decorative unless it has a label, as core's Icon is.
  • Typed icon slot in the contract generator. Core's icon prop is IconName | ComponentType<SVGProps>; the generator gains an icon slot kind (AST-082 FR4, FR17), so IconProps.icon is a generated IconName instead of being dropped as web-only. IconName is string-backed with every built-in name as a static member (.search, .close), so a theme-registered "acme:rocket" is valid too. The same slot kind is what Button's and Badge's icon props need next.

Current specification

  • spec:AST-082 FR4 (typed slots), FR7 (base props), FR17 (shared icon names, platform symbols).
  • spec:AST-093 FR1–FR5.
  • Swift library records: component API (A1–A9), styles, theme environment, slots (S1–S5), accessibility (X1, X4).
  • New: component:swift/Icon at packages/native/apple/Sources/Astryx/Icon/Icon.spec.md (draft, platform_of: component:Icon).

Public delta and caller burden

import Astryx

Icon(.search)                                        // md, surrounding foreground, decorative
Icon(.success, color: .success, label: "Completed")  // named for VoiceOver
Icon(.chevronRight, color: .secondary, size: .sm)
Icon(IconProps(icon: "acme:rocket"))                 // a name the theme maps
  • New: Icon (Astryx), and generated IconProps, IconColor, IconSize, IconName (AstryxNative, re-exported by Astryx).
  • Theme(icons:) already maps a name to an SF Symbol; it now also registers a <library>:<name> name.
  • An unmapped name stops a development build and draws nothing in release.
  • No change to any existing view. An SVG component has no Swift form (the web keeps it).

Design and alternatives

  • Platform control: SwiftUI Image(systemName:) (AST-093 FR2), no custom drawing.
  • The glyph fits the box (resizable().scaledToFit()), so a size means the same box as on the web.
  • Owners give icons in their icon slot a size through an internal environment value; an explicit size wins (component size cascade). Avatar and Item are the first owners.

Evidence

Capture set: iPhone 16 simulator, iOS 18.5, Xcode 16.4, built from this PR's head (cdf32a5d) by a scratch capture app. Each capture shows every size, every color, inherit under a pink and a blue foreground, all 29 built-in names, labeled status icons beside text, and the accent in three themes.

Light Dark Light, largest text Dark, largest text
Icon, Light Icon, Dark Icon, Light, largest text Icon, Dark, largest text

Not measured: an Icon inside an owner's icon slot. No Swift component gives its icon slot a size yet (Avatar and Item do not, as in core), so that state has no capture; IconTests.swift checks the precedence by rendering with the owner's size set.

check:knowledge does not scan native component specs yet; this record passes the checker in #8288, which does.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Writing rubric results

  • R1 narration: none; the record states what the view does.
  • R2 research: none.
  • R3 evidence: bases name specs, core's Icon, and tests; no pull request or commit.
  • R4 rejected alternatives: none recorded.
  • R5 change references: none.
  • R6 open questions: OQ1 (asset images in the theme's icon map) is unanswered and keeps its question.

Testing

  • vitest run packages/native: contract tests, including the new Icon contract and IconName generation, pass; the committed Swift binding matches the contracts.
  • swiftc -typecheck of AstryxNative, Astryx, the tests, and the templates passes; an immediate-mode render checks 20/24/12 pt boxes, the owner's size, growth at AX5, a hidden icon, an inherit icon taking the surrounding foreground, and that all 29 built-in symbols exist.
  • The Swift tests run in CI on macOS and on the iOS Simulator.
  • Screenshot states for the per-component screenshot target, once it lands: every size, every color, light and dark, inside an owner's icon slot, a labeled icon, and the largest text size.

Every changed file is under packages/native/.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 25, 2026 12:08am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 25, 2026, 1:00 AM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Previews ready: Storybook and Sandbox were built with this PR's docsite. CI evidence will be added when available.

View Storybook for this PR · View Sandbox for this PR

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for an engineering-owner decision: the draft component:swift/Icon record's FR2 (the Dynamic Type multiplier), FR4 (the color map), FR5 (the two initializers) and PR1 (symbols only), and import Astryx re-exporting AstryxNative, are not yet covered by any current spec. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or says which parts to change or drop. Separately, spec:AST-093 still needs two things:

  • Attach the simulator or device captures of every state (each size and color in light and dark, inside an owner's slot, labeled, and at the largest text size); the record and the description say they exist, but none is linked here.
  • Make pnpm check:knowledge validate packages/native/apple/Sources/Astryx/Icon/Icon.spec.md; its discovery skips native records today, so the record's frontmatter, id and platform_of go unchecked.

[Automated review]

| Behavior | Draws the SF Symbol the theme or the platform maps the name to, in a square box at core's icon size grown with Dynamic Type, in core's icon color token or the surrounding foreground. A name nothing maps draws nothing. |
| End-user impact | A person sees the platform's own symbol for each shared icon, sized and colored as on the web, and VoiceOver reads only icons that carry a label. |
| Builder impact | An app passes a shared name (`.search`) or a name it maps in its theme (`"acme:rocket"`). An SVG component, which the web also accepts, has no Swift form. |
| Compatibility/readiness | Additive: a new view and a new generated `IconName` type. Draft; built and tested on macOS and the iOS Simulator; device captures are attached to the change. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This row says device captures are attached to the change, but no capture or link is on the pull request. spec:AST-093 FR5 needs them attached (each size and color in light and dark, inside an owner's slot, labeled, and at the largest text size), or each uncaptured claim reported as not measured.

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec a2084db

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ready for an engineering owner's approval once the queued checks finish: the /approve-spec on this commit settles the draft component:swift/Icon requirements that were on hold, the simulator captures are attached, and the Swift tests pass on macOS and the iOS Simulator. None of the following blocks approval:

  • OQ1 asks whether a theme can map a name to an image the app ships, which spec:AST-082 FR17 already allows; asking how Swift carries such an asset would match it.
  • The record's schema_version: 5 and template_version: 8 come from #8288, so landing after it keeps the record's schema on main.

[Automated review]


## Open questions

- **OQ1 — Can a theme map a name to an image the app ships?** (`human-api`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: spec:AST-082 FR17 already lets native theme configuration name an image the app ships ({assetName: 'pixel.close'}), so whether a theme may map a name to an app image is decided. What stays open is how Swift carries one, since Theme(icons:) holds SF Symbol names only; asking that would keep OQ1 from reading as an open question about FR17 itself.

Comment on lines +97 to +98
nonisolated static func symbol(for name: IconName, themeIcons: [String: String]) -> String? {
themeIcons[name.rawValue] ?? astryxDefaultIcons[name.rawValue]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: this repeats AstryxStyle.icon(_:) (Theme.swift:107-111), which Banner and Button already use and which stops a debug build on an unknown name. Drawing from astryx.icon(props.icon.rawValue) keeps one lookup, so asset icons or any later resolution change land once.

| Behavior | Draws the SF Symbol the theme or the platform maps the name to, in a square box at core's icon size grown with Dynamic Type, in core's icon color token or the surrounding foreground. A name nothing maps draws nothing. |
| End-user impact | A person sees the platform's own symbol for each shared icon, sized and colored as on the web, and VoiceOver reads only icons that carry a label. |
| Builder impact | An app passes a shared name (`.search`) or a name it maps in its theme (`"acme:rocket"`). An SVG component, which the web also accepts, has no Swift form. |
| Compatibility/readiness | Additive: a new view and a new generated `IconName` type. Draft; built and tested on macOS and the iOS Simulator; device captures are attached to the change. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the attached captures come from the iPhone simulator, which spec:AST-093 FR5 accepts; 'simulator captures' would match them.

This branch was successfully deployed

1 active deployment
Preview — a2084dbb Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant