agentleFS
Sign inSign up

ios-agent-skill / site

Nagarjuna2997/ios-agent-skill/site/llms-full.txt

Give your coding agent the Apple references, Swift source and local tools it needs to build and review an iOS app. Use it to turn an app idea into an editable starter, improve an existing Swift project, and check the result with Xcode and the simulator. The project notebookBuild with AI. Check with evidence.Practical Swift and iOS workflows: focused reviews, simulator checks and honest setup advice.Explore all framework and feature guides → Guided series →All Swift reviews Testing & Simulator…

llms.txt36 starsChanged 9 days ago
  • Installs packages
# iOS Agent Skill

Give your coding agent the Apple references, Swift source and local tools it needs to build and review an iOS app. Use it to turn an app idea into an editable starter, improve an existing Swift project, and check the result with Xcode and the simulator.

# Practical Swift & AI development guides
https://nagarjuna2997.github.io/ios-agent-skill/blog.html

The project notebookBuild with AI.

Check with evidence.Practical Swift and iOS workflows: focused reviews, simulator checks and honest setup advice.Explore all framework and feature guides →
Guided series →All
Swift reviews
Testing & Simulator
AI clients
SwiftUI
Design & assets
Agent workflowSwift reviewsReview AI-generated Swift before you trust itA focused review, a small patch and a real test beat a confident completion message.Read the guide →
Testing & SimulatorFive checks for an AI-built SwiftUI appUse persistence, search and accessible states to make “it works” a testable claim.Read the guide →
AI clientsChoose an AI client for your iOS workflowUnderstand the difference between a skill, a local MCP connection and ChatGPT web setup.Read the guide →
AI clientsClaude Code: turn a Swift finding into a tested patchA small review-to-test loop for an existing iOS project.Read the guide →
AI clientsChatGPT and Codex: plan the app, then verify it locallySeparate a browser planning session from a connected development environment.Read the guide →
AI clientsGemini CLI: connect once and review one iOS featureAvoid duplicate connections and keep setup evidence separate from app evidence.Read the guide →
AI clientsMuse Code: what our integration actually verifiesDiscovery and hooks are useful milestones, but they are not a finished app.Read the guide →
SwiftUISwiftUI: review state transitions before polishing the screenMake loading, empty, success and error states part of the implementation brief.Read the guide →
Design & assetsGenerate light and dark asset catalogs without a paid design toolTurn explicit color tokens into Assets.xcassets, then inspect the result in your app.Read the guide →
AI clientsWhich Muse Code MCP settings actually work with a local Swift review server?A reproducible Muse 1.3.0 connection check, with the exact configuration and clear limits on what discovery proves.Read the guide →
Agent workflowHow can Claude Code hooks protect generated files and verify a project before stopping?A tested generated-file guard and Stop check, with configuration, reproducible exit codes, and enforcement limits.Read the guide →
Swift reviewsHow can I catch Swift concurrency review findings before accepting an agent’s changes?A real before-and-after MCP review with file and line evidence, plus the limits of text-based concurrency checks.Read the guide →
Swift reviewsHow should I review availability guards when moving a Swift app toward iOS 27?A measured guard-review example that separates SDK availability, runtime readiness, and static-analysis limits.Read the guide →
Design & assetsHow can design tokens generate light, dark and high-contrast color assets?Generate a real xcassets catalog from explicit color tokens, inspect its four variants, and compile it with actool.Read the guide →

---

# How can design tokens generate light, dark and…
https://nagarjuna2997.github.io/ios-agent-skill/blog/asset-catalogs.html

← Back to all articlesDesign & assets · iOS Agent Skill projectHow can design tokens generate light, dark and high-contrast color assets?Generate a real xcassets catalog from explicit color tokens, inspect its four variants, and compile it with actool.On this pageWhat belongs in the token file?How do I generate the catalog?What should I inspect in Contents.json?Did Xcode accept the generated files?Do I need Figma or a paid design tool?LimitsLast verified
The workflow
01
What belongs in the token file?02
How do I generate the catalog?03
What should I inspect in Contents.json?04
Did Xcode accept the generated files?
Define each semantic color with explicit light, dark, high-contrast-light, and high-contrast-dark values, then generate a named color set from that JSON. In this walkthrough, the published CLI created a real asset catalog and Xcode's asset compiler successfully produced an
Assets.car
 file.

What belongs in the token file?

A token name should express a role that the application can reuse.
AccentColor
,
Background
, and
TextPrimary
 describe intent more clearly than names tied to a particular screen coordinate. This example uses only the required accent to keep the generated output small enough to inspect completely.

The generator accepts a versioned JSON format, not arbitrary design prose. Every color needs all four appearance values. That requirement avoids silently guessing a dark equivalent from a light color. It also makes omissions visible during generation instead of leaving the coding agent to fill them differently in several Swift files.

Here is the
exact token input
 used for the run:

{
  "version": 1,
  "colors": {
    "AccentColor": {
      "light": "#2457DB",
      "dark": "#90B4FF",
      "highContrastLight": "#12358F",
      "highContrastDark": "#C7DAFF"
    }
  }
}

The repository documents sRGB hexadecimal values, including an optional alpha component. It also requires ASCII identifier names that are unique without regard to case. These are constraints of this generator's input contract, not a claim that every asset pipeline must use the same token format.

How do I generate the catalog?

With the published package, the command is:

npx -y ios-agent-mcp@2.7.0 assets \
  --tokens tokens.json \
  --output App/Assets.xcassets

The parent
App
 directory must already exist. The destination catalog must not exist, because this command refuses to overwrite an existing catalog. Generate a sibling directory when evaluating a token change, inspect the differences, and merge the intended output into the project through its normal review process.

For the actual run I used an isolated installation of that exact npm version and called its entry point directly. This avoids an ambiguous
latest
 dependency while recording evidence. The command reported two created catalog files: the catalog metadata and
AccentColor.colorset/Contents.json
.

The resulting directory structure is small:

Assets.xcassets/
  Contents.json
  AccentColor.colorset/
    Contents.json

Apple's
named-color format reference
 documents color-set metadata and color components. The generated
Contents.json
 is available with this draft, rather than represented only by an illustration.

What should I inspect in Contents.json?

Check that the color set has four entries and that each entry corresponds to the intended appearance. The default entry supplies the ordinary light value. The remaining entries distinguish dark appearance, increased contrast, and their combination. Compare the normalized channel values with the input rather than assuming a successfully written file has the intended color.

Also check the semantic name. A Swift reference to a differently spelled asset will not become correct merely because the catalog itself compiles. Keep the code and token vocabulary aligned. This generator does not audit all
Color
 calls or prove that the application actually uses the newly generated color.

For broader palettes, review foreground/background pairs together. Four present variants are a structural property. Readable contrast is a separate visual and numerical property that depends on the actual background, transparency, and surrounding interface.

Did Xcode accept the generated files?

Yes. I compiled the generated catalog with the installed simulator SDK:

mkdir -p compiled-assets
xcrun actool Assets.xcassets \
  --compile compiled-assets \
  --platform iphonesimulator \
  --minimum-deployment-target 17.0 \
  --target-device iphone \
  --output-format human-readable-text

The command returned exit code 0 and reported
compiled-assets/Assets.car
. The
recorded compiler output
 is included. This verifies that the asset compiler accepted the catalog under the recorded toolchain; it is stronger evidence than JSON parsing alone.

It is still not a screenshot test. The run did not launch an app, switch interface appearance, or inspect Dynamic Type. To establish the final user experience, integrate the catalog into the target, build, and inspect the relevant screens in all supported appearance states.

Do I need Figma or a paid design tool?

No. The input is ordinary JSON and can be authored in a text editor. If a designer already uses Figma, map resolved variables to the four explicit fields. Do not add a second tool solely to supply a small token file.

Icons are a separate concern. The repository can rasterize ordered SVG layers into a PNG, but a flattened PNG is not a native Icon Composer document. This color-only test generated no icon and makes no claim about Liquid Glass, required icon sizes, or App Store submission. Keep those deliverables and their verification records separate.

Limits

Successful generation does not certify accessibility, color contrast, semantic naming quality, or review acceptance. Existing catalogs are deliberately not overwritten. This run used one color and one simulator target; it did not exercise every possible alpha value, device family, or project integration. The cover image is conceptual artwork, not a rendering of these exact four swatches in an app.

Last verified

September 16, 2026. Published MCP package 2.7.0 with CLI 0.3.0; Node.js 24.15.0; Xcode 26.6 build 17F113. Asset generation and
actool
 compilation passed. Repository source:
docs/design/asset-generation.md
.

Example project:
ios-agent-skill
.

---

# How should I review availability guards when moving a…
https://nagarjuna2997.github.io/ios-agent-skill/blog/availability-guards.html

← Back to all articlesSwift reviews · iOS Agent Skill projectHow should I review availability guards when moving a Swift app toward iOS 27?A measured guard-review example that separates SDK availability, runtime readiness, and static-analysis limits.On this pageWhich versions must stay separate?What did the test contain?What did the reviewer report?What happened after editing the fixtures?Which checks belong in the real review?LimitsLast verified
The workflow
01
Which versions must stay separate?02
What did the test contain?03
What did the reviewer report?04
What happened after editing the fixtures?
Guard a symbol at the OS version where it becomes available, then verify the older-system fallback and any separate runtime readiness requirements. This article ran the published availability reviewer against synthetic files, but did not compile or execute iOS 27 APIs because the installed toolchain is Xcode 26.6.

Which versions must stay separate?

A toolchain version, the SDK it contains, and an application's minimum deployment target answer different questions. The repository's compatibility matrix distinguishes them because a newly installed SDK should not automatically remove support for older devices. A guard must describe the API being used, not simply repeat the newest version number in the project documentation.

There are two useful review failures to look for. An unguarded newer API can prevent supporting an older deployment target. An unnecessarily restrictive guard can send supported devices down an older fallback path. The latter may be invisible if every local test runs on the newest OS.

Apple's
Private Cloud Compute integration guide
 supplies a concrete iOS 27 example and discusses falling back on earlier systems. Treat that documentation as the API authority. The table below is instead a record of what this review tool detected.

What did the test contain?

I created four separate Swift source fixtures.
Card.swift
 uses
glassEffect()
 inside an iOS 27 guard. Three additional files contain bare symbol references for
PrivateCloudComputeLanguageModel
,
DynamicProfile
, and
OCRTool
. The latter files are explicitly labeled lexical fixtures: they exercise the reviewer's matching rules and are not compiling examples of those APIs.

The distinction is especially important for nested types or APIs that require framework-specific setup. A token appearing in a source file is enough to test a lexical check, but not enough to teach correct API usage. I have kept the synthetic source downloadable so a reader can see exactly what was tested.

I called
check_availability_guards
 through the published server's stdio MCP connection, passing the fixture directory. The
before response
 reported three blocker findings and one serious finding across four files.

What did the reviewer report?

Fixture location

Matched item

Reported result

Cloud.swift:2

PrivateCloudComputeLanguageModel

missing iOS 27 guard

Profile.swift:2

DynamicProfile

missing iOS 27 guard

Tools.swift:2

OCRTool

missing iOS 27 guard

Card.swift:6

glassEffect()

iOS 27 guard stricter than the reviewer's iOS 26 entry

This table is a reproducible analyzer result, not a complete iOS 27 availability index. Before adopting any symbol, check its current Apple declaration, enclosing type, platform, and minor-version requirements. A short maintained pattern table cannot enumerate every API in an SDK.

For the card, the proposed change is deliberately small:

if #available(iOS 26.0, *) {
    Text("Reading").glassEffect()
} else {
    Text("Reading")
}

The corresponding
Apple API reference
 remains the source for the modifier's contract. The fallback here preserves readable content; a production design may require more deliberate visual treatment.

What happened after editing the fixtures?

I changed the card guard from 27.0 to 26.0. For the lexical iOS 27 fixtures, I placed the references inside functions annotated with
@available(iOS 27.0, *)
. These changes test the rule's response to the relevant annotation; they do not turn placeholder references into complete Foundation Models features.

The same tool then returned zero findings across the four files. The
after response
 preserves that result. The before and after sources are included beside the JSON, allowing another reviewer to check that the experiment changed guards rather than removing the matched names entirely.

The clean result answers a narrow question: did the tool stop flagging these patterns? It does not answer whether the surrounding app compiles, whether every call site is guarded, or whether a selected model is available at runtime.

Which checks belong in the real review?

First identify the application's oldest supported OS. Then inspect the introduction version of each newly adopted symbol. Check the scope of the guard around the use, including helper methods and initializers; finding an unrelated guard elsewhere in a file is insufficient.

Next inspect the fallback as product behavior. Does the same action remain possible? Is disabled functionality explained? Does a view still have meaningful content? An empty branch can be syntactically acceptable while leaving an older device with a broken experience.

For model-backed features, separate API presence from model readiness. Apple's
generation guide
 documents checking availability before starting a session. An OS-version check alone does not establish that the model can answer a request.

Limits

The reviewer uses source heuristics and can miss scope, target settings, and minor-version distinctions. It is not an SDK parser or a compiler. This draft demonstrates its current behavior, including that limitation, and does not certify iOS 27 compatibility. Runtime fallback testing on supported OS versions is still required. Never replace a compiler diagnostic with a more convenient zero-findings report.

Last verified

September 16, 2026, local time. Published
ios-agent-mcp
 2.7.0; Node.js 24.15.0; host Xcode 26.6 build 17F113. No Xcode 27 build performed. Repository sources:
docs/compatibility-matrix.md
 and
docs/apple/ios-27-release-verification.md
.

Example project:
ios-agent-skill
.

---

# ChatGPT and Codex: plan the app, then verify it locally
https://nagarjuna2997.github.io/ios-agent-skill/blog/chatgpt-codex-ios.html

← Back to all articlesAI clients · iOS Agent Skill projectChatGPT and Codex: plan the app, then verify it locallySeparate a browser planning session from a connected development environment.On this pageStart with behavior, not a screen description aloneChoose the correct connection pathMake the handoff explicitReview the completion messageDecision checkpoints
The workflow
01
App brief02
Acceptance criteria03
Local implementation04
Recorded evidence
Start with behavior, not a screen description alone
A useful app brief describes what the developer should be able to verify. For a reading list, specify adding a book, preserving it after a relaunch and returning the original list after clearing a search. Ask the agent to list unresolved decisions before generating a large implementation.
Plan a reading-list app with local persistence and search.
List screens, state transitions and acceptance criteria.
Identify which checks require a running simulator.
Do not claim any check has passed until it has run.
Choose the correct connection path
For a local Codex environment, the project documents this MCP setup:
codex mcp add ios-agent -- npx -y ios-agent-mcp@latest
ChatGPT web is a different environment. It does not run that command on your Mac. Follow the
ChatGPT guide
 for supported skills or a separately configured HTTPS/private-tunnel connection. Availability depends on account and workspace policy; this project does not provide a public hosted Mac.
Make the handoff explicit
Carry the brief and acceptance criteria into the local project. Ask the connected agent to inspect existing code, retrieve relevant references and propose one implementation slice. Keep the actual app source and test results in the local development workflow. A plan written in a browser is not a simulator result.
Review the completion message
Require the changed files, commands run, outcomes and remaining gaps. Compilation is useful but does not prove persistence or accessible states. Use the simulator for visual evidence and tests for behavior where practical. Do not infer better token efficiency merely because references are retrieved in smaller sections.
Official Codex MCP documentation
 ·
Project setup instructions
Decision checkpoints
Situation
Action or status
Evidence or boundaryPlanning
Screens and acceptance criteria
A plan, not a running appImplementation
Connected local environment
Inspectable source changesVerification
Build, tests and simulator
Recorded evidence

---

# Choose an AI client for your iOS workflow
https://nagarjuna2997.github.io/ios-agent-skill/blog/choose-ai-client.html

← Back to all articlesAI clients · iOS Agent Skill projectChoose an AI client for your iOS workflowUnderstand the difference between a skill, a local MCP connection and ChatGPT web setup.On this pageA skill and a tool connection do different jobsClaude and Codex on your MacGemini CLI and MuseChatGPT in a browserUse the same first taskDecision checkpoints
The workflow
01
A skill and a tool connection do different jobs02
Claude and Codex on your Mac03
Gemini CLI and Muse04
ChatGPT in a browser
A skill and a tool connection do different jobs
A skill supplies instructions and references to a coding agent. An MCP connection exposes callable tools. Installing source guidance alone does not give a client permission or an executable path to build your app in Xcode.
iOS Agent Skill focuses on Claude, ChatGPT/Codex, Gemini CLI and Muse. Choose the client you already use and start with one small review before enabling build or simulator actions.
Claude and Codex on your Mac
Local coding clients can connect to the npm server. Open your app folder, follow the appropriate setup command and verify tool discovery. Claude Desktop uses a different configuration path from Claude Code; do not paste one client’s configuration into another.
Claude setup
 ·
Codex setup
Gemini CLI and Muse
The repository includes setup instructions for both. The Gemini record verifies extension validation and a stdio connection. Muse’s record verifies tool discovery and a Stop hook. These are bounded checks, not evidence that every model-driven app-building task succeeds.
Gemini CLI setup
 ·
Muse setup
ChatGPT in a browser
A browser conversation cannot use a local stdio command as if it were a terminal client. The guide separates portable skills from an HTTPS or private-tunnel MCP connection. Supported options depend on your account and workspace policy. Do not expose a local development server publicly just to make a connection work.
Read the ChatGPT connection options
Use the same first task
List the available iOS Agent tools.
Review one Swift file and show the finding locations.
Explain which checks ran and which remain unverified.
The package requires Node.js 20 or later. Xcode build, test and simulator operations require macOS and Xcode. The project is MIT-licensed, but your selected AI service may have its own costs.
When comparing clients, keep the project, prompt and acceptance criteria constant. A successful connection is a setup result; it is not a benchmark or a guarantee of a finished app.
Read the recorded client checks
Decision checkpoints
Situation
Action or status
Evidence or boundaryLocal coding client
Local stdio MCP server
Node.js; Xcode for simulator workBrowser conversation
Supported remote connection or portable guidance
Account policy and secure hostingConnection test
Tool discovery
Does not establish model task success

---

# How can Claude Code hooks protect generated files and…
https://nagarjuna2997.github.io/ios-agent-skill/blog/claude-hooks.html

← Back to all articlesAgent workflow · iOS Agent Skill projectHow can Claude Code hooks protect generated files and verify a project before stopping?A tested generated-file guard and Stop check, with configuration, reproducible exit codes, and enforcement limits.On this pageWhich problem does each hook solve?How are the three events connected?What did the worked example return?What does the Stop check establish?What can these hooks miss?LimitsLast verified
The workflow
01
Which problem does each hook solve?02
How are the three events connected?03
What did the worked example return?04
What does the Stop check establish?
Use a PreToolUse hook to reject edits to generated files, a PostToolUse hook to regenerate outputs after source changes, and a Stop hook to run deterministic checks. In this example, direct script tests blocked an instruction mirror, allowed its source, and passed repository consistency checks; a complete Claude-driven lifecycle session was not rerun.

Which problem does each hook solve?

A generated file can look like the easiest place to make an edit. The change appears to work until the generator runs again and removes it. A prompt asking the coding agent to remember the source of truth helps, but a script can detect the specific mistake earlier and return a useful explanation.

The example repository keeps its source instructions in
SKILL.md
. Its supported client instruction files are generated mirrors. The guard protects those mirror paths and tells the agent to edit the source. This is a repository-maintenance example used in an iOS tooling project, not a claim that every iOS app should generate the same files.

The second hook synchronizes mirrors after the source changes. The final hook checks that the repository is internally consistent before the turn ends. Each check has a small, inspectable responsibility. None is a replacement for compiling an app or exercising a simulator.

Anthropic's
hook reference
 documents event configuration and hook inputs. Keep the client reference close when adapting a hook: input fields and event behavior should come from the client, while your file-protection policy should come from your project.

How are the three events connected?

The repository configures these command handlers:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/hooks/guard-generated-files.sh", "timeout": 10}]
    }],
    "PostToolUse": [{
      "matcher": "Edit|Write|MultiEdit",
      "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/hooks/sync-mirrors-on-edit.sh", "timeout": 30}]
    }],
    "Stop": [{
      "hooks": [{"type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/scripts/hooks/verify-repo.sh", "timeout": 60}]
    }]
  }
}

The quoted project directory matters when a checkout path contains spaces. The command scripts must exist and be executable. Inspect them before enabling the configuration, and merge the hooks with existing settings instead of overwriting permissions or other handlers.

Do not copy these paths into an unrelated app and expect them to work. An app might protect generated API models or an Xcode project derived from a specification. Its generator and verification command will differ. Start by naming the actual source file, generated outputs, and command that relates them.

What did the worked example return?

I sent the guard synthetic Edit input for
AGENTS.md
, using the checkout's absolute path. The process returned exit code 2 and a message explaining that the file is generated from
SKILL.md
. It named the source file and the synchronization command. No edit was performed; this test exercises the decision script directly.

I repeated the input with
SKILL.md
. The guard returned exit code 0 with no error. That pair tests both sides of the policy: a protected target is rejected, while a permitted target can proceed. Testing only the rejection could hide a guard that blocks all development.

Then I ran the configured verification script directly:

bash scripts/hooks/verify-repo.sh

It returned exit code 0. The
recorded inputs and results
 preserve these checks and a successful direct PostToolUse synchronization run. Read the
guard script
,
synchronization handler
, and
verification script
 before enabling them; they depend on a complete source checkout. The guard message, rather than a screenshot of a green badge, is the useful evidence because it shows the corrective instruction that would be returned.

What does the Stop check establish?

The repository documentation describes mirror synchronization, instruction frontmatter, referenced documentation paths, and subagent frontmatter as consistency checks. A successful run means those checks passed for this checkout at this moment. It does not mean Swift tests ran, an app launched, or every reference page is technically correct.

For an application project, select checks that match the work. A resource-only edit might need asset compilation and a build. A persistence change needs behavior tests. Avoid describing a successful documentation script as an app acceptance result merely because both run at the same lifecycle event.

The most useful failure output identifies the check, the affected path, and the repair command. A bare failure code forces the agent to rediscover the problem. Conversely, a message claiming a test ran when the script only checked metadata creates false confidence.

What can these hooks miss?

The configured matcher covers named editing tools. It is not a filesystem security boundary. A different write path, including a shell command, needs separate consideration. Path normalization, symlinks, and platform separators also deserve tests before extending this pattern across operating systems.

A PostToolUse action occurs after a tool has run, so it cannot retroactively prevent that write. Keep generated changes reviewable and retain CI checks even when hooks work locally. Client-side feedback and remote verification serve different points in the workflow.

Limits

This article verified standalone script behavior, not the current client's complete hook dispatch. The PostToolUse script also completed successfully when invoked directly with synthetic source-edit input; it was not exercised in a model session. The example protects instruction mirrors, not arbitrary Xcode outputs. Existing documentation contains broader descriptions of generated mirrors; use the tested paths and current script when deciding what is actually protected.

Last verified

September 16, 2026. Installed Claude Code 2.1.273; direct Bash/Python hook tests on macOS; repository verification exit code 0. Repository source:
docs/orchestration/hooks.md
; the downloadable record states the narrower tested scope.

Example project:
ios-agent-skill
.

---

# Claude Code: turn a Swift finding into a tested patch
https://nagarjuna2997.github.io/ios-agent-skill/blog/claude-swift-review.html

← Back to all articlesAI clients · iOS Agent Skill projectClaude Code: turn a Swift finding into a tested patchA small review-to-test loop for an existing iOS project.On this pageKeep the first task deliberately smallConnect the local toolsAsk for evidence before a rewriteStop at a meaningful checkpointDecision checkpoints
The workflow
01
Connect MCP02
Review one file03
Inspect patch04
Build + test
Keep the first task deliberately small
When an agent reviews an entire app at once, it can be difficult to tell which suggestions are worth acting on. Start with the Swift file you just changed. Define the behavior it must preserve, then ask for one focused review before allowing edits.
Connect the local tools
claude mcp add ios-agent -- npx -y ios-agent-mcp@latest
Run the command in your project context, reconnect Claude Code and confirm the server is available. The local server needs Node.js 20 or later. Xcode operations additionally need a Mac with Xcode. Claude Desktop has a different configuration path; use the
client setup guide
.
Ask for evidence before a rewrite
Review the concurrency in the Swift file I changed.
Show each finding with its location and supporting context.
Propose one minimal patch. Preserve the public behavior.
After approval, build and run the relevant tests.
Report failures and checks you could not run.
Read the suggested diff. A heuristic finding about actor isolation is a reason to inspect the surrounding code, not a reason to annotate every type. Keep unrelated refactors out of the patch so a failing test has a smaller set of possible causes.
Stop at a meaningful checkpoint
Save the changed files, commands used and remaining failures. If the build cannot run because Xcode or the scheme is unavailable, record that constraint instead of claiming success. The repository's Claude record demonstrates a bounded text-repair test; it does not establish complete app generation.
Official Claude MCP documentation
 ·
Project verification record
Decision checkpoints
Situation
Action or status
Evidence or boundaryBefore editing
Read the finding in context
Location and reasonAfter patching
Review the diff
Only intended changesBefore completion
Build and test
Commands, outcomes, remaining gaps

---

# Gemini CLI: connect once and review one iOS feature
https://nagarjuna2997.github.io/ios-agent-skill/blog/gemini-cli-ios.html

← Back to all articlesAI clients · iOS Agent Skill projectGemini CLI: connect once and review one iOS featureAvoid duplicate connections and keep setup evidence separate from app evidence.On this pageUse one installation pathRetrieve only what the feature needsDiagnose setup separatelyKnow what the project testedDecision checkpoints
The workflow
01
Project folder02
One connection03
Focused references04
Swift review
Use one installation path
The project supports a local MCP connection and also has extension guidance. Start with one path rather than installing both and assuming every duplicate tool is necessary. From your app folder, the documented direct connection is:
gemini mcp add ios-agent -- npx -y ios-agent-mcp@latest
Restart or reconnect Gemini CLI and confirm that the server appears. This workflow is for Gemini CLI, not Gemini web chat. The server requires Node.js 20 or later, and simulator tools require macOS and Xcode.
Retrieve only what the feature needs
For a persistence change, ask for the relevant local source or guide sections before reviewing the implementation. Loading an entire documentation collection can obscure the specific behavior you are trying to verify. Focused retrieval is an organization technique, not a measured cost-saving promise.
Inspect this app's persistence change.
Find relevant local references and identify assumptions.
Review only the affected Swift files.
Propose a minimal fix and a test for relaunch persistence.
Diagnose setup separately
If no tools appear, first check the configuration, command path and package availability. A failed connection is not a Swift compiler failure. If tools appear but a build fails, inspect the project, scheme and Xcode diagnostic instead of reinstalling the client repeatedly.
Know what the project tested
The Gemini CLI 0.49.0 record verifies extension validation and a stdio connection to the published package. It explicitly leaves the model session unverified. Reproduce your own task, record its results and report only what happened in that run.
Official Gemini CLI MCP documentation
 ·
Verification record
Decision checkpoints
Situation
Action or status
Evidence or boundaryTools absent
Check configuration and executable
Connection problemBuild fails
Inspect scheme and Xcode diagnostic
Project problemWrong behavior
Reproduce acceptance criterion
Implementation problem

---

# Generate light and dark asset catalogs without a paid…
https://nagarjuna2997.github.io/ios-agent-skill/blog/generate-asset-catalog.html

← Back to all articlesDesign & assets · iOS Agent Skill projectGenerate light and dark asset catalogs without a paid design toolTurn explicit color tokens into Assets.xcassets, then inspect the result in your app.On this pageMake colors explicitGenerate a new catalogChoose every appearance deliberatelyKeep icon layers editableInspect in contextDecision checkpoints
The workflow
01
Color tokens02
Four appearances03
Asset catalog04
App verification
Make colors explicit
Asset generation is a shipped workflow in ios-agent-mcp 2.7.0. It converts a strict JSON token file into a new asset catalog locally. It does not require Figma, an AI model or an API key. Start with semantic names rather than scattering literal colors through SwiftUI views.
{
  "version": 1,
  "colors": {
    "AccentColor": {
      "light": "#2457DB",
      "dark": "#90B4FF",
      "highContrastLight": "#12358F",
      "highContrastDark": "#C7DAFF"
    }
  }
}
Generate a new catalog
npx -y ios-agent-mcp@latest assets --tokens tokens.json --output App/Assets.xcassets
The parent directory must exist. The generator refuses to overwrite an existing catalog. Generate a sibling catalog when reviewing changes, compare the output and merge only the intended assets.
Choose every appearance deliberately
All four color appearances are required; the generator does not invent a dark-mode conversion. Names must be ASCII identifiers and unique ignoring case, with AccentColor present. Values use the documented sRGB hex format. Check contrast against the actual background rather than assuming a high-contrast token name guarantees readability.
Keep icon layers editable
New app scaffolds can retain editable SVG layers and render their composition as an opaque 1024-pixel PNG. Customize placeholder shapes before distributing an app. A flattened PNG is not a native Liquid Glass Icon Composer bundle, and generating it does not certify App Review acceptance.
Inspect in context
Use the named assets in the app, build it and inspect light, dark and higher-contrast appearances. The generator handles catalog structure; you still verify the visual result and accessible usage. Keep these checks in your acceptance criteria so a later design change does not silently undo them.
Full token schema and icon workflow
Decision checkpoints
Situation
Action or status
Evidence or boundaryLight and dark
Explicit token values
Inspect both appearancesHigh contrast
Two additional explicit values
Measure against real backgroundsExisting catalog
Generate a sibling output
Review and merge intentionally

---

# Which Muse Code MCP settings actually work with a local…
https://nagarjuna2997.github.io/ios-agent-skill/blog/muse-connection.html

← Back to all articlesAI clients · iOS Agent Skill projectWhich Muse Code MCP settings actually work with a local Swift review server?A reproducible Muse 1.3.0 connection check, with the exact configuration and clear limits on what discovery proves.On this pageWhat was actually tested?Which configuration should I start with?How can I repeat the example?What should I check when discovery fails?LimitsLast verified
The workflow
01
What was actually tested?02
Which configuration should I start with?03
How can I repeat the example?04
What should I check when discovery fails?
Muse Code 1.3.0 connected to the published Swift review server using
schema_version: 1
 and the camel-case
mcpServers
 configuration key. The recorded check discovered 36 tools and executed a Stop hook, but did not verify a model choosing tools or building an app.

What was actually tested?

This walkthrough is deliberately a connection test. The local
echo
 provider exercises initialization without making a model request. That distinction matters: a successful tool list establishes that two programs can communicate, while a completed development task requires evidence from the model, the tool, and the resulting project.

The repository's installation guide and its existing client record describe this boundary. I repeated the discovery harness against an isolated installation of the published
ios-agent-mcp@2.7.0
 package. The result again contained 36 tool names. Representative entries included
review_swift_concurrency
,
search_local_references
,
create_app
, and
simulator_list
. A command-based Stop hook also left the expected marker.

The record is available as
the complete verification JSON
. It identifies the client build, server package, discovered catalog, and the checks that remain false. It contains no account tokens, personal project content, or generated-app claims.

Which configuration should I start with?

For a local installation, this is the configuration shape documented by the project:

{
  "schema_version": 1,
  "mcpServers": {
    "ios-agent": {
      "command": "/absolute/path/to/ios-agent-mcp",
      "args": []
    }
  }
}

Merge that server entry into your existing settings rather than replacing the file. The absolute executable path is a placeholder: obtain the real location from your installation. The harness used an absolute Node executable and the server's JavaScript entry point, rather than relying on shell startup files to add a command to PATH.

The repository's configuration note records camel-case
mcpServers
 as the tested key. It does not establish that the snake-case alternative
mcp_servers
 works, and this article makes no such claim. Likewise, the schema version belongs to Muse's configuration format; it is not the npm package version and should not be changed to match a release number.

Meta's
configuration documentation
 is the vendor reference. That page did not expose readable content to the unauthenticated research tool during this review. The compatibility statement here therefore rests on the executable record and repository documentation, not an invented quotation from Meta.

How can I repeat the example?

Use the verification harness from a source checkout and point it at the Muse executable and the installed server entry point:

node scripts/verify-muse.mjs \
  /absolute/path/to/muse \
  /absolute/path/to/ios-agent-mcp/dist/unified.js

Both paths must exist. The harness prepares temporary configuration and a temporary workspace, places the existing skill in that workspace, starts Muse with the echo provider, and records MCP discovery. It checks several representative tools rather than accepting any nonempty response. Finally it checks the Stop marker and removes its temporary files.

For this article, the server came from a separate installation pinned to 2.7.0. A first run against the working checkout exposed 37 tools, including an unpublished feedback tool. That was useful development evidence but unsuitable as a description of the npm release. Repeating the check against the released artifact resolved the mismatch. This is why a package version string alone is insufficient when testing uncommitted builds.

A compact reading of the published-package result is:

{
  "serverVersion": "2.7.0",
  "toolCount": 36,
  "stopHookExecuted": true,
  "provider": "echo",
  "modelSessionVerified": false
}

What should I check when discovery fails?

First confirm the executable path outside the agent. Then validate the JSON and check that the server is nested under the tested key. Preserve unrelated settings when correcting either problem. A missing executable and a valid executable that never completes initialization are different failures, so keep the startup error with the client version.

Next compare the environment used by your shell with the environment the client inherits. This example avoids package fetching during startup; it does not prove an
npx
 download will succeed under every network policy. A global installation may be convenient, but the sandboxed download workaround has not been verified by this article.

Finally, do not paste private settings into a public issue. A synthetic configuration with executable placeholders, the versions, and the failure category is usually enough to begin diagnosis.

Limits

Tool discovery does not verify tool invocation, simulator permissions, model quality, skill selection, or app completion. The Stop test does not verify PreToolUse, PostToolUse, or an observer feature. No claim about HTTP transport, pricing tiers, or training policy follows from this stdio test. Recheck compatibility after either program changes.

Last verified

September 16, 2026, America/Chicago; the JSON timestamp is September 17 in UTC. Muse Code 1.3.0 build 1.3.0-R3233.1, published MCP package 2.7.0, Node.js 24.15.0. Repository sources:
docs/mcp/installation.md
 and
examples/client-verification/muse-1.3.0.json
.

Example project:
ios-agent-skill
.

---

# Muse Code: what our integration actually verifies
https://nagarjuna2997.github.io/ios-agent-skill/blog/muse-verification.html

← Back to all articlesAI clients · iOS Agent Skill projectMuse Code: what our integration actually verifiesDiscovery and hooks are useful milestones, but they are not a finished app.On this pageRead the evidence before the compatibility claimInstall outside the agent sandboxVerify in stagesKeep failures usefulDecision checkpoints
The workflow
01
Install server02
Discover tools03
Check hook04
Verify model task
Read the evidence before the compatibility claim
The repository records Muse Code 1.3.0 discovering the published MCP tools and executing a Stop hook. The record uses an echo provider. It does not verify a model-driven app-building session, pre/post hooks or observer behavior. That distinction is the starting point for trying the integration.
Install outside the agent sandbox
The project setup guide uses a global install, followed by a settings entry:
npm install -g ios-agent-mcp@latest
Follow the
Muse setup section
 and merge its configuration with your existing settings. Preserve the schema version and other servers. If the command cannot be found, inspect the installed executable path instead of guessing a new location.
Verify in stages
Confirm the server starts and tools are discoverable.Ask for a read-only review of a small synthetic Swift file.Inspect the returned finding and its source location.Only then try a controlled edit and an available build/test step.
Record the Muse version and npm package version alongside the outcome. A successful hook invocation does not prove that the agent understood the failure or repaired it correctly.
Keep failures useful
If a step fails, distinguish an unavailable executable, an invalid configuration, a tool error and a model decision. Keep private project source out of public reports. A small synthetic reproduction is easier to inspect and safer to share.
This article describes the project's recorded test scope, not a newly verified Muse release or a claim that its background observer works with every subagent. Future compatibility claims should be backed by a new reproducible record.
Inspect the exact verification record
Decision checkpoints
Situation
Action or status
Evidence or boundaryTool discovery
Recorded
Server communicationStop hook
Recorded with echo provider
Hook invocationModel-driven app build
Not verified by this record
Requires a separate real task

---

# Review AI-generated Swift before you trust it
https://nagarjuna2997.github.io/ios-agent-skill/blog/review-swift-with-ai.html

← Back to all articlesSwift reviews · iOS Agent Skill projectReview AI-generated Swift before you trust itA focused review, a small patch and a real test beat a confident completion message.On this pageStart with one questionConnect, then narrow the taskVerify the proposed fixKeep the review reproducibleDecision checkpoints
The workflow
01
Start with one question02
Connect, then narrow the task03
Verify the proposed fix04
Keep the review reproducible
Start with one question
A broad “review my app” request can produce a long list without a clear next action. Start with the file or feature you changed. Ask your agent to distinguish a compiler error, a heuristic finding and a design suggestion.
iOS Agent Skill provides local references and file-located reviews. It does not replace the Swift compiler. A reported issue is something to investigate, and a clean result is not proof that the app is correct.
Connect, then narrow the task
For Claude Code, run this from your app folder:
claude mcp add ios-agent -- npx -y ios-agent-mcp@latest
Reconnect the client and confirm that the tools appear. For another client, use its
setup guide
. The local server requires Node.js 20 or later.
Review the Swift concurrency in the files I changed.
Search only relevant local references.
For each finding, show the file, line and reason.
Propose the smallest fix and explain how to test it.
Separate verified results from suggestions.
Verify the proposed fix
For example, when a finding concerns UI-observed state, inspect where that state is mutated and what actor isolation the type actually has. Do not add annotations across the project simply because a heuristic suggests them. Ask for the relevant source context, then build with your target SDK.
Review the diff before accepting it. Run tests for the affected behavior and record the command and outcome. If no suitable test exists, say that explicitly instead of treating compilation as a behavior test.
Keep the review reproducible
Keep one issue and its proposed patch together.Record the package version and client used.Use a synthetic example when reporting a false positive.Never attach private app source or credentials to a public issue.
The useful result is an inspectable change with evidence. Token savings and a quality advantage over other workflows have not been established.
Inspect the review tools
 ·
Read the evidence and limits
Decision checkpoints
Situation
Action or status
Evidence or boundaryCompiler diagnostic
Build with the same scheme and SDK
A successful buildHeuristic finding
Inspect the surrounding isolation and ownership
A justified patch or documented false positiveBehavior concern
Reproduce the user action
A passing behavioral test

---

# How can I catch Swift concurrency review findings before…
https://nagarjuna2997.github.io/ios-agent-skill/blog/swift-concurrency.html

← Back to all articlesSwift reviews · iOS Agent Skill projectHow can I catch Swift concurrency review findings before accepting an agent’s changes?A real before-and-after MCP review with file and line evidence, plus the limits of text-based concurrency checks.On this pageWhy review before accepting a change?What source did the reviewer inspect?Which findings were returned?What change was made?How should I use the result in an app?LimitsLast verified
The workflow
01
Why review before accepting a change?02
What source did the reviewer inspect?03
Which findings were returned?04
What change was made?
Run a focused concurrency review on the proposed changes, inspect each finding in its isolation context, and then use the compiler and behavior tests to verify the repair. A synthetic Swift model produced two real findings in this walkthrough; a narrower, main-actor-isolated version produced none, which is evidence about the review tool rather than proof of race freedom.

Why review before accepting a change?

Agent-generated code can combine patterns from different examples. A model might use observation for UI state while also starting detached work that writes the same state. The important review question is who owns the mutable value and which execution context may access it. Counting occurrences of
async
 does not answer that question.

The repository documents a focused tool called
review_swift_concurrency
. It returns file locations, rule identifiers, severities, explanations, and suggested changes. That structure makes a finding easier to investigate than a broad request to improve concurrency. The tool is still heuristic: it reads source patterns rather than constructing the compiler's full isolation model.

Swift's
concurrency migration guide
 is the primary reference for language-level diagnosis. The experiment below measures this project's reviewer. It is not a survey establishing which mistakes coding agents make most often.

What source did the reviewer inspect?

I created an intentionally small, synthetic fixture called
FeedModel.swift
. It contains no user app code, networking, or persistence:

import Observation

@Observable
final class FeedModel {
    var title = ""
    func refresh() {
        Task.detached { self.title = "Updated" }
    }
}

The detached operation has no independent computation to perform. It exists only to assign a string. That makes the example suitable for discussing ownership without pretending that moving expensive work onto the main actor is a general performance solution.

I connected an MCP client to the published 2.7.0 server and called
review_swift_concurrency
 with the fixture directory as its absolute
path
 argument. This was a real tool call, not an illustration of what a response might look like. The complete response is preserved in
the before record
.

Which findings were returned?

The response reported one blocker and one serious finding across one Swift file:

Location

Rule

Reported severity

FeedModel.swift:3

observable-without-mainactor

blocker

FeedModel.swift:7

task-detached

serious

Those are the reviewer's classifications. Do not confuse them with compiler diagnostic levels. In particular, observation alone does not establish that every observable type must have an explicit main-actor annotation. Actual ownership, callers, and project isolation settings matter. A reviewer that uses text patterns cannot infer every one of those conditions.

The second finding points to an operation whose detached execution is unnecessary in this fixture. The repair should preserve the program's intended behavior, not merely remove a keyword until the count turns green. In a real application, identify the work that should run independently, the values it returns, and the isolated state that consumes them.

What change was made?

For this UI-state example I made ownership explicit and removed the unnecessary task:

import Observation

@MainActor
@Observable
final class FeedModel {
    var title = ""
    func refresh() {
        title = "Updated"
    }
}

The change does not introduce another asynchronous wrapper. Callers now have to respect the model's actor isolation. If a real refresh loads data, keep the loading contract explicit and update the model through its isolation boundary; this tiny example does not implement that service.

I called the same reviewer against the second fixture. The
after record
 reports zero findings across one file. Both source fixtures are included with the draft so the line references and change can be inspected. There is no hidden rewrite, model scoring step, or manually adjusted result.

How should I use the result in an app?

Start with the changed model and its callers. Determine whether isolation is explicit on the type, inherited through context, or selected by build settings. Then decide whether the suggested annotation describes the intended ownership. A correct fix in one model may be unnecessary or misleading in another.

Next compile using the application's real Swift language mode and deployment settings. The fixture's successful review does not establish that a view initializer, service, or test can call the repaired model correctly. Those dependencies are precisely where a compiler adds information that the text reviewer lacks.

Finally exercise the behavior that motivated the edit. A feed refresh should show loading, success, failure, and cancellation states as required by the app. A clean review cannot demonstrate any of them. Keep review output beside build and test results so the acceptance report does not flatten several different checks into a single pass.

Limits

This experiment did not run a coding model, compile these fixtures, or measure runtime races. It does not establish an agent error rate or a token-saving advantage. The published reviewer's explanatory wording is stronger than the evidence its pattern matching can prove; interpret findings as investigation leads. A file-level annotation can also conceal a problem in another type in the same file. Zero findings means no matching rule fired, not that all concurrency behavior is correct.

Last verified

September 16, 2026, local time. Published
ios-agent-mcp
 2.7.0, Node.js 24.15.0, actual stdio tool calls. Repository sources:
docs/mcp/tools.md
,
docs/mcp/examples.md
, and
docs/evidence-and-scope.md
.

Example project:
ios-agent-skill
.

---

# SwiftUI: review state transitions before polishing the…
https://nagarjuna2997.github.io/ios-agent-skill/blog/swiftui-state-review.html

← Back to all articlesSwiftUI · iOS Agent Skill projectSwiftUI: review state transitions before polishing the screenMake loading, empty, success and error states part of the implementation brief.On this pageA polished screen can still hide missing behaviorDescribe the transitionsKeep ownership understandableUse reviews as a starting pointVerify more than the screenshotDecision checkpoints
The workflow
01
Loading02
Empty03
Content04
Recoverable error
A polished screen can still hide missing behavior
A preview usually shows a convenient state. Real users encounter an empty store, an unfinished request and a failed operation. Before asking an agent to polish a SwiftUI screen, list the states and the actions that move between them.
Describe the transitions
For a reading list, a fresh store starts empty. Adding an item produces content. A search with no matches is different from having no saved books. A storage failure should preserve useful context and offer an appropriate recovery action. Treat those as separate acceptance criteria.
Review this screen's state transitions.
Distinguish loading, empty library, empty search and error.
Keep previews deterministic with injected sample data.
List one observable acceptance check for each state.
Keep ownership understandable
Ask which object owns the state, which views read it and where mutations occur. Follow the project's concurrency guidance and validate isolation with the installed Swift toolchain. Do not change observation mechanisms solely because an API name sounds newer; the app's deployment target and existing architecture matter.
Use reviews as a starting point
The repository includes a SwiftUI review tool and local state/data-flow guidance. Inspect each finding in context. Heuristics do not provide a complete model of runtime behavior, and a screen that compiles may still show the wrong state after an action.
Verify more than the screenshot
Capture each important state in the simulator, then separately test transitions such as clearing a search and relaunching after saving. Inspect larger text and meaningful control labels. Visual checks and accessibility checks answer different questions; do not substitute one for the other.
Read the local state guidance
 ·
Use the five-check verification workflow
Decision checkpoints
Situation
Action or status
Evidence or boundaryEmpty library
Fresh test store
Explain how to add an itemEmpty search
Nonmatching query
Clear search without losing dataError
Controlled failure
Preserve context and offer recovery

---

# Five checks for an AI-built SwiftUI app
https://nagarjuna2997.github.io/ios-agent-skill/blog/verify-swiftui-simulator.html

← Back to all articlesTesting & Simulator · iOS Agent Skill projectFive checks for an AI-built SwiftUI appUse persistence, search and accessible states to make “it works” a testable claim.On this pageDefine “working” before generating code1. Add and reopen2. Search and recover3. Check empty and error states4. Inspect accessibility5. Keep build and test evidenceDecision checkpoints
The workflow
01
Define “working” before generating code02
1. Add and reopen03
2. Search and recover04
3. Check empty and error states
Define “working” before generating code
A screenshot of a populated list can look convincing while persistence or search is broken. Give your coding agent observable acceptance criteria before asking it to implement the feature. A reading-list app makes a useful small exercise because its behavior is easy to describe.
1. Add and reopen
Create a synthetic book, terminate the app and reopen it. Confirm the item survives. A list that only stays populated during one process has not demonstrated persistence. Keep this test separate from any preview data used for development.
2. Search and recover
Search for a known title, then a title that is absent. Clear the query and verify the original list returns. Decide whether matching should ignore case before implementing the test; do not silently change acceptance criteria to fit the output.
3. Check empty and error states
Use a fresh test store to exercise an empty library. Trigger a controlled storage failure through a test seam if available. The app should explain what happened and provide an appropriate next action. A happy-path screenshot does not verify an error state.
4. Inspect accessibility
Check meaningful labels, readable text and the layout with larger text settings. A screenshot helps reveal clipping, but it does not prove VoiceOver behavior or control semantics. Report visual and accessibility checks separately.
5. Keep build and test evidence
Run the build and tests on a configured Mac with Xcode. Capture the important simulator states and compare them with the acceptance criteria. The bundled MCP tools can build, test, install, launch and capture screens; the agent still needs to choose the right project and checks.
Build the Reading List project and run its tests.
Verify add, persistence after relaunch, search and empty state.
Capture the relevant simulator screens.
List any failed or unverified acceptance criteria.
The linked sample contains recorded project evidence; it does not prove that an arbitrary app or a new client session passed these checks. Start with the sample’s own instructions and do not overwrite real user data.
Run the Reading List demo
 ·
Set up the verification workflow
Decision checkpoints
Situation
Action or status
Evidence or boundaryPersistence
Terminate and relaunch
Previously saved item remainsSearch
Clear a nonmatching query
Original list returnsAccessibility
Inspect labels and larger text
Readable layout and meaningful controls

---

# Daily community monitor
https://nagarjuna2997.github.io/ios-agent-skill/community-monitor.html

Daily community monitorDaily community monitor
Operational instructions for the scheduled Codex task. Scheduling is in Codex, not the Pages workflow. The daily Pages job refreshes download counts; it does not search the web.
Repository workflow

Read
site/community-mentions.json
 and the live
community.html
 page first.
Verified mentions are stored in that JSON. Generate the isolated HTML section in
site/community.html
 with
python3 scripts/render-community.py
.
Run
python3 scripts/render-community.py --check
,
python3 scripts/render-site.py --check
,
python3 -m unittest discover -s scripts/tests -p test_community.py
 and
bash scripts/hooks/verify-repo.sh
.
Keep verification dates stable on no-change days. Do not commit daily reports, timestamp-only updates or inaccessible-candidate churn. Record the run report in the scheduled task.
Work from a clean current checkout; preserve unrelated work, never reset it. Commit only intended files. Push main to publish through GitHub Pages. Never publish npm or change package versions.
Initial deferred baseline links: LibHunt, PickMCP, Product Hunt and npm web page could not be opened successfully. Retry later; inaccessible does not mean removed. Agentmods has a verified alternative canonical card; do not add its hooks/tag pages as duplicate coverage.
Deduplicate language variants, mirrors on the same site and alternate canonical URLs using editorial judgment plus the renderer’s URL checks. Each standalone republication must be explicitly labelled a mirror.
This initial run found skills.rest beyond the supplied baseline. Exclude scraper republications from the directory list. Reddit is maintainer-started with independent comments, not an independent endorsement.

User-supplied monitoring specification
You are responsible for maintaining the public “Community & Mentions” section for this project:
GitHub:

https://github.com/Nagarjuna2997/ios-agent-skill
npm:

https://www.npmjs.com/package/ios-agent-mcp
Project names to monitor:
ios-agent-skill
ios-agent-mcp
iOS Agent Skill
iOS Agent MCP
Nagarjuna2997/ios-agent-skill
Run this workflow every day.
Your job is to search the public internet for new mentions of the project, verify them, compare them against the mentions already shown on the website, and update the website only when a genuinely new and useful mention is found.
Do not add duplicate links.
Do not rewrite the section every day if nothing has changed.
Do not manufacture work.
If everything already looks correct, leave the website unchanged.
SEARCH FOR MENTIONS ACROSS:
Google-indexed web pages
developer blogs
personal blogs
newsletters
Reddit
Hacker News
X/Twitter pages that are publicly indexed
LinkedIn pages that are publicly indexed
YouTube videos and descriptions
GitHub repositories
GitHub issues and discussions
MCP directories
Agent Skill directories
Claude Code directories
Codex directories
Swift/iOS developer communities
Product Hunt
DEV Community
Medium
Hashnode
npm-related pages
Apple/Swift ecosystem websites
AI coding-agent resource pages
comparison websites
curated developer-tool lists
software discovery websites
international or translated pages
Search both exact names and URLs.
Use queries based on:
"ios-agent-skill"
"ios-agent-mcp"
"Nagarjuna2997/ios-agent-skill"
"github.com/Nagarjuna2997/ios-agent-skill"
"npm ios-agent-mcp"
"iOS Agent MCP"
"iOS Agent Skill"
Also search combinations with:
Swift
SwiftUI
Xcode
Xcode 27
iOS
MCP
Claude Code
Codex
Cursor
Gemini CLI
coding agents
Apple development
AI coding
CLASSIFY EVERY RESULT
For each result determine whether it is:
INDEPENDENT COMMUNITY MENTION
Someone else discussed, recommended, reviewed, compared, commented on, linked to, or used the project.
This is the highest-value category.
DIRECTORY / INDEX
A third-party directory automatically or manually indexed the project.
This is useful for discovery but should not be described as an endorsement.
COMMUNITY DISCUSSION
A Reddit, Hacker News, GitHub, forum, or other discussion containing real comments about the project.
ARTICLE / BLOG
An independent article that discusses the project.
MY OWN CONTENT
Posts originally created by Nagarjuna Reddy / Nagarjuna2997.
These can be listed as project coverage but must not be presented as independent press.
MIRROR / REPUBLICATION
A site copying, syndicating, translating, or mirroring one of my own articles.
Do not describe this as independent praise.
OFFICIAL PROJECT PAGE
GitHub, npm, project website, Product Hunt listing, etc.
VERIFY BEFORE ADDING
Before adding a result:
Open the page.
Confirm that it genuinely mentions this exact project.
Confirm the URL still works.
Check whether it is already on the website.
Do not add search-result pages that only coincidentally contain similar words.
Do not confuse similarly named iOS MCP projects with this repository.
Do not add spam or scraped garbage pages unless they provide meaningful discovery value.
Do not claim:
“endorsed by”
“recommended by”
“partnered with”
“officially supported by”
“featured by”
unless the source explicitly supports that statement.
Use safer wording such as:
“Mentioned on”
“Discussed on”
“Indexed on”
“Discovered on”
“Listed on”
“Community discussion”
“Featured, discussed, indexed, or discovered across”
WEBSITE SECTION
Maintain a section titled:
Community & Mentions
Keep this introduction:
“Seeing ios-agent-skill shared, indexed, discussed, and discovered across the developer community means a lot to me. Thank you to everyone who has checked out the project, shared feedback, starred the repository, or helped others discover it. I’m still improving it, and every bit of support genuinely motivates me to keep building.”
Then show the verified mentions as clean cards.
Each card should contain:
Site/platform name
Short category such as:
Community
Article
Directory
Developer Resource
Discussion
Launch
Package
One short factual description
Direct external link
Do not show exaggerated marketing language.
Prefer the strongest mentions first.
Suggested ordering:

Independent human/community mentions
Independent articles/blogs
Developer resource collections
Discussions
Curated directories
Automated directories
My own posts
Mirrors

CURRENT KNOWN LINKS
Use these as the starting baseline and do not duplicate them:
Kimi

https://www.kimi.ai/resources/software-skills-for-agents
LibHunt

https://www.libhunt.com/compare-appstore-doctor-vs-ios-agent-skill
SkillsMP

https://skillsmp.com/creators/nagarjuna2997/ios-agent-skill/skill
Awesome Skills

https://www.awesomeskills.dev/en/skill/nagarjuna2997-ios-agent-skill
Awesome MCP Servers

https://mcpservers.org/servers/nagarjuna2997/ios-agent-skill
PickMCP

https://pickmcp.com/servers/Nagarjuna2997/ios-agent-skill
Agentmods

https://agentmods.dev/hooks/nagarjuna2997/ios-agent-skill
SkillWorks

https://skillworks.thecompound.tech/claude-md-examples
Reddit / r/Xcode

https://www.reddit.com/r/Xcode/comments/1v8j94w/i_got_tired_of_ai_agents_writing_2019era_swiftui/
DEV Community

https://dev.to/nagarjuna_reddy_7ca85e003/im-building-an-mcp-toolbox-for-swift-xcode-and-ios-simulator-kp9
Product Hunt

https://www.producthunt.com/products/ios-agent-mcp
npm

https://www.npmjs.com/package/ios-agent-mcp
GitHub

https://github.com/Nagarjuna2997/ios-agent-skill
Do not assume this list is complete.
Search for new mentions every day.
THANK-YOU AREA
Keep this message near the bottom:
“Thank you for supporting ios-agent-skill.”
“This started as a side project because I wanted AI coding agents to work better with real iOS development workflows. Seeing developers discover it, read the documentation, give feedback, and share it keeps me motivated to make it better.”
Then add:
“Using ios-agent-skill in a real project? I’d love to hear about it.”
Keep buttons for:
View on GitHub
Share Feedback
Star the Project
DAILY BEHAVIOR
Every daily run should follow this process:

Read the existing website section first.

Extract all URLs already displayed.

Search broadly for new mentions.

Verify every candidate.

Deduplicate against existing links.

Add only genuinely new mentions.

Preserve all good existing links.

Fix broken links if necessary.

Do not remove a mention simply because it did not appear in today's search.

Do not modify unrelated parts of the website.

Do not redesign the page unless there is an actual layout problem.

Keep the section fast, responsive, accessible, and mobile friendly.

External links should open safely in a new tab where appropriate.

If the website repository has tests or linting, run them after changes.

If the build can be run, verify it before committing.

GIT WORKFLOW
If new verified mentions are found:
Update only the necessary website files.
Run available checks.
Commit with a clear message such as:
docs: add new community mentions
or
site: update ios-agent-skill mentions
Do not create meaningless daily commits when there are no changes.
If no new mention exists:
Do not change files.
Do not create a commit.
Report:
“No new verified mentions today. Website unchanged.”
DAILY REPORT
At the end of every run report:
New mentions found:
[number]
Added to website:
[number]
Independent human/community mentions:
[number]
Directory/index mentions:
[number]
Duplicates ignored:
[number]
Questionable/unverified results ignored:
[number]
Website changed:
YES / NO
Build/check status:
PASS / FAIL / NOT REQUIRED
For each new mention show:
Platform:
URL:
Type:
Why it matters:
Added to website: YES / NO
Most important rule:
The goal is not to make the project appear more popular than it is.
The goal is to honestly document the growing public footprint of ios-agent-skill and thank the people and communities helping others discover it.
If a link is already present and correct, leave it alone.
If nothing new happened, do nothing.

---

# Community & Mentions
https://nagarjuna2997.github.io/ios-agent-skill/community.html

Public footprint
Community & Mentions
A record of verified directory listings, articles and developer discussions about ios-agent-skill. Thank you to everyone who has shared useful feedback or helped others find the project.
Verified links, clearly labelled. Directory listings are not endorsements; maintainer posts and republications are identified separately.

Third-party articleVoronkin StudioDiscusses ios-agent-mcp and the Swift, Xcode and simulator development workflow.Visit Voronkin Studio (opens in a new tab)

Developer ResourceKimiLists ios-agent-skill among open-source software skills and links to the repository.Visit Kimi (opens in a new tab)

Discussion · maintainer-startedReddit / r/XcodeReaders discuss Xcode overlap, rule sources and the need for measured evidence.Visit Reddit / r/Xcode (opens in a new tab)

Developer Resource · indexSkillWorksIndexes the repository’s CLAUDE.md among real-world instruction-file examples.Visit SkillWorks (opens in a new tab)

DirectorySkillsMPProvides a skill listing with the source repository and installation information.Visit SkillsMP (opens in a new tab)

DirectoryAwesome SkillsLists the skill with a direct source link and installation options.Visit Awesome Skills (opens in a new tab)

DirectoryAwesome MCP ServersLists the MCP server and reproduces project documentation.Visit Awesome MCP Servers (opens in a new tab)

DirectoryAgentmodsIndexes the project’s Gemini CLI extension with links to its source.Visit Agentmods (opens in a new tab)

Directoryskills.restProvides an indexed skill page linking to this repository.Visit skills.rest (opens in a new tab)

Directory · automated indexMCPHQIndexes the MCP package with installation configurations and repository release information.Visit MCPHQ (opens in a new tab)

Security analysis · automatedVerifyMCPPublishes automated package checks and captured tool schemas, including limitations and findings.Visit VerifyMCP (opens in a new tab)

Tracking index · automatedSmall PrintTracks the Gemini extension’s instruction changes and publishes automated change flags.Visit Small Print (opens in a new tab)

Package analysisSocketProvides an npm package analysis page for ios-agent-mcp with package metadata and source links.Visit Socket (opens in a new tab)

Project discoveryLibHuntIndexes the project, related alternatives and mentions, including the maintainer’s DEV and Show HN posts.Visit LibHunt (opens in a new tab)

Article · maintainer-authoredDEV CommunityThe maintainer explains the Swift, Xcode and simulator toolbox and asks for feedback.Visit DEV Community (opens in a new tab)

Official project pageGitHubSource code, issues and releases maintained by the project.Visit GitHub (opens in a new tab)

Using ios-agent-skill in a real project? I’d love to hear about it.
View on GitHub
Share Feedback

---

# Documentation index
https://nagarjuna2997.github.io/ios-agent-skill/docs-index.html

Documentation indexBrowse the repository’s documentation indexes and their linked source guides on GitHub.AI and Apple IntelligenceProfessional Motion and AnimationApple Framework Indexdocs/frameworks/apple-intelligence.mddocs/frameworks/core-ai.mddocs/frameworks/ml/coreml.mddocs/frameworks/core-spotlight-rag.mddocs/testing/evaluations.mddocs/frameworks/foundation-models.mddocs/frameworks/extended-apple-frameworks.mddocs/frameworks/ml/natural-language.mddocs/frameworks/ml/sound-analysis.mddocs/frameworks/ml/speech.mddocs/frameworks/ml/translation.mddocs/frameworks/ml/vision.mddocs/frameworks/authentication-services.mddocs/frameworks/cryptokit.mddocs/frameworks/device-integrity.mddocs/frameworks/local-authentication.mddocs/frameworks/avfoundation.mddocs/frameworks/photosui.mddocs/frameworks/services/passkit.mddocs/frameworks/storekit.mddocs/frameworks/activitykit.mddocs/frameworks/app-clips.mddocs/frameworks/app-intents.mddocs/frameworks/swift-charts.mddocs/swiftui/views-and-controls.mddocs/frameworks/tipkit.mddocs/uikit/uikit-essentials.mddocs/frameworks/visionkit.mddocs/frameworks/widgetkit.mddocs/frameworks/cloudkit.mddocs/frameworks/core-data.mddocs/frameworks/data-concurrency.mddocs/frameworks/foundation.mddocs/frameworks/swiftdata.mddocs/design/stunning-ui-patterns.mddocs/design/liquid-glass-adoption.mddocs/web/native-vs-web-animation.mddocs/swiftui/animations.mddocs/tooling/device-hub.mddocs/tooling/fm-cli.mddocs/tooling/foundation-models-instruments.mddocs/tooling/ios-simulator-mcp.mddocs/tooling/xcode-27-agents.mddocs/frameworks/arkit.mddocs/frameworks/metal.mddocs/frameworks/realitykit.mddocs/frameworks/scenekit.mddocs/frameworks/hardware/core-motion.mddocs/frameworks/hardware/core-nfc.mddocs/frameworks/hardware/healthkit.mddocs/frameworks/hardware/core-bluetooth.mddocs/frameworks/network-framework.mddocs/frameworks/networking.mddocs/platforms/ios.mddocs/platforms/macos.mddocs/platforms/tvos.mddocs/platforms/visionos.mddocs/platforms/watchos.mddocs/frameworks/background-tasks.mddocs/frameworks/services/contacts.mddocs/frameworks/core-location.mddocs/frameworks/services/eventkit.mddocs/frameworks/hardware/homekit.mddocs/frameworks/mapkit.mddocs/frameworks/oslog.mddocs/frameworks/usernotifications.mddocs/frameworks/services/weatherkit.mdData and PersistenceProfessional UI/UX Systempalette generationcolor accessibilityGraphics, 3D, and Spatial DevelopmentNetworking and Connectivitybackend knowledge layerPerformanceSecurity, Authentication, and PrivacyWebKit and JavaScript Interoperability

---

# Backend architectures by app workflow
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-app-architectures.html

← Back to all frameworks & guides
All articles →Backend · Reference guideBackend architectures by app workflowRepository guidance for Backend architectures by app workflow. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

A notes app's state flow
Avoid hidden coupling
Evidence to keep

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Choose feature contract
↓2
Separate repository and transport
↓3
Define offline ownership
↓4
Test failure transitions

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Backend architectures by app workflowBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Keep feature models independent from service-specific records. The following are design options, not endorsements or live-tested deployments. Start with the failure state and choose a backend only after a small prototype verifies it.

App

Suggested boundary

Data and recovery design

Check before shipping

Notes

Local store → sync repository → CloudKit or an authorized API

Stable note IDs, tombstones, revision/conflict policy; local edit first

Offline edit, account switch, delete/reappear race

Social

Feed repository → Supabase/Postgres or Firebase data + storage

Server pagination, ownership policy, moderation, upload cleanup

Cross-user denial, blocked content, cursor stability

File storage

Transfer coordinator → authorized object store

File-backed uploads, progress/cancel, short-lived signed access, metadata record

Interrupted upload, orphan cleanup, unauthorized download

Chat

Conversation store → realtime coordinator + catch-up API

Message IDs, acknowledgements, ordering, dedupe and durable catch-up

Disconnect during send, duplicate events, revoked membership

Subscription

StoreKit client → entitlement repository → trusted verification

Server verifies transaction/entitlement contract; client boolean is not access control

Expired/revoked entitlement, offline grace policy, replay

Collaborative

Local document → sync engine → shared records or revisioned server

Explicit conflict/merge rules, participant permissions, change cursor

Concurrent edits, removal from share, stale permissions

Offline-first

Local database + outbox → bounded sync worker

Durable operation IDs, replay policy, tombstones and visible pending state

Relaunch mid-sync, auth loss, repeated delivery, migration

A notes app's state flow

The UI reads local notes. A repository accepts an edit and atomically records pending work. A sync owner sends it when authenticated, records the acknowledged revision, then updates local status. Incoming changes merge through the same repository. A view disappearing cancels view-owned fetches, not necessarily durable sync work. Account changes cancel and isolate both.

Avoid hidden coupling

Use DTO-to-domain mapping at the boundary. Do not put SDK singleton calls inside SwiftUI body, copy a token into each feature or create competing refresh loops. Tests inject an offline repository. Provider migration then replaces an adapter and data migration, rather than every screen. See
REST
 and
BackendPatterns
.

Evidence to keep

Save exact SDK versions, policy tests, schema migration tests and device lifecycle outcomes. Test a rejected operation as carefully as a successful one. A compiled view is not evidence of authorized storage or correct offline synchronization. Consult
choosing a backend
,
security
 and
privacy
.

---

# Appwrite for Apple clients
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-appwrite.html

← Back to all frameworks & guides
All articles →Backend · Reference guideAppwrite for Apple clientsRepository guidance for Appwrite for Apple clients. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Swift SDK setup
Data and realtime
Production gate

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Match SDK and server
↓2
Authenticate registered client
↓3
Scope resource permissions
↓4
Verify recovery and upgrades

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Appwrite for Apple clientsBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Appwrite combines auth, data, storage, functions and realtime, with managed or self-hosted operations. Choose it when its permissions and hosting model fit the team. Self-hosting also means owning backups, mail delivery, upgrades, monitoring and security fixes.

Swift SDK setup

Add
https://github.com/appwrite/sdk-for-apple
 through SPM. The official Apple quickstart currently references
10.1.0
; pin the version you validate against your server. Register the app's bundle platform and inject the endpoint/project ID. Never call a server-key configuration method with an administrator API key in an iOS target.

import Appwrite

func signIn(account: Account, email: String, password: String) async throws {
    _ = try await account.createEmailPasswordSession(email: email, password: password)
}
func signOut(account: Account) async throws {
    _ = try await account.deleteSession(sessionId: "current")
}

These snippets need the external SDK and configured service; they were not SDK-compiled here. OAuth uses the documented browser flow and callback configuration. Validate the exact registered callback route, not a substring match. Inspect session persistence in the pinned SDK and never copy sensitive session state to UserDefaults. Test relaunch, expired sessions and identity switching.

Data and realtime

Match the database API generation to the server/SDK—current TablesDB terminology and older Databases examples must not be mixed blindly. Use explicit permissions, bounded queries, stable ordering and cursor pagination. Client filtering is not authorization. Storage buckets/files need permission, size/type and orphan-cleanup policies. Functions keep privileged operations server-side and must validate caller identity and input.

A realtime subscription is not an offline database. Reconnect with backoff, resubscribe once, deduplicate events and fetch missing changes. One repository owns subscriptions and cancellation. Queue offline edits only with idempotency and conflict rules; background execution and socket delivery are not guaranteed on iOS.

Production gate

Test with synthetic users in an isolated instance: cross-user denial, OAuth cancellation, callback cold launch, session revocation, pagination, file cancellation and deletion of account-owned data. Use injected repositories for offline Swift tests; self-hosted compatibility and upgrades need integration tests against the actual server. Configure development and production endpoints separately and avoid logging sessions or signed URLs.

Review
privacy
 for identity, cloud storage, telemetry and data synchronization. Plan server/SDK upgrades, schema migration and backup restore before shipping. Primary source:
official Apple quickstart
, with linked SDK/server references for the pinned release.

---

# Authentication and session boundaries
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-authentication.html

← Back to all frameworks & guides
All articles →Backend · Reference guideAuthentication and session boundariesRepository guidance for Authentication and session boundaries. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

OAuth callback ownership
Secure persistence and refresh
Native identity versus browser OAuth
Production verification

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Restore session
↓2
Start provider transaction
↓3
Validate callback and exchange
↓4
Observe identity changes

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Authentication and session boundariesBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Model authentication as restoring, signed out, authenticating, signed in, refreshing and revoked states. The first screen must wait for restoration or show a deliberate loading state; an SDK client existing is not evidence of a valid session.

OAuth callback ownership

Use the provider's documented ASWebAuthenticationSession or native SDK flow. PKCE binds an authorization code to the initiating client; state binds the callback to the initiating attempt; an OIDC nonce binds an ID token to that attempt. They are different protections. Prefer the SDK's implementation instead of manually concatenating an authorization URL or decoding a JWT as proof of identity.

Register a custom URL scheme or an associated-domain universal link, configure the exact redirect allowlist on the server, and route scene/openURL callbacks to one coordinator. Match scheme, host and path exactly, reject unexpected ports/userinfo, and let the SDK validate the transaction's state and exchange the code. Never use
url.absoluteString.contains("callback")
 as authorization. Universal links also require a valid association file and entitlements. A syntactically matching URL alone does not authenticate anybody.

BackendPatterns
 demonstrates exact callback-route validation. It intentionally does not implement OAuth state, PKCE or token exchange. Test cancelled browser sessions, duplicate callbacks, cold launch, expired links, alternate schemes and malicious suffix hosts. Do not log callback URLs: they can carry credentials.

Secure persistence and refresh

Inspect the pinned SDK's storage implementation. If you supply sensitive persistence, use Keychain with a deliberate accessibility class, access group and synchronizability policy.
WhenUnlockedThisDeviceOnly
 may be inappropriate for a legitimate background operation; select the policy from the actual access requirement. Do not put tokens in UserDefaults, app-group preferences, analytics properties or source control. Do not replace SDK session storage with an incomplete homemade token store.

Use one refresh owner. Concurrent 401s must join one refresh attempt, not race and overwrite rotated refresh tokens. Retry the original operation only if safe; do not turn every 403 into refresh. Clear identity-scoped local records, image caches and pending work on account changes. Stop listeners before presenting another user's data. Account deletion is a server-authorized operation; signing out is not deletion.

Native identity versus browser OAuth

Apple and Google can provide native ID-token credentials for supported SDK exchange. Native Apple uses a fresh nonce and its documented digest/exchange contract; an authorization code is not an ID token. Other social providers generally use browser OAuth. Do not reuse the native Apple example for GitHub, Slack or X. Verify the provider name and credential type in the exact Swift SDK version.

Supabase
 and
Firebase
 contain SDK-specific snippets.
App Review 4.8
 defines conditions and exceptions for an equivalent login option; social login is a reason to review the policy, not an automatic scanner error.
Privacy
 covers deletion and data disclosures.

Production verification

Test relaunch, refresh-token rotation, server revocation, offline restoration, clock skew, reauthentication before destructive actions and cancellation before callback delivery. Use synthetic accounts in isolated environments. Never paste provider secrets into agent context. Official references:
ASWebAuthenticationSession
,
Supabase sessions
,
native mobile deep links
.

---

# AWS Amplify in a Swift app
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-aws-amplify.html

← Back to all frameworks & guides
All articles →Backend · Reference guideAWS Amplify in a Swift appRepository guidance for AWS Amplify in a Swift app. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Install and environment
Data, files and functions
Recovery and production

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Pin generated environment
↓2
Configure Auth and API plugins
↓3
Enforce AWS authorization
↓4
Test sync and transfer recovery

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
AWS Amplify in a Swift appBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Amplify fits teams already operating Cognito, AppSync, S3 and Lambda with explicit cloud ownership. It is less suitable when IAM, multiple auth modes and deployment configuration would exceed the team's operational capacity.

Install and environment

Use
https://github.com/aws-amplify/amplify-swift
 and the documented products/plugins for Auth, API and Storage. Pin the SDK. Initialize plugins and configure once in the composition root before repositories issue work. Match generated backend configuration to the environment and SDK generation; do not combine an old amplifyconfiguration.json recipe with new outputs without following the migration guide.

Cognito Auth handles user-pool sessions and supported social/hosted UI flows. Configure redirect sign-in/sign-out URLs and native provider identifiers; use the documented web authentication session integration. Inspect credential persistence and never store AWS secret access keys in the app. Temporary identity credentials and server IAM permissions are different from hardcoded administrator keys.

Data, files and functions

AppSync/GraphQL needs a deliberate authorization mode: Cognito, IAM, API key or other configured identity paths have different access boundaries. A client API key is not user authorization. Generated models and schema belong in reviewed source/code-generation workflows. Keep UI code behind repositories instead of exposing generated network types everywhere.

S3 Storage requires prefix/object authorization, upload cancellation and cleanup. Invoke Lambda/serverless integrations through authenticated APIs with server validation; never ship deployment credentials. Push support is product/provider-specific: confirm the currently supported AWS integration and APNs setup instead of assuming an installed Auth plugin delivers notifications.

Recovery and production

GraphQL cache behavior and DataStore synchronization are distinct; do not claim all Amplify API requests have automatic offline replay. Choose a local cache/outbox with conflict rules, cursor pagination and idempotent writes. Own async tasks/listeners, preserve cancellation and avoid multiplying retries already performed by an SDK. Reconcile on foreground; iOS background execution is bounded.

Test configuration selection, auth restoration/revocation, cross-user denial, upload progress/cancel, subscription recovery and schema evolution. Use dependency injection for offline tests and an isolated AWS environment for separately authorized integration tests. No deployment or Amplify SDK compilation occurred in this change. Follow
privacy
 for Cognito identities, cloud files, telemetry and deletion across services.

Primary sources:
Amplify Swift
,
Auth
,
Data API
.

---

# Choosing an iOS backend
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-choosing-a-backend.html

← Back to all frameworks & guides
All articles →Backend · Reference guideChoosing an iOS backendRepository guidance for Choosing an iOS backend. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Decision matrix
Capabilities to price and prototype
A useful prototype

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Define hardest requirement
↓2
Compare operating costs
↓3
Prototype denied and offline paths
↓4
Choose a verified fit

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Choosing an iOS backendBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Begin with the hardest requirement: cross-platform identity, relational joins, offline edits, Apple-only sharing or operational control. There is no universally best service. Budget for migration and policy testing as well as a happy-path SDK demo.

Decision matrix

Backend

Strong fit

Important tradeoff

Offline and local testing

CloudKit

Apple accounts, private records and system sharing

iCloud identity and Apple platform boundaries; public data needs deliberate permissions

App owns local persistence; CKSyncEngine coordinates supported sync, not a complete database. Test account transitions on devices

Supabase

Postgres relationships, SQL/RLS, flexible auth, storage and realtime

Schema, grants and RLS require server expertise

Explicit app cache/outbox; local stack and policy tests; no automatic Swift offline database

Firebase

Mobile auth, realtime data, push and operational SDKs

Document/tree modeling, Rules and indexes; query costs and lock-in

Firestore persistent cache is supported on Apple; emulator tests differ from production

Amplify

Cognito, AppSync and S3 within AWS operations

IAM/auth modes, generated configuration and cloud complexity

Cache/offline behavior depends on selected API/DataStore architecture; do not assume all GraphQL calls sync

Appwrite

Integrated auth/data/storage/functions, managed or self-hosted

Hosting upgrades, permissions and SDK/server compatibility

Explicit local state/outbox and isolated test instance

Custom REST

Stable contract, existing backend, transport ownership

Build auth, rate limiting, observability and policy tests

URLProtocol/injected transports; explicit cache and sync design

GraphQL

Typed multi-resource queries, schema evolution

Resolver authorization, code generation and partial errors

Normalized cache does not by itself queue offline mutations

Capabilities to price and prototype

CloudKit sharing is Apple-native; multi-provider login favors Supabase, Firebase, Cognito or Appwrite. Relational constraints favor Postgres or a custom relational backend. Realtime delivery is not a guarantee of ordered, durable synchronization in any of these choices. CloudKit subscriptions, FCM and AWS push integrations have different setup paths; Appwrite or custom services need a compatible push path and APNs configuration.

Supabase Storage, Firebase Storage, S3 and Appwrite Storage need per-object access policy and orphan cleanup. Server functions differ in runtime and deployment: Supabase Edge Functions, Firebase Functions, Lambda and Appwrite Functions all keep privileged credentials off-device. CloudKit is not a general server-function runtime.

Self-hosting is an option for Supabase and Appwrite; it transfers backups, patching, mail delivery and incident response to you. Export data, preserve stable domain IDs and isolate provider types behind repositories to reduce migration cost. Assess Swift SDK release cadence and deployment targets from the pinned package, not this matrix.

A useful prototype

Implement one list, one upload and one identity transition. Test two users, denied access, revocation, airplane mode, interrupted writes and account switching. Measure payloads and query count, not only the number of SDK calls. Choose only after validating the hardest flow.

Convex, PocketBase, Parse/Back4App, Hasura and Workers/D1/R2 can fit particular teams; confirm a maintained Swift integration or use a documented HTTP boundary. Vapor, FastAPI and Node.js/NestJS are server implementation choices, not interchangeable iOS SDKs. Keep secondary choices behind the same repository contracts.

See
app architectures
,
security
, and each provider's primary sources in
the source record
.

---

# CloudKit as an Apple-native backend
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-cloudkit.html

← Back to all frameworks & guides
All articles →Backend · Reference guideCloudKit as an Apple-native backendRepository guidance for CloudKit as an Apple-native backend. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Configuration and identity
Databases, records and sharing
Sync and local persistence
Verification and privacy

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Select container and database
↓2
Check iCloud account
↓3
Merge local and remote records
↓4
Verify sharing and recovery

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
CloudKit as an Apple-native backendBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
CloudKit is a major choice for Apple-only apps with iCloud identity, private synchronization and system sharing. It is not a drop-in multi-provider auth service or a relational SQL backend. Start with
existing CloudKit API guidance
, then apply the lifecycle and deployment decisions here.

Configuration and identity

CloudKit ships in the Apple SDK; no external SPM package is required. Enable iCloud/CloudKit capabilities and select the actual container entitlement.
CKContainer.default()
 uses the configured default container, not an assumption that its identifier equals the bundle ID. Development and production environments have separate schema/data behavior.

Check
accountStatus()
 and present deliberate unavailable/restricted/indeterminate states. Do not promise an email address or another cross-service identity from an iCloud account. A user can change accounts while the app is installed. Rebind caches and sync state to the current account; never blend private data between accounts.

Databases, records and sharing

Choose private, public or shared CKDatabase scope explicitly. Public database does not mean every user can read/write everything; inspect record type permissions. Model CKRecord IDs, zones and ownership as stable domain decisions. CKQuery is paginated; follow cursors and inspect per-record failures. Store file data with CKAsset through managed local files, retaining those files for the operation's required lifetime.

Use CKShare and supported sharing flows for collaboration. Shared data has permission and participant transitions that must be tested. Never assume a record in the shared database is editable. Resolve conflict errors with server/client/ancestor records and a domain merge policy rather than last-writer-wins by accident.

Sync and local persistence

CKSubscription/push signals tell the app to fetch changes; they are not guaranteed complete event delivery. CKSyncEngine coordinates supported change tracking and scheduling on available OS versions; persist its state and your own local records/outbox. It is not a complete local database. Recreate state safely after account/zone deletion and handle retries/rate limits as documented.

SwiftData + CloudKit and NSPersistentCloudKitContainer are higher-level sync choices with model/schema constraints. Choose one owner for a dataset; do not simultaneously mutate the same records through an unrelated manual sync layer. Check availability and supported schema features in the installed SDK. Plan additive migrations, defaults and old-client compatibility before deploying schema.

Verification and privacy

Use injected repositories for offline tests;
BackendPatterns
 includes an account-status adapter compiled against the local Apple SDK without calling iCloud. Test real private/public/shared databases on entitled devices with multiple accounts, revoked sharing, offline changes, quota/rate limits, asset failures and account switching. Simulator results alone are insufficient for production iCloud claims.

Review CloudKit Dashboard schema promotion, indexes and permissions deliberately; never make cloud changes from an unaudited preview. Show sync/pending/conflict status and a recovery path. Account deletion/data deletion requirements depend on the app's account model; iCloud usage does not exempt data disclosure or retention review. See
privacy
.

Primary sources:
CloudKit
,
CKSyncEngine
,
SwiftData sync
,
Core Data and CloudKit
.

---

# Firebase for iOS
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-firebase.html

← Back to all frameworks & guides
All articles →Backend · Reference guideFirebase for iOSRepository guidance for Firebase for iOS. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Installation and authentication
Data, files and authorization
Optional products
Concurrency, lifecycle and migration
Testing and production gate

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Configure selected products
↓2
Restore Firebase identity
↓3
Test Rules and cache states
↓4
Verify deletion and telemetry

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Firebase for iOSBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Firebase suits apps needing mobile authentication, document or realtime data, push and operational tooling. Avoid adopting every product by default: analytics, crash reporting and messaging add configuration and disclosure responsibilities independently of database access.

Installation and authentication

Add
https://github.com/firebase/firebase-ios-sdk
 with SPM and only the needed products. Register the exact bundle ID, include the intended GoogleService-Info.plist in the target, and configure Firebase once at the app composition root. This plist contains client identifiers, not an admin service-account credential. Pin the SDK and check its minimum iOS/Xcode versions.

import FirebaseAuth

func passwordSignIn(email: String, password: String) async throws {
    _ = try await Auth.auth().signIn(withEmail: email, password: password)
}
func guestSignIn() async throws {
    _ = try await Auth.auth().signInAnonymously()
}
func appleSignIn(idToken: String, rawNonce: String) async throws {
    let credential = OAuthProvider.appleCredential(
        withIDToken: idToken, rawNonce: rawNonce, fullName: nil
    )
    _ = try await Auth.auth().signIn(with: credential)
}
func googleSignIn(idToken: String, accessToken: String) async throws {
    let credential = GoogleAuthProvider.credential(
        withIDToken: idToken, accessToken: accessToken
    )
    _ = try await Auth.auth().signIn(with: credential)
}

These SDK snippets require configured native Apple/Google flows and an installed Firebase release; they were not compiled against Firebase in the offline sample. Apple needs a fresh nonce/digest pairing; Google needs its own SDK and client ID configuration. Do not log either credential. Phone auth requires its platform verification setup, APNs/reCAPTCHA path as applicable, quotas and abuse controls. Test on a device, including denial and timeout.

Own the auth-state listener and remove it at teardown. Wait for restoration before selecting an account cache. Inspect SDK token persistence instead of copying tokens into UserDefaults. Linking anonymous accounts needs conflict handling; deleting an auth user does not automatically remove Firestore documents or Storage objects.

Data, files and authorization

Firestore document queries need indexes, limits and cursor pagination. Apple-platform offline persistence can serve cached results; distinguish pending writes and server acknowledgement. Realtime Database uses a different tree model, rules language and persistence behavior; do not assume Firestore rules or query semantics apply.

Cloud Storage requires object rules, upload cancellation/progress and orphan cleanup. Cloud Functions perform privileged operations with server validation; callable transport alone does not establish authorization. Test unauthenticated and cross-user denial in Security Rules. Admin SDK operations are a separate trusted boundary. App Check helps reject untrusted clients but does not replace user authorization or Rules.

Optional products

Product

Integration decision and verification

FCM

Configure APNs, authorization UX and token rotation; notification delivery is not guaranteed sync

Crashlytics

Review crash collection, breadcrumbs and custom keys; never attach sessions or payloads

Analytics

Enable only with a documented data/consent policy and accurate disclosures

Remote Config

Use safe defaults and bounded fetch; never store secrets or treat a flag as authorization

App Check

Configure a supported Apple attestation provider and debug tokens only in development

Concurrency, lifecycle and migration

Repository adapters own listeners and cancellation; marshal UI updates to the UI model's isolation. Bound queries and uploads. Distinguish SDK retry behavior from your own retries to avoid duplicate writes. Stable document IDs or transactions can support idempotent workflows; assess the actual mutation semantics. Pause listeners when no longer needed and reconcile on foreground—no continuous-background promise.

Use exact callback routing for native providers. Follow the current email-link documentation rather than resurrecting legacy Dynamic Links recipes. Plan schema evolution for old clients, security-rule deployment order, index creation and local-cache/account transitions. Read
authentication
 and
privacy
 for deletion and App Review considerations.

Testing and production gate

Inject repositories for offline Swift tests. Separately run Auth/Firestore/Database/Storage emulators where supported, with explicit emulator endpoints and synthetic users. Ensure emulator/debug App Check configuration cannot leak into production. Test rule denial, session revocation, offline writes, conflict recovery, upload cancellation, account deletion and real APNs behavior. No Firebase SDK or emulator/live-service execution is claimed by this documentation.

Primary sources:
Apple setup
,
Apple auth
,
Google auth
,
password
,
anonymous
,
offline Firestore
,
Rules
,
App Check
.

---

# GraphQL and Apollo iOS
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-graphql.html

← Back to all frameworks & guides
All articles →Backend · Reference guideGraphQL and Apollo iOSRepository guidance for GraphQL and Apollo iOS. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Installation and schema boundary
Correctness beyond HTTP 200
Lifecycle, testing and migration

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Pin schema and generator
↓2
Run typed operation
↓3
Handle partial errors
↓4
Reconcile normalized cache

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
GraphQL and Apollo iOSBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
GraphQL fits a typed schema with related resources and clients needing different selections. It does not remove backend authorization, query-cost controls or network failures. Use REST when a small stable endpoint surface is simpler.

Installation and schema boundary

Add the official
https://github.com/apollographql/apollo-ios
 package and select the products needed by the pinned release. Match the code generation tool to that release. Keep a reviewed schema snapshot and operation files; generate types in CI without calling production. Package product/module names and subscription transport APIs vary by major version: compile your lockfile, not an unversioned tutorial.

Queries read data; mutations express writes; subscriptions stream changes. Put operations behind a repository returning domain models, so generated types do not spread through SwiftUI. Inject authentication at the network transport boundary; refresh once and recreate/re-authenticate subscriptions when identity changes.

Correctness beyond HTTP 200

A response can contain both data and GraphQL errors. Define which partial data is safe to show and distinguish transport errors from field errors. Normalize cache identity consistently; account-scope or clear caches on logout. A normalized cache is not an offline mutation queue. Optimistic updates need reconciliation and rollback after server rejection.

Use cursor pagination and server-provided pageInfo, not an unbounded collection query. Bound selection depth and payloads on the server. Store large files using an authorized upload service or signed URL contract instead of base64 blobs in arbitrary mutations. Realtime subscriptions still require deduplication and a catch-up query after disconnection.

Lifecycle, testing and migration

Do not keep subscriptions alive indefinitely in the background; iOS may suspend the app. One actor/coordinator owns subscription cancellation, retry and state. Use bounded backoff, honour authorization failures and avoid retrying mutations without idempotency. Validate deep links before selecting an entity, then re-authorize the fetch.

Use generated mocks or injected repositories for previews and offline tests. Test partial responses, missing nullable fields, cursor termination, optimistic rollback, account changes and schema compatibility. Verify code generation determinism and compilation with the pinned SDK; no Apollo SDK execution is claimed by the dependency-free sample.

Follow
authentication
,
security
 and
privacy
 for token storage, account deletion and analytics. Primary source:
Apollo iOS documentation
.

---

# Backend services for Swift apps
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-overview.html

← Back to all frameworks & guides
All articles →Backend · Reference guideBackend services for Swift appsRepository guidance for Backend services for Swift apps. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Start with a boundary
Verification contract
Review output and limits
Production gate

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Identify data ownership
↓2
Choose service boundary
↓3
Validate identity and policies
↓4
Record recovery evidence

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Backend services for Swift appsBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Choose a backend around the app's data ownership, identity and recovery requirements. This library connects iOS lifecycle and concurrency decisions to concrete service configuration; it is not a hosted backend or a security certification.

Start with a boundary

SwiftUI view → UI-isolated feature model → injected repository → SDK or HTTP transport → server authorization. Keep SDK sessions and caches behind this boundary. A preview receives an in-memory repository and never creates a live service. An actor owns shared mutable pagination, refresh or reconnect state; do not put the entire networking layer on the main actor.

Choose a backend
 and
map your app architecture
.
Establish
authentication and callback ownership
, then
authorization and secret boundaries
.
Read the specific integration:
Supabase
,
Firebase
,
CloudKit
,
AWS Amplify
,
Appwrite
,
REST
,
GraphQL
 or
WebSockets
.
Exercise
privacy and account lifecycle
 using synthetic identities.
Run
review_backend_integration
 with the project path. Treat its file/line evidence as a review starting point, not a remote configuration audit.

Verification contract

The source snapshot is dated
2026-09-21
. Provider API documentation was reviewed; that does not mean every SDK version was compiled, every OAuth provider was configured, or any live service was tested.
Source record
 records this distinction. Resolve and pin your chosen SDK, retain Package.resolved, compile for your deployment target, and then test the specific provider configuration on a device.

BackendPatterns
 contains dependency-free Swift examples and offline tests. Provider auth snippets in these guides need the named SDK and configured service; they are not complete applications. Use environment-injected client identifiers. Never put a server key in the app, a preview or a prompt.

Review output and limits

The new read-only tool returns services, matched imports/dependencies/configurations, auth and data-use evidence, risks with severity/confidence, and contextual review questions. It reads bounded local text only; it neither calls providers nor writes fixes. It omits credential values. Paths remain developer-local information.

Concrete checks cover privileged literal credentials, sensitive UserDefaults writes, direct token logging, discarded raw HTTP responses, immediate reconnect loops, explicit RLS disabling and unconditional Firebase allow rules. A public Firebase collection can be intentional; the finding asks for policy review, not an automatic rewrite. Missing RLS files, missing Apple login, missing deletion or absence of a cancellation keyword do not prove a defect.

Use existing SwiftUI/performance/security reviewers for body-side effects, force unwraps and error handling. Review pagination, endpoint environments, relaunch restoration and background assumptions with feature context. The scanner cannot infer custom wrappers, deployed policies, entitlements, SDK persistence or an app's eligibility for App Review exceptions.

Production gate

Record the exact SDK/toolchain, environment and test identity. Verify cross-user denial, expired/revoked sessions, sign-out cache clearing, offline recovery, bounded paging, upload cancellation and deletion of dependent records/files. Capture safe event IDs and outcomes, never tokens, callback URLs or user payloads. Do not label local tests as proof that production is secure.

---

# Backend privacy and account lifecycle
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-privacy.html

← Back to all frameworks & guides
All articles →Backend · Reference guideBackend privacy and account lifecycleRepository guidance for Backend privacy and account lifecycle. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Service-specific inventory
Apple policy review
Deletion workflow
Verification record

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Inventory outgoing data
↓2
Classify collection and tracking
↓3
Implement deletion lifecycle
↓4
Verify actual SDK behavior

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Backend privacy and account lifecycleBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Map data before selecting SDKs: what leaves the device, why, who receives it, how long it persists and how deletion propagates. A backend service name does not determine an app's App Privacy answers. Audit the actual product configuration, optional SDKs and server behavior.

Service-specific inventory

Integration

Data and operational questions

Supabase

Auth identifiers, Postgres rows, Storage files, function logs and realtime payloads; delete dependent objects and review log retention

Firebase

Separate Auth/data/storage from optional Analytics, Crashlytics, FCM and Remote Config; review breadcrumbs, device tokens and consent configuration

CloudKit

Private/public/shared scope, synchronized records/assets and participant visibility; inspect local copies and account changes

Amplify

Cognito attributes, AppSync data, S3 objects, Lambda logs and optional push/analytics; deletion spans services

Appwrite

Identity, database/files/functions and realtime; managed/self-hosted retention and backup policy differ

REST / GraphQL

Request payloads, error bodies, access logs, identifiers and resolver/service telemetry; the custom server remains responsible

WebSockets

Message contents, subscription metadata, connection logs and retention; avoid token-bearing URLs

Apple policy review

Use Apple's current App Privacy guidance to classify data collection, linkage and tracking. Advertising identifiers and tracking are separate decisions; do not request ATT merely because any backend exists. Review SDK privacy manifests and required-reason API use in the actual built dependencies. A manifest does not replace accurate App Privacy disclosures.

App Review 4.8 has conditions and exceptions for apps using third-party/social login. Assess the specific app against the policy rather than asserting every social login always mandates the same implementation. When the app supports account creation, review Apple's in-app account-deletion requirements and applicable exceptions. Signing out, disabling a local cache or opening a support email is not automatically a compliant deletion flow.

For user-generated content, evaluate moderation, reporting and blocking obligations under the applicable guideline. Public chat and a private personal notebook have different risks. Don't use one generic backend checklist as legal approval.

Deletion workflow

Reauthenticate as needed → submit an authorized deletion operation → stop listeners and pending writes → remove or anonymize dependent records/files according to the disclosed policy → revoke credentials → clear local identity data → present the actual outcome. Explain legally required retention and backup handling. A server failure must not be reported as successful deletion.

Verification record

Use synthetic identities to test export/deletion, telemetry redaction, account switching and sync cleanup. Inspect network destinations and crash SDK custom fields. Record which configurations were inspected versus exercised on a device; this library does not certify compliance.

Primary sources:
App Review Guidelines
,
App Privacy details
,
account deletion
,
privacy manifests
.

---

# REST clients with URLSession
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-rest.html

← Back to all frameworks & guides
All articles →Backend · Reference guideREST clients with URLSessionRepository guidance for REST clients with URLSession. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Request to domain value
Retry and cancellation contract
Paging, cache and files
Security, migration and tests

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Construct typed request
↓2
Validate HTTP response
↓3
Decode domain value
↓4
Bound retry and cancellation

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
REST clients with URLSessionBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Use REST when a stable HTTP contract or existing service fits the app. Do not add GraphQL or a vendor SDK merely to avoid writing status validation. URLSession is part of Foundation; no external SPM dependency is required. Inject the transport and base URL at the composition root; keep endpoint paths and response types explicit.

Request to domain value

Construct URLs with URLComponents and URLQueryItem. Set the HTTP method, content type, timeout and acceptable payload size deliberately. Obtain bearer credentials from a session owner immediately before dispatch. A request interceptor should add safe headers, not log the complete request or independently refresh every failing request.

Retain HTTPURLResponse and validate the endpoint's success statuses before decoding Codable. Handle 204 without decoding an empty JSON object. Distinguish transport, cancellation, HTTP status, API error and decoding errors; a JSON body alone is not success. Server error payloads are untrusted and may contain personal information. Translate them to a bounded domain error rather than displaying or logging them wholesale.

BackendPatterns
 supplies an injected transport, validated response and bounded GET retries. It deliberately does not implement token refresh, multipart encoding or a background transfer service.

Retry and cancellation contract

Retry only operations whose semantics permit it: GET/HEAD are normally safe; writes need a server-enforced idempotency key and documented replay behavior. Bound attempts and total deadline. Respect Retry-After and rate limiting; use exponential backoff plus jitter in a real transport. Cancellation must escape immediately, including during the delay. Do not retry validation failures, authorization denial or arbitrary decoding errors. A 401 can trigger one coordinated refresh; a 403 is not automatically an expired session.

A feature
.task
 should cancel its request when no longer needed. A long-lived sync owner may outlive one view but must still expose cancellation and account-switch handling. URLSession's async APIs do not grant unlimited background execution.

Paging, cache and files

Use server cursors with stable ordering, preserve the next cursor as opaque data and deduplicate by stable record ID. Avoid fetching all rows to paginate locally. Bind cache entries and ETags to identity and request representation; handle 304 through the existing cached body. Never reuse private cached content after logout.

For multipart uploads follow the server contract for boundaries and disposition fields; use a file-backed request for large payloads. Validate file size/type server-side and clean up partial uploads. Prefer download tasks for large responses rather than accumulating Data. Background URLSession transfers need an app delegate completion handoff and supported task configuration; the server must tolerate resumption and duplicate completion delivery.

Security, migration and tests

Use HTTPS with ATS intact. Certificate pinning has rotation and outage costs; adopt it only with a reviewed recovery plan, not a blanket disable-trust workaround. Keep production/staging endpoints in explicit configuration. Test URLProtocol or an injected transport for status, malformed JSON, timeout, cancellation, 429, retry limits, pagination and refresh races. Contract tests should include backward-compatible server schema evolution and unknown fields. Run live integration checks separately from offline fixtures.

Review data disclosure, deletion and retention in
privacy
. Primary sources:
URLSession
,
URLProtocol
,
background downloads
. Extend the existing
networking guide
 rather than creating a second global networking manager.

---

# Backend authorization and client secrets
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-security.html

← Back to all frameworks & guides
All articles →Backend · Reference guideBackend authorization and client secretsRepository guidance for Backend authorization and client secrets. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Keys are not all equivalent
Server responsibilities
Tool evidence
Safe observability

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Classify client and server keys
↓2
Enforce object authorization
↓3
Test cross-user denial
↓4
Rotate and redact

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Backend authorization and client secretsBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
An iOS binary is a public client. Bundle files, strings, configuration and network traffic on a developer-controlled device can be inspected. Obfuscating a server key does not create a trusted boundary.

Keys are not all equivalent

Supabase
sb_publishable_
 keys and legacy anon-role keys identify a public client. They depend on correctly configured grants and RLS.
sb_secret_
 and legacy service-role keys are privileged server credentials and must never ship. A decoded JWT role is only classification evidence, not signature verification. Firebase's GoogleService-Info.plist contains client identifiers; it is not a service-account private key. Appwrite server API keys, AWS secret access keys and OAuth client secrets belong on trusted infrastructure.

Keep environments separate. Commit reviewed client configuration only when intended; use private secret management for server values. A gitignored file can still enter an app bundle. If a real key leaked, revoke/rotate it and audit access; deleting a line does not erase history or already published binaries.

Server responsibilities

Authorize each operation against the authenticated identity, tenant and object. Enforce ownership during insert and update, not only reads. Restrict storage paths, signed URL lifetime, functions and database RPC privileges. Validate payload limits and rate limits server-side. Client checks improve UX but cannot enforce access control.

For Supabase test RLS with an unauthenticated client and two different users using client-safe keys. For Firebase test Rules with the emulator and verify production deployment; Admin SDK operations bypass the client's rules boundary. For Amplify review AppSync auth modes and IAM/Cognito claims. For CloudKit review database scope, record type permissions and sharing. For Appwrite inspect resource permissions and server functions. Auth success alone establishes none of these policies.

Tool evidence

review_backend_integration
 flags explicit privileged literals and returns only a rule, relative file, line, risk and repair advice. It also checks sensitive UserDefaults writes, direct credential logging, raw URLSession response discards, tight reconnect loops, explicit SQL RLS disabling and unconditional Firebase allows. It reads bounded source/config text, skips dependency/build/test folders and symlinks, and does not validate deployed policy state. Quoted SQL identifiers and dollar-quoted procedure bodies are deliberately outside its top-level policy checks.

An allow-true rule may intentionally expose public content. A migration can temporarily disable RLS. Review target scope and deployment order; do not automatically rewrite either. No local policy file is not evidence that remote RLS is absent. No
@MainActor
 keyword is not proof of unsafe concurrency. Use compiler and existing reviewers for their respective contracts.

Safe observability

Record event type, duration, status class, retry count and a non-sensitive correlation identifier. Never record passwords, OTPs, tokens, raw callback URLs, authorization headers or entire request/response objects. Crash reporters can capture breadcrumbs and custom keys; redact there too. Reports contain local filenames, so review them before sharing.

Test redaction with sentinel secrets and verify cross-user denial, upload type/size limits, revoked identities, replayed callbacks and rate limiting. See
privacy
,
RLS
,
Firebase Rules
, and
Apple Keychain
.

---

# Supabase for Swift apps
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-supabase.html

← Back to all frameworks & guides
All articles →Backend · Reference guideSupabase for Swift appsRepository guidance for Supabase for Swift apps. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Install and configure
Native Apple and Google examples
Social provider verification
MFA and passkeys
Sessions, metadata and deletion
Postgres, RPC and RLS
Storage, realtime and functions
Offline and production checklist
Primary references

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Configure public client
↓2
Authenticate and restore
↓3
Enforce RLS and object policies
↓4
Reconcile data and revoke

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Supabase for Swift appsBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Supabase fits relational apps needing Postgres, flexible authentication, storage and realtime. It is a poor shortcut if the team cannot maintain schemas, grants and Row Level Security. The Swift client is not an offline database or a trusted server.

Install and configure

Add
https://github.com/supabase/supabase-swift
 through SPM, choose the Supabase product and pin the resolved version. Inject a SupabaseClient at the composition root using a validated project URL and publishable key from client configuration. Never use a secret or service-role key. Separate local, staging and production projects and redirect allowlists. These snippets use the documented Swift API surface; external SDK compilation and live provider sign-in are not claimed here.

import Foundation
import Supabase

// URL and client-safe key are supplied by validated environment configuration.
func makeClient(url: URL, publishableKey: String) -> SupabaseClient {
    SupabaseClient(supabaseURL: url, supabaseKey: publishableKey)
}

func signIn(client: SupabaseClient, email: String, password: String) async throws {
    _ = try await client.auth.signIn(email: email, password: password)
}

func requestLink(client: SupabaseClient, email: String, redirect: URL) async throws {
    try await client.auth.signInWithOTP(email: email, redirectTo: redirect)
}

func requestPhoneOTP(client: SupabaseClient, phone: String) async throws {
    try await client.auth.signInWithOTP(phone: phone)
}

func verifyEmailOTP(client: SupabaseClient, email: String, token: String) async throws {
    _ = try await client.auth.verifyOTP(email: email, token: token, type: .email)
}

func startGuest(client: SupabaseClient) async throws {
    _ = try await client.auth.signInAnonymously()
}

Configure email templates to distinguish a clickable magic link from a user-entered OTP. SMS requires an enabled provider, limits and abuse controls; phone OTP is not free proof of identity. Keep passwords and OTPs out of logs and analytics. Anonymous users still have identities and policies; plan upgrade/linking and cleanup instead of treating guest data as globally public.

Native Apple and Google examples

Obtain the ID token through the native provider SDK, not by decoding an arbitrary callback. For Apple generate a fresh cryptographic nonce, supply its SHA-256 digest to the Apple request, then pass the original nonce to Supabase. For Google obtain a current ID token with the configured iOS/server client IDs; do not substitute an access token for it.

func exchangeApple(client: SupabaseClient, idToken: String, rawNonce: String) async throws {
    _ = try await client.auth.signInWithIdToken(
        credentials: .init(provider: .apple, idToken: idToken, nonce: rawNonce)
    )
}

func exchangeGoogle(client: SupabaseClient, idToken: String) async throws {
    _ = try await client.auth.signInWithIdToken(
        credentials: .init(provider: .google, idToken: idToken)
    )
}

func browserGitHub(client: SupabaseClient, redirect: URL) async throws {
    _ = try await client.auth.signInWithOAuth(provider: .github, redirectTo: redirect)
}

func receiveAuthCallback(client: SupabaseClient, url: URL) async throws {
    // Caller has matched the exact registered route; SDK validates the auth exchange.
    _ = try await client.auth.session(from: url)
}

func observeSession(client: SupabaseClient) async {
    for await (_, session) in client.auth.authStateChanges {
        if Task.isCancelled { break }
        // Send a domain signed-in/signed-out state to the UI owner; never print session.
        _ = session?.user.id
    }
}

The coordinator owns and cancels the observer task on teardown. A production implementation updates state and clears identity-scoped caches; the snippet only illustrates safe API wiring. See
callback validation
 before calling
session(from:)
 from
onOpenURL
.

Social provider verification

Official Supabase social-login documentation lists the following providers as of the source snapshot. Server support is not a claim that every provider supports native Swift ID-token exchange.

Provider

iOS approach

Configuration check

Apple

Native ID token + nonce, or documented browser flow

Sign in with Apple identifiers, nonce, redirect and provider secret lifecycle

Google

Native Google ID token or browser OAuth

iOS/server client IDs and callback setup

GitHub

Browser OAuth

Provider app callback and granted scopes

Facebook

Browser OAuth unless using a separately verified native integration

App review, scopes and redirect

Microsoft / Azure

Browser OAuth

Tenant policy and identity scopes

LinkedIn

Supported OIDC provider path

Use the current OIDC configuration, not an obsolete provider slug

Discord

Browser OAuth

Redirect and least scopes

Slack

Supported OIDC provider path

Workspace/org restrictions and current OIDC configuration

Spotify

Browser OAuth

Scope and provider access restrictions

Twitch

Browser OAuth

Registered callback and scopes

X / Twitter

Browser OAuth

Provider app access and enabled OAuth flow

Only use enum cases that exist in the pinned Swift SDK. Do not translate this table into guessed case names. Supabase session refresh does not automatically refresh an external provider's access token; if the app calls that provider's APIs, implement the documented separate token lifecycle server-side where necessary.

MFA and passkeys

MFA is an enrollment → challenge → verify flow, with assurance level checked before sensitive server operations. Test recovery, unenrollment and expired challenges; do not invent recovery codes or accept a client-only MFA flag.

The official passkeys guide currently labels passkeys
experimental
, requires opt-in, and lists
supabase-swift 2.48.0 or later
. Registration requires an existing confirmed, non-anonymous user. RP identity and associated platform configuration are security-critical. The documented high-level operations include sign-in and registration, but no live Swift passkey ceremony was verified in this change. Pin a supported SDK and follow its current platform recipe before enabling production passkeys. Do not describe WebAuthn support as generally stable or interchangeable with password authentication.

Sessions, metadata and deletion

Let the SDK coordinate access/refresh tokens and inspect the pinned version's persistence policy. Use an appropriate Keychain-backed storage adapter when supplying sensitive persistence. Handle initial restoration, refresh, revoked sessions and signed-out events.
try await client.auth.signOut()
 ends the selected session scope; clear local identity data and listeners as well. Choose global/local scope deliberately according to the SDK contract.

User-editable metadata is not an authorization claim. Derive tenant and role access from trusted server policy. Account deletion must invoke an authenticated server function that verifies the caller and performs privileged deletion plus related row/file cleanup. Never embed the admin API key to call deletion from the app. Reauthenticate where warranted, cancel pending uploads and explain retention exceptions.

Postgres, RPC and RLS

Use typed Codable DTOs and explicit selections. Filter and paginate on the server with stable ordering; do not load a whole table to filter in Swift. Keep domain models independent from database column naming. Test nullable columns, timestamps and incompatible migrations.

Enable and test RLS for exposed tables and review grants. Policies need ownership constraints on insert and update as well as select. Test two users and unauthenticated clients with client-safe keys; service-role tests can accidentally bypass the boundary you intended to measure. RPC functions need reviewed execution grants and security-definer/search-path behavior. SQL constraints protect invariants even when clients are stale.

Storage, realtime and functions

Scope Storage bucket/object policies by identity and path; enforce file size/type and clean orphaned objects. Treat signed URLs as temporary bearer access and omit them from logs. Upload progress/cancellation belongs to a repository, not a SwiftUI body.

Realtime channels need cancellation, reconnect/catch-up and duplicate handling. Event delivery does not replace initial queries or durable local state. Auth changes require channel reauthorization or recreation. Edge Functions verify identity and authorize operations independently; a client publishable key alone is not user authentication. Keep outbound provider secrets and privileged database operations on the function/server.

Offline and production checklist

Use a local store and explicit outbox if offline edits are required. Define conflict resolution and idempotency keys; retry only safe operations with bounded backoff. Foreground listeners cannot run continuously while iOS is suspended. Deep links can arrive before restoration: route once, then reconcile session state.

Use the Supabase CLI/local stack for synthetic policy and migration tests, and an injected repository for offline Swift tests. Apply reviewed migrations to staging before production. Verify auth redirects on a device, RLS denials, session revocation, paging termination, upload cleanup, deletion and schema rollback strategy. Record
App Privacy and telemetry choices
; third-party social login needs an App Review 4.8 assessment.

Primary references

Swift auth
,
OAuth
,
ID token exchange
,
OTP
,
auth events
,
social providers
,
passkeys
,
MFA
,
RLS
,
Storage policies
,
function auth
,
client keys
.

---

# Realtime connections on iOS
https://nagarjuna2997.github.io/ios-agent-skill/guides/backend-websockets.html

← Back to all frameworks & guides
All articles →Backend · Reference guideRealtime connections on iOSRepository guidance for Realtime connections on iOS. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Connection owner
Recovery protocol
App lifecycle
Verification

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Authenticate connection
↓2
Receive and deduplicate
↓3
Back off on disconnect
↓4
Catch up and resubscribe

02 / ArchitectureResponsibility boundariesBoundary 1
Swift feature modelBoundary 2
Realtime connections on iOSBoundary 3
Authorized server and local stateConnected responsibilities, not a required class hierarchy or an execution trace.
Use a WebSocket for foreground bidirectional events when polling or push notification delivery is insufficient. A socket is not a durable offline sync engine and cannot guarantee continuous background execution on iOS.

Connection owner

URLSessionWebSocketTask is provided by Foundation; no external package is required. Wrap connect, receive, send, ping and close in one injected connection interface. Model disconnected, connecting, connected, backing off and stopped states. One owner serializes transitions so two views do not create duplicate subscriptions. Separate transport connectivity from authenticated readiness.

A receive loop handles one message at a time, bounds message sizes and validates event schemas. A ping timeout should transition to reconnect once, not start another independent loop. On cancellation, close the task and stop timers. Preserve cancellation errors rather than swallowing them.

Recovery protocol

Use bounded exponential backoff with jitter and a retry budget. Treat authentication rejection differently from transient loss. Refresh credentials through the shared session owner, then resubscribe deliberately. Supply a last-seen cursor or perform a catch-up fetch. Dedupe by stable event ID and resolve ordering/version conflicts; receipt order across reconnects is not necessarily commit order.

Persist accepted domain state in a local store. An outgoing queue needs durable IDs, acknowledgement and replay rules. Never resend a purchase or other non-idempotent action merely because an acknowledgement was lost. For uploads use authorized HTTP/file transfer, not an unbounded socket payload.

App lifecycle

Stop or suspend foreground subscriptions when appropriate and revalidate on activation. APNs can signal that a fetch is needed, but silent pushes are not guaranteed delivery. Deep links may open a conversation before reconnect completes: show cached/loading states and authorize the target. Restrict heartbeat frequency to avoid battery and mobile-data waste.

Verification

Use a fake clock and scripted transport to exercise duplicate events, out-of-order versions, expired credentials, delayed pong, repeated disconnect, cancellation during backoff and resubscription exactly once. Test foreground/background and network transitions on a physical device separately. Log event classes and counters only, not token-bearing connection URLs or message bodies. Follow
privacy
 for messages and retention.

The analyzer flags only an immediate unbounded reconnect shape; it cannot prove arbitrary wrappers have correct cancellation or ordering. Primary source:
URLSessionWebSocketTask
.

---

# App Store Submission Checklist
https://nagarjuna2997.github.io/ios-agent-skill/guides/checklists-app-store-submission.html

← Back to all frameworks & guides
All articles →Authentication, Security, and Privacy · Reference guideApp Store Submission ChecklistRepository guidance for Privacy Manifest. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

App Metadata
Screenshots and Previews
App Review Guidelines Compliance

1.0 Safety
2.0 Performance
3.0 Business
4.0 Design
5.0 Legal

Privacy
App Transport Security
Required Device Capabilities
Launch Screen and App Icons
Entitlements and Capabilities
Build Configuration
TestFlight Beta Testing
Common Rejection Reasons and Fixes
Xcode Archive and Upload
Post-Submission

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Inventory app capabilities
↓2
Check privacy and signing
↓3
Exercise release build
↓4
Submit reviewed archive

02 / ArchitectureResponsibility boundariesBoundary 1
Release artifactBoundary 2
Privacy declarationsBoundary 3
Review evidenceConnected responsibilities, not a required class hierarchy or an execution trace.
A comprehensive checklist for submitting an iOS app. Work through each section before uploading your archive to App Store Connect.

App Metadata

[ ] App name finalized (30 character limit, no keyword stuffing)
[ ] Subtitle written (30 character limit, descriptive and compelling)
[ ] App description written (up to 4000 characters, most important info first)
[ ] Promotional text set (170 characters, can be updated without a new build)
[ ] Keywords optimized (100 character budget, comma-separated, no spaces after commas)
[ ] Primary and secondary categories selected
[ ] Support URL provided (must be a working webpage)
[ ] Marketing URL provided (optional but recommended)
[ ] Copyright field filled (e.g., "2026 Your Company Name")
[ ] Version number follows semantic versioning (e.g., 1.0.0)

Screenshots and Previews

[ ] Screenshots provided for 6.7" display (iPhone 15 Pro Max / 16 Pro Max -- 1290 x 2796)
[ ] Screenshots provided for 6.5" display (iPhone 11 Pro Max -- 1242 x 2688) if targeting older devices
[ ] Screenshots provided for 5.5" display (iPhone 8 Plus -- 1242 x 2208) if supporting iPhone SE
[ ] iPad Pro 12.9" screenshots (2048 x 2732) if universal app
[ ] iPad Pro 13" (M4) screenshots (2064 x 2752) if targeting latest iPads
[ ] Minimum 3 screenshots per device size (maximum 10)
[ ] Screenshots show actual app UI (not misleading)
[ ] App preview videos uploaded (optional, 15-30 seconds, no watermarks)
[ ] All screenshots and previews localized for each supported language

App Review Guidelines Compliance

1.0 Safety

[ ] No objectionable content without proper age gating
[ ] User-generated content has reporting and blocking mechanisms
[ ] No realistic violence in icons or screenshots for apps aimed at children

2.0 Performance

[ ] App is complete and functional (no beta, demo, or trial labels)
[ ] No hidden or undocumented features
[ ] App does not download additional executable code after install
[ ] App works without requiring additional hardware to review

3.0 Business

[ ] In-app purchases use StoreKit (not third-party payment for digital goods)
[ ] Subscriptions include clear pricing and terms
[ ] Subscription offers restore previous purchases
[ ] Free trials clearly state what happens when trial ends
[ ] No bait-and-switch pricing

4.0 Design

[ ] App uses standard system UI or well-designed custom UI
[ ] App functions on all supported device sizes (no black bars)
[ ] No use of private APIs
[ ] App does not mimic native iOS system UI in misleading ways
[ ] Extensions (widgets, keyboards, etc.) include sufficient standalone functionality

5.0 Legal

[ ] App complies with all local laws in each territory
[ ] Developer Program License Agreement followed
[ ] No use of copyrighted material without permission
[ ] GDPR and CCPA compliance if targeting EU/California users

Privacy

[ ] Privacy policy URL provided (required for all apps)
[ ] Privacy policy is accessible and clearly written
[ ] App Privacy labels configured in App Store Connect (Data Types questionnaire)
[ ] Each data type categorized correctly (collected vs. tracked vs. linked)
[ ] App Tracking Transparency (ATT) prompt implemented if tracking users across apps
[ ] ATT prompt shown before any tracking begins
[ ]
NSUserTrackingUsageDescription
 added to Info.plist if using ATT
[ ] Purpose strings (usage descriptions) provided for all permission requests:
[ ]
NSCameraUsageDescription
[ ]
NSPhotoLibraryUsageDescription
[ ]
NSLocationWhenInUseUsageDescription
[ ]
NSLocationAlwaysAndWhenInUseUsageDescription
 (if applicable)
[ ]
NSMicrophoneUsageDescription
[ ]
NSContactsUsageDescription
[ ]
NSCalendarsUsageDescription
[ ]
NSBluetoothAlwaysUsageDescription
[ ]
NSFaceIDUsageDescription
[ ]
NSHealthShareUsageDescription
 /
NSHealthUpdateUsageDescription
[ ] Each purpose string clearly explains why the permission is needed (in user-friendly language)

App Transport Security

[ ] ATS enabled (default in modern Xcode projects)
[ ] No blanket
NSAllowsArbitraryLoads = YES
 in production
[ ] Any ATS exceptions are justified and documented
[ ] All API endpoints use HTTPS with TLS 1.2+
[ ] Third-party SDKs do not require ATS exceptions (or exceptions are scoped narrowly)

Required Device Capabilities

[ ]
UIRequiredDeviceCapabilities
 in Info.plist lists only truly required capabilities
[ ] Do not require capabilities unnecessarily (this restricts compatible devices)
[ ] Common capabilities reviewed:
[ ]
armv7
 /
arm64
 -- processor architecture
[ ]
camera-flash
 -- only if core feature needs it
[ ]
gps
 -- only if precise location is essential
[ ]
nfc
 -- only if NFC is core to the app
[ ]
arkit
 -- only if AR is mandatory

Launch Screen and App Icons

[ ] Launch screen configured (storyboard or Info.plist configuration)
[ ] Launch screen matches initial app state (no logos per HIG unless branding)
[ ] App icon provided as a single 1024x1024 asset in the asset catalog
[ ] App icon does not contain alpha channel / transparency
[ ] App icon is not a photograph of an iPhone or iPad
[ ] App icon renders well at small sizes (no fine details lost)
[ ] Alternate app icons configured if supported (optional)

Entitlements and Capabilities

[ ] Only required entitlements are enabled in Signing & Capabilities
[ ] Push Notifications: APNs certificate or key configured, entitlement enabled
[ ] Sign In with Apple: entitlement enabled if using Apple ID sign-in
[ ] Associated Domains: configured for universal links / web credentials
[ ] App Groups: configured if sharing data between app and extensions
[ ] Background Modes: only required modes selected
[ ] HealthKit: entitlement and usage descriptions set
[ ] iCloud / CloudKit: container configured if using cloud sync
[ ] In-App Purchase: capability enabled, products configured in App Store Connect
[ ] Provisioning profile matches entitlements (no mismatch errors)

Build Configuration

[ ] Deployment target set appropriately (check analytics for user base)
[ ] Build number incremented from last upload
[ ] Release build configuration used (not Debug)
[ ] Bitcode setting matches project requirements (deprecated in Xcode 16+)
[ ] All architectures included (arm64 required)
[ ] dSYM files generated for crash reporting
[ ] No compiler warnings in Release build
[ ] No
#if DEBUG
 code leaking into release paths
[ ] All test/staging API URLs replaced with production URLs
[ ] Logging level reduced for production (no verbose console output)

TestFlight Beta Testing

[ ] Internal testing group created (up to 100 testers)
[ ] External testing group created if needed (up to 10,000 testers)
[ ] Beta App Description written
[ ] Beta build uploaded and processed successfully
[ ] Compliance information answered (encryption export regulations)
[ ] If using non-exempt encryption, proper export compliance documentation filed
[ ] Test notes written for each build describing what to test
[ ] At least one full round of beta testing completed
[ ] Critical crash reports from TestFlight addressed
[ ] Beta feedback reviewed and acted upon

Common Rejection Reasons and Fixes

[ ]
Crashes/bugs
: Test every user flow, including edge cases and poor network
[ ]
Broken links
: Verify every URL in the app (support, privacy policy, terms)
[ ]
Placeholder content
: Remove all lorem ipsum, test data, TODO comments visible to users
[ ]
Incomplete information
: App description, screenshots, and metadata must be final
[ ]
Login required but no demo account
: Provide demo credentials in review notes
[ ]
Permissions without features
: Do not request permissions until the feature needs them
[ ]
Third-party sign-in without Sign In with Apple
: If you offer Google/Facebook sign-in, you must also offer Sign In with Apple
[ ]
Subscription issues
: Clearly disclose pricing before paywall; include restore purchases button
[ ]
Minimum functionality
: App must provide lasting value beyond a simple website wrapper
[ ]
Misleading metadata
: Keywords, description, and screenshots must accurately represent the app

Xcode Archive and Upload

[ ] Select "Any iOS Device (arm64)" as build destination
[ ] Product > Archive (builds the release archive)
[ ] Archive appears in Organizer window without errors
[ ] Validate the archive (Organizer > Validate App)
[ ] Resolve any validation warnings or errors
[ ] Distribute App > App Store Connect > Upload
[ ] Upload succeeds without errors
[ ] Build appears in App Store Connect under TestFlight within 15-30 minutes
[ ] Build processing completes (check for processing errors via email)
[ ] Select build in App Store Connect release
[ ] Submit for Review

Post-Submission

[ ] Monitor App Store Connect for review status changes
[ ] Respond to any App Review questions promptly (via Resolution Center)
[ ] Prepare release notes for the version
[ ] Decide release method: manual release, automatic after approval, or phased rollout
[ ] Phased release recommended for major updates (1% > 2% > 5% > 10% > 20% > 50% > 100% over 7 days)
[ ] Monitor crash reports after release via Xcode Organizer or third-party tool
[ ] Monitor App Store reviews and respond to user feedback

---

# Generate real asset catalogs
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-asset-generation.html

← Back to all frameworks & guides
All articles →Design · Reference guideGenerate real asset catalogsRepository guidance for Generate real asset catalogs. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Icons without paid tools
Optional Figma handoff
Evidence and limits
Palette tooling

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Read design tokens
↓2
Generate named assets
↓3
Compile asset catalog
↓4
Inspect appearance variants

02 / ArchitectureResponsibility boundariesBoundary 1
Token documentBoundary 2
Asset generatorBoundary 3
Assets.xcassetsConnected responsibilities, not a required class hierarchy or an execution trace.
The CLI converts a strict JSON token file into a new
Assets.xcassets
. It runs
locally without Figma, a paid service, an AI model or an API key. This is an
implementation of the semantic color guidance in
design tokens
,
not a parser for arbitrary Markdown or Swift examples.

From a source checkout:

npm ci --prefix cli
npm run build --prefix cli
node cli/dist/index.js assets --tokens tokens.json --output App/Assets.xcassets

The output parent directory must exist. An existing catalog is never overwritten;
generate a sibling catalog, review its diff, then merge the intended changes.
The combined package includes this CLI starting with ios-agent-mcp 2.7.0.
Run
npx -y ios-agent-mcp@latest assets --tokens tokens.json --output App/Assets.xcassets

to use it without a source checkout.

{
  "version": 1,
  "colors": {
    "AccentColor": {
      "light": "#2457DB",
      "dark": "#90B4FF",
      "highContrastLight": "#12358F",
      "highContrastDark": "#C7DAFF"
    }
  }
}

Names are ASCII identifiers, unique ignoring case;
AccentColor
 is required.
All four appearances are required. Values are sRGB
#RRGGBB
 or
#RRGGBBAA
.
There is no guessed dark-mode conversion. High contrast variants should be chosen
and contrast-tested against their actual background. The generator does not
certify contrast, layout accessibility or App Review acceptance.

New scaffolds include
App/design-tokens.json
, named
AccentColor
,
Background

and
TextPrimary
 sets. XcodeGen starters set the global accent to
AccentColor
.
Consume named assets with
Color("Background")
 and
Color("TextPrimary")
.

Icons without paid tools

new MyApp --xcodegen
 keeps the three editable SVG layers and also renders their
ordered composition as an opaque RGB 1024×1024 PNG in
AppIcon.appiconset
. Xcode
uses its single-size iOS icon entry. These are placeholder shapes; customize them
before distributing an app. The generator does not add a person's name or branding.

After editing the SVG paths in any text editor or a free vector editor, regenerate:

node cli/dist/index.js assets --tokens App/design-tokens.json \
  --output App/ReviewedAssets.xcassets \
  --icon-layers App/MyApp/IconLayers --icon-background '#2457DB'

The existing layer manifest specifies back-to-front order. Inputs are bounded to
16 square SVGs, each at most 1 MiB. Use paths and shapes; text, linked images,
external references and entities are rejected. Convert text to paths in your
editor. The rasterizer is
resvg-js
; the PNG encoder
is
pngjs
. Their dependency licenses are retained.
No web upload or model call is involved.

A flattened PNG is not a native Liquid Glass icon. For that, import the separate
SVG layers into Apple's free Icon Composer, save the
.icon
 document and validate
it in Xcode. This generator does
not
 fabricate an undocumented
.icon
 bundle.
See
Apple's Icon Composer workflow
.

Optional Figma handoff

Use Figma's own integration to read variables if you already use it. Map semantic
color names and light/dark/high-contrast modes to the JSON fields above. Resolve
aliases to explicit sRGB hex values first. The same JSON can be authored by hand;
Figma and its MCP are optional. There is no maintained Figma parser here.

Evidence and limits

The CLI tests verify schema rejection, appearance slots, alpha conversion,
non-overwrite behavior, ordered raster pixels, RGB output and 1024×1024 dimensions.
On macOS, generated colors and the iOS app-icon set were compiled using
xcrun
actool
 against the installed simulator SDK. This does not verify a native
.icon
,
a full screenshot capture pipeline, symbol availability or visual accessibility.

Catalog format:
Apple named colors

and
appearance variants
.

Palette tooling

Use
palette generation
 to produce a four-appearance token preview compatible with the existing assets CLI. Use
color accessibility
 to interpret measured pairs. Only write or merge a catalog when requested; retain system semantics when custom colors add no value.

---

# Derive a usable seed from brand color evidence
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-brand-color-extraction.html

← Back to all frameworks & guides
All articles →Design · Reference guideDerive a usable seed from brand color evidenceRepository guidance for Derive a usable seed from brand color evidence. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
Local workflow
Limits

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Sample locally
↓2
Remove background noise
↓3
Rank candidates
↓4
Confirm brand intent

02 / ArchitectureResponsibility boundariesBoundary 1
Opaque pixel samplesBoundary 2
Candidate seedBoundary 3
Semantic variantsConnected responsibilities, not a required class hierarchy or an execution trace.
Context

An extracted logo color is a candidate, not proof of an official brand standard. Prefer a user-provided brand seed when available.
generate_color_system
 accepts locally sampled opaque sRGB colors; the MCP adapter also accepts
imagePath
 for a local PNG. It never uploads images or fetches URLs.

Local workflow

Supply an explicit local
imagePath
 (PNG, at most 1024 × 1024 and 4 MiB), or use a trusted local decoder to produce opaque sRGB samples. The PNG adapter skips transparent pixels and rejects color profiles requiring conversion. Exclude known background regions before manual sampling.
Pass a bounded list of colors and positive frequency weights as
imageSamples
.
The generator rejects very light/dark and near-neutral background-like samples, combines identical colors and ranks by frequency, with deterministic tie-breaking.
Inspect the chosen seed and candidates. The report includes black/white text contrast, showing which foreground is unsuitable without adjustment.
Confirm the intended identity with the designer. Regenerate supporting semantic variants and inspect them in real screens.

{"imageSamples":[{"color":"#FFFFFF","weight":900},{"color":"#145AC8","weight":160},{"color":"#E34D7A","weight":30}]}

This heuristic favors chromatic logo colors. A monochrome logo can legitimately produce no extracted seed; provide black/gray explicitly rather than pretending background rejection identifies brand intent. Existing project colors and design tokens can also supply seeds, but neither reveals the designer's semantic roles automatically.

Limits

The existing bundled PNG decoder is reused with size/dimension bounds. JPEG/HEIC/SVG, color-profile conversion, alpha compositing, spatial segmentation, near-color clustering and trademark lookup are unsupported. JPEG noise may create many small buckets; pre-quantize with the local decoder or select an explicit seed. Do not claim this is a full image-understanding system. Sample weights must be finite positive values; inputs are bounded for predictable local operation.

---

# Validate color relationships, not just swatches
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-color-accessibility.html

← Back to all frameworks & guides
All articles →Design · Reference guideValidate color relationships, not just swatchesRepository guidance for Validate color relationships, not just swatches. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
What is measured
What contrast cannot establish
Source and formula

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Identify foreground and background
↓2
Measure opaque sRGB pairs
↓3
Mark unresolved contexts
↓4
Verify in the app

02 / ArchitectureResponsibility boundariesBoundary 1
Luminance mathBoundary 2
Evidence reportBoundary 3
Runtime inspectionConnected responsibilities, not a required class hierarchy or an execution trace.
Context

The generator measures opaque sRGB foreground/background pairs using WCAG relative luminance. Its standard target for text is 4.5:1; the high setting targets 7:1. Focus against the base background is measured at 3:1. These are numerical guidance checks, not a complete native-app accessibility audit.

What is measured

The report includes primary/secondary text on background, text on surface and elevated/grouped surfaces, primary and pressed buttons, secondary buttons, destructive labels, links, selected text, success/warning/error on their containers, focus and disabled controls. Ratios are evaluated before rounding.

Disabled controls are
informational
, because WCAG's minimum text contrast criterion exempts inactive components. Do not convert this observation into a requirement or silently count an inactive control as a failure. Readability may still be a product goal.

The project reviewer only diagnoses an explicit, contiguous
Text(...).foregroundStyle(Color("Name")).background(Color("Name"))
 pair with unambiguous opaque sRGB catalog values. A ratio below 3:1 is a concern even for large text. The static catalog check leaves a conservative 0.05 margin because its inventory normalizes components to eight-bit sRGB. Between 3:1 and 4.5:1 requires font/context evidence that this lexical reviewer does not have. It does not claim coverage of all view compositions.

What contrast cannot establish

Contrast does not establish Dynamic Type support, VoiceOver labels, focus order, usable touch targets, color-vision accessibility, or adequate error recovery. Links need a non-color cue when their surrounding context requires one. Selection, success, warning and destructive states need text, symbols, shape or other cues beyond color alone. Test Differentiate Without Color and Increase Contrast in the running app.

Materials, gradients, opacity, Display P3, HDR, dynamic UIColor providers and inherited modifiers require rendered inspection. A system color cannot be reduced to one universal RGB value. No warning is emitted simply because an app uses Apple semantic colors or legitimate custom branding.

Source and formula

WCAG contrast minimum
 explains text thresholds and inactive-component exceptions. For each sRGB component c, linearize with c/12.92 below 0.04045, otherwise ((c+0.055)/1.055)^2.4. Relative luminance is 0.2126R + 0.7152G + 0.0722B. The ratio is (lighter+0.05)/(darker+0.05).

Apple's
SwiftUI Color
 and
UIColor
 documentation are the primary API sources for named and adaptive colors. WCAG is cited as contrast guidance, not an invented App Store rule.

---

# iOS Color System -- Complete Guide for Stunning SwiftUI UIs
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-color-system.html

← Back to all frameworks & guides
All articles →Design · Reference guideiOS Color System -- Complete Guide for Stunning SwiftUI UIsRepository guidance for iOS Color System -- Complete Guide for Stunning SwiftUI UIs. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Generate and review a project color system
Overview
1. Apple's Semantic Colors

System Grouped Colors
Flat Background Hierarchy
Fill Colors

2. Material Effects

Combining Materials with Borders

3. Dark Mode Support

Automatic Adaptation
Manual Color Scheme Override

4. Color Extension -- Hex Initializer
5. Five Stunning Pre-Built Color Palettes

Palette 1 -- Ocean Blue (Fintech / Productivity)
Palette 2 -- Sunset Warm (Social / Lifestyle)
Palette 3 -- Midnight Dark (Premium / Luxury)
Palette 4 -- Nature Green (Health / Wellness)
Palette 5 -- Violet Dream (Creative / Entertainment)
Using Palettes with Environment-Aware Adaptive Colors

6. Custom Colors via Asset Catalog
7. Gradient Recipes

Linear Gradient
Radial Gradient
Angular (Conic) Gradient
Mesh Gradient (iOS 18+)

8. Ten Stunning Gradient Combinations
9. Vibrancy and Blur Effects
10. Color Accessibility

Contrast Ratios
Color Blind Friendly Design Tips

Quick Reference

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Choose semantic roles
↓2
Assign appearance variants
↓3
Measure contrast
↓4
Inspect real screens

02 / ArchitectureResponsibility boundariesBoundary 1
Semantic rolesBoundary 2
Light and dark palettesBoundary 3
Accessible UIConnected responsibilities, not a required class hierarchy or an execution trace.
Generate and review a project color system

Use
generate_color_system
 for a read-only semantic palette preview, and
review_color_system
 for bounded project evidence. The new engine extends the existing token schema rather than replacing Apple system colors or creating a second asset writer.

Palette generation
: seed inputs, families, harmonies, OKLCH scales and asset handoff.
Dark-mode colors
: independent appearances, OLED and native materials.
Color accessibility
: measured relationships and unsupported contexts.
Brand extraction
: local sample input and identity limitations.

The reviewer returns color locations, duplicate values, approximate families, catalog appearance coverage, explicit contrast concerns and consolidation suggestions. Missing Dark variants are advisories; identical values do not prove identical roles. Dynamic or ambiguous cases are unresolved rather than guessed. Generation never modifies a project; the existing assets CLI remains the explicit mutation step.

Overview

Color is the single most powerful tool for creating emotional impact in iOS applications. This guide covers every aspect of the SwiftUI color system, from Apple's semantic tokens to custom brand palettes, gradients, materials, and accessibility. Examples are guidance; compile the selected code in the target SDK and inspect the result before claiming it is verified.

1. Apple's Semantic Colors

Semantic colors adapt automatically to light mode, dark mode, and increased contrast settings. Always prefer these over hardcoded values.

import SwiftUI

struct SemanticColorsShowcase: View {
    var body: some View {
        VStack(spacing: 16) {
            // Label colors -- automatically adapt to appearance
            Text("Primary Label")
                .foregroundStyle(.primary)
            Text("Secondary Label")
                .foregroundStyle(.secondary)
            Text("Tertiary Label")
                .foregroundStyle(.tertiary)
            Text("Quaternary Label")
                .foregroundStyle(.quaternary)

            Divider()

            // Tint / accent color
            Button("Accent Color Button") {}
                .tint(.accentColor)

            // Semantic intent colors
            HStack(spacing: 12) {
                Circle().fill(.red).frame(width: 32, height: 32)    // Destructive
                Circle().fill(.orange).frame(width: 32, height: 32) // Warning
                Circle().fill(.green).frame(width: 32, height: 32)  // Success
                Circle().fill(.blue).frame(width: 32, height: 32)   // Informational
                Circle().fill(.yellow).frame(width: 32, height: 32) // Caution
            }
        }
        .padding(24)
    }
}

System Grouped Colors

These create the layered card-on-background look native to iOS Settings and many Apple apps.

struct SystemBackgroundsDemo: View {
    var body: some View {
        ZStack {
            Color(.systemGroupedBackground)
                .ignoresSafeArea()

            VStack(spacing: 20) {
                // Primary surface card
                RoundedRectangle(cornerRadius: 16)
                    .fill(Color(.secondarySystemGroupedBackground))
                    .frame(height: 120)
                    .overlay(
                        Text("Secondary Grouped Background")
                            .foregroundStyle(.primary)
                    )

                // Nested surface
                RoundedRectangle(cornerRadius: 16)
                    .fill(Color(.tertiarySystemGroupedBackground))
                    .frame(height: 120)
                    .overlay(
                        Text("Tertiary Grouped Background")
                            .foregroundStyle(.secondary)
                    )
            }
            .padding(20)
        }
    }
}

Flat Background Hierarchy

For non-grouped layouts (full-bleed content rather than inset cards).

struct FlatBackgroundsDemo: View {
    var body: some View {
        ZStack {
            Color(.systemBackground)
                .ignoresSafeArea()

            VStack(spacing: 0) {
                Rectangle()
                    .fill(Color(.secondarySystemBackground))
                    .frame(height: 80)
                    .overlay(Text("Secondary").foregroundStyle(.primary))

                Rectangle()
                    .fill(Color(.tertiarySystemBackground))
                    .frame(height: 80)
                    .overlay(Text("Tertiary").foregroundStyle(.primary))
            }
        }
    }
}

Fill Colors

Use fills for shapes and backgrounds within cells.

struct FillColorsDemo: View {
    var body: some View {
        VStack(spacing: 12) {
            RoundedRectangle(cornerRadius: 10)
                .fill(Color(.systemFill))
                .frame(height: 50)
                .overlay(Text("System Fill"))

            RoundedRectangle(cornerRadius: 10)
                .fill(Color(.secondarySystemFill))
                .frame(height: 50)
                .overlay(Text("Secondary Fill"))

            RoundedRectangle(cornerRadius: 10)
                .fill(Color(.tertiarySystemFill))
                .frame(height: 50)
                .overlay(Text("Tertiary Fill"))

            RoundedRectangle(cornerRadius: 10)
                .fill(Color(.quaternarySystemFill))
                .frame(height: 50)
                .overlay(Text("Quaternary Fill"))
        }
        .padding()
    }
}

2. Material Effects

Materials create frosted-glass blur over underlying content. They are essential for modern iOS design.

struct MaterialShowcase: View {
    var body: some View {
        ZStack {
            // Rich background to show blur effect
            LinearGradient(
                colors: [.purple, .blue, .cyan, .mint],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()

            ScrollView {
                VStack(spacing: 16) {
                    materialCard("Ultra Thin Material", material: .ultraThinMaterial)
                    materialCard("Thin Material", material: .thinMaterial)
                    materialCard("Regular Material", material: .regularMaterial)
                    materialCard("Thick Material", material: .thickMaterial)
                    materialCard("Ultra Thick Material", material: .ultraThickMaterial)
                }
                .padding(20)
            }
        }
    }

    func materialCard(_ title: String, material: Material) -> some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(material)
            .frame(height: 100)
            .overlay(
                Text(title)
                    .font(.headline)
                    .foregroundStyle(.primary)
            )
            .shadow(color: .black.opacity(0.1), radius: 10, y: 5)
    }
}

Combining Materials with Borders

struct GlassCard: View {
    var body: some View {
        ZStack {
            Image(systemName: "photo.artframe")
                .resizable()
                .scaledToFill()
                .frame(width: 400, height: 600)
                .clipped()

            VStack(alignment: .leading, spacing: 8) {
                Text("Glass Card")
                    .font(.title2.weight(.bold))
                Text("Beautiful frosted glass with a subtle border that catches light.")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
            .padding(24)
            .frame(maxWidth: .infinity, alignment: .leading)
            .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24))
            .overlay(
                RoundedRectangle(cornerRadius: 24)
                    .stroke(
                        LinearGradient(
                            colors: [.white.opacity(0.5), .white.opacity(0.1)],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        ),
                        lineWidth: 1
                    )
            )
            .padding(20)
        }
    }
}

3. Dark Mode Support

Automatic Adaptation

SwiftUI semantic colors adapt automatically. For custom colors, use Asset Catalog entries with "Any" and "Dark" appearances.

Manual Color Scheme Override

struct DarkModeDemo: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        VStack(spacing: 20) {
            Text("Current: \(colorScheme == .dark ? "Dark" : "Light")")
                .font(.headline)

            // Adaptive custom color
            RoundedRectangle(cornerRadius: 16)
                .fill(colorScheme == .dark
                    ? Color(hex: "1A1A2E")
                    : Color(hex: "F8F9FA"))
                .frame(height: 100)
                .overlay(
                    Text("Adaptive Card")
                        .foregroundStyle(colorScheme == .dark ? .white : .black)
                )
        }
        .padding()
    }
}

// Force a specific color scheme on any view subtree
struct ForcedSchemeExample: View {
    var body: some View {
        HStack(spacing: 20) {
            CardView(label: "Always Light")
                .environment(\.colorScheme, .light)

            CardView(label: "Always Dark")
                .environment(\.colorScheme, .dark)
        }
        .padding()
    }
}

struct CardView: View {
    let label: String
    var body: some View {
        Text(label)
            .padding()
            .background(Color(.secondarySystemBackground))
            .cornerRadius(12)
    }
}

4. Color Extension -- Hex Initializer

This extension is used throughout this guide and is essential for any custom palette work.

import SwiftUI

// The hex initialiser is NOT redefined here.
//
// It lives in `docs/design/design-tokens.md`, and this file used to declare a
// second `init(hex: String)` with a different body. Copy both into one target
// and the compiler stops you: `invalid redeclaration of 'init(hex:)'`. Worse,
// the old body fell back to **black** on a malformed string, which looks like
// a deliberate colour and ships unnoticed.
//
// Copy the canonical one from design-tokens.md. It accepts both
// `Color(hex: 0x6C63FF)` and `Color(hex: "6C63FF")`, and a malformed literal
// traps in DEBUG and renders magenta in release.

5. Five Stunning Pre-Built Color Palettes

Palette 1 -- Ocean Blue (Fintech / Productivity)

Role

Light Hex

Dark Hex

Text on it (light / dark)

Description

Primary

#0A6EBD

#3DA5F4

white 5.28:1 / black 7.89:1

Trust-inspiring blue

Secondary

#1E88E5

#64B5F6

black 5.71:1 / black 9.48:1

Lighter accent blue

Accent

#00BCD4

#4DD0E1

black 9.14:1 / black 11.43:1

Teal action highlight

Background

#F4F7FA

#0D1117

—

Clean paper / deep night

Surface

#FFFFFF

#161B22

—

Card surface

Text

#1A2233

#E6EDF3

—

High-contrast readable text

Error

#D32F2F

#EF5350

white 4.98:1 / black 6.02:1

Clear danger signal

struct OceanBluePalette {
    static let primary     = Color(hex: "0A6EBD")
    static let secondary   = Color(hex: "1E88E5")
    static let accent      = Color(hex: "00BCD4")
    static let background  = Color(hex: "F4F7FA")
    static let surface     = Color(hex: "FFFFFF")
    static let text        = Color(hex: "1A2233")
    static let error       = Color(hex: "D32F2F")

    static let primaryDark     = Color(hex: "3DA5F4")
    static let secondaryDark   = Color(hex: "64B5F6")
    static let accentDark      = Color(hex: "4DD0E1")
    static let backgroundDark  = Color(hex: "0D1117")
    static let surfaceDark     = Color(hex: "161B22")
    static let textDark        = Color(hex: "E6EDF3")
    static let errorDark       = Color(hex: "EF5350")
}

Body-text pairs
 — Text on Background
14.79:1
 light,
16.02:1
 dark · Text on Surface
15.90:1
 light,
14.64:1
 dark. All four clear the 4.5:1 body-text bar.

Palette 2 -- Sunset Warm (Social / Lifestyle)

Role

Light Hex

Dark Hex

Text on it (light / dark)

Description

Primary

#FF6B35

#FF8A5C

black 7.41:1 / black 9.04:1

Warm energetic orange

Secondary

#F7C948

#FFD966

black 13.4:1 / black 15.37:1

Sunny golden yellow

Accent

#E84393

#FD79A8

black 5.66:1 / black 8.48:1

Playful magenta

Background

#FFF8F0

#1A1215

—

Warm cream / warm dark

Surface

#FFFFFF

#2D1F23

—

Card surface

Text

#2D1810

#F5E6D8

—

Dark warm brown / cream

Error

#C0392B

#E74C3C

white 5.44:1 / black 5.5:1

Red alert

struct SunsetWarmPalette {
    static let primary     = Color(hex: "FF6B35")
    static let secondary   = Color(hex: "F7C948")
    static let accent      = Color(hex: "E84393")
    static let background  = Color(hex: "FFF8F0")
    static let surface     = Color(hex: "FFFFFF")
    static let text        = Color(hex: "2D1810")
    static let error       = Color(hex: "C0392B")

    static let primaryDark     = Color(hex: "FF8A5C")
    static let secondaryDark   = Color(hex: "FFD966")
    static let accentDark      = Color(hex: "FD79A8")
    static let backgroundDark  = Color(hex: "1A1215")
    static let surfaceDark     = Color(hex: "2D1F23")
    static let textDark        = Color(hex: "F5E6D8")
    static let errorDark       = Color(hex: "E74C3C")
}

Body-text pairs
 — Text on Background
15.95:1
 light,
15.06:1
 dark · Text on Surface
16.80:1
 light,
12.91:1
 dark. All four clear the 4.5:1 body-text bar.

Palette 3 -- Midnight Dark (Premium / Luxury)

Role

Light Hex

Dark Hex

Text on it (light / dark)

Description

Primary

#6C63FF

#8B83FF

black 4.87:1 / black 6.79:1

Electric indigo

Secondary

#A78BFA

#C4B5FD

black 7.72:1 / black 11.38:1

Soft lavender

Accent

#F472B6

#F9A8D4

black 7.93:1 / black 11.58:1

Rose gold accent

Background

#F5F3FF

#0B0B1A

—

Faint violet / pure dark

Surface

#FFFFFF

#13132B

—

Card surface

Text

#1E1B4B

#E2E0F0

—

Deep indigo / soft light

Error

#DC2626

#F87171

white 4.83:1 / black 7.59:1

Bright red

struct MidnightDarkPalette {
    static let primary     = Color(hex: "6C63FF")
    static let secondary   = Color(hex: "A78BFA")
    static let accent      = Color(hex: "F472B6")
    static let background  = Color(hex: "F5F3FF")
    static let surface     = Color(hex: "FFFFFF")
    static let text        = Color(hex: "1E1B4B")
    static let error       = Color(hex: "DC2626")

    static let primaryDark     = Color(hex: "8B83FF")
    static let secondaryDark   = Color(hex: "C4B5FD")
    static let accentDark      = Color(hex: "F9A8D4")
    static let backgroundDark  = Color(hex: "0B0B1A")
    static let surfaceDark     = Color(hex: "13132B")
    static let textDark        = Color(hex: "E2E0F0")
    static let errorDark       = Color(hex: "F87171")
}

Body-text pairs
 — Text on Background
14.58:1
 light,
15.00:1
 dark · Text on Surface
15.99:1
 light,
13.98:1
 dark. All four clear the 4.5:1 body-text bar.

Palette 4 -- Nature Green (Health / Wellness)

Role

Light Hex

Dark Hex

Text on it (light / dark)

Description

Primary

#2D9F6F

#4ADE80

black 6.3:1 / black 12.05:1

Fresh healing green

Secondary

#22D3EE

#67E8F9

black 11.62:1 / black 14.49:1

Cool sky cyan

Accent

#F59E0B

#FBBF24

black 9.78:1 / black 12.58:1

Warm honey gold

Background

#F0FDF4

#0A1A12

—

Faint mint / forest dark

Surface

#FFFFFF

#112118

—

Card surface

Text

#14352A

#D1FAE5

—

Deep forest / soft mint

Error

#DC2626

#FB7185

white 4.83:1 / black 7.8:1

Alert red

struct NatureGreenPalette {
    static let primary     = Color(hex: "2D9F6F")
    static let secondary   = Color(hex: "22D3EE")
    static let accent      = Color(hex: "F59E0B")
    static let background  = Color(hex: "F0FDF4")
    static let surface     = Color(hex: "FFFFFF")
    static let text        = Color(hex: "14352A")
    static let error       = Color(hex: "DC2626")

    static let primaryDark     = Color(hex: "4ADE80")
    static let secondaryDark   = Color(hex: "67E8F9")
    static let accentDark      = Color(hex: "FBBF24")
    static let backgroundDark  = Color(hex: "0A1A12")
    static let surfaceDark     = Color(hex: "112118")
    static let textDark        = Color(hex: "D1FAE5")
    static let errorDark       = Color(hex: "FB7185")
}

Body-text pairs
 — Text on Background
12.76:1
 light,
15.83:1
 dark · Text on Surface
13.36:1
 light,
14.76:1
 dark. All four clear the 4.5:1 body-text bar.

Palette 5 -- Violet Dream (Creative / Entertainment)

Role

Light Hex

Dark Hex

Text on it (light / dark)

Description

Primary

#8B5CF6

#A78BFA

black 4.96:1 / black 7.72:1

Vibrant violet

Secondary

#EC4899

#F472B6

black 5.95:1 / black 7.93:1

Hot pink

Accent

#06B6D4

#22D3EE

black 8.65:1 / black 11.62:1

Electric cyan

Background

#FAF5FF

#0F0720

—

Lavender mist / deep purple

Surface

#FFFFFF

#1A0F2E

—

Card surface

Text

#2E1065

#EDE9FE

—

Deep purple / pale lavender

Error

#E11D48

#FB7185

white 4.7:1 / black 7.8:1

Rose red

struct VioletDreamPalette {
    static let primary     = Color(hex: "8B5CF6")
    static let secondary   = Color(hex: "EC4899")
    static let accent      = Color(hex: "06B6D4")
    static let background  = Color(hex: "FAF5FF")
    static let surface     = Color(hex: "FFFFFF")
    static let text        = Color(hex: "2E1065")
    static let error       = Color(hex: "E11D48")

    static let primaryDark     = Color(hex: "A78BFA")
    static let secondaryDark   = Color(hex: "F472B6")
    static let accentDark      = Color(hex: "22D3EE")
    static let backgroundDark  = Color(hex: "0F0720")
    static let surfaceDark     = Color(hex: "1A0F2E")
    static let textDark        = Color(hex: "EDE9FE")
    static let errorDark       = Color(hex: "FB7185")
}

Using Palettes with Environment-Aware Adaptive Colors

struct AdaptiveColor {
    let light: Color
    let dark: Color

    func resolve(for scheme: ColorScheme) -> Color {
        scheme == .dark ? dark : light
    }
}

struct AdaptiveCardExample: View {
    @Environment(\.colorScheme) var scheme

    var primary: Color {
        AdaptiveColor(
            light: OceanBluePalette.primary,
            dark: OceanBluePalette.primaryDark
        ).resolve(for: scheme)
    }

    var body: some View {
        Text("Adaptive Palette Card")
            .font(.headline)
            .foregroundStyle(.white)
            .padding(24)
            .background(primary, in: RoundedRectangle(cornerRadius: 16))
    }
}

6. Custom Colors via Asset Catalog

Step-by-step for Xcode:

Open
Assets.xcassets
.
Click the
+
 button, choose "Color Set".
Name it (e.g.,
BrandPrimary
).
In the Attributes Inspector, set "Appearances" to "Any, Dark".
Set hex values for each appearance.
Use in SwiftUI:

// After defining "BrandPrimary" in the Asset Catalog:
Text("Brand Styled")
    .foregroundStyle(Color("BrandPrimary"))

// Type-safe alternative using an extension:
extension Color {
    static let brandPrimary = Color("BrandPrimary")
    static let brandSecondary = Color("BrandSecondary")
    static let brandAccent = Color("BrandAccent")
}

Text("Type Safe")
    .foregroundStyle(.brandPrimary)

7. Gradient Recipes

Linear Gradient

struct LinearGradientExamples: View {
    var body: some View {
        VStack(spacing: 16) {
            // Horizontal gradient
            RoundedRectangle(cornerRadius: 20)
                .fill(
                    LinearGradient(
                        colors: [Color(hex: "6C63FF"), Color(hex: "E84393")],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                )
                .frame(height: 100)

            // Diagonal gradient with multiple stops
            RoundedRectangle(cornerRadius: 20)
                .fill(
                    LinearGradient(
                        stops: [
                            .init(color: Color(hex: "667EEA"), location: 0),
                            .init(color: Color(hex: "764BA2"), location: 0.5),
                            .init(color: Color(hex: "F093FB"), location: 1),
                        ],
                        startPoint: .topLeading,
                        endPoint: .bottomTrailing
                    )
                )
                .frame(height: 100)
        }
        .padding()
    }
}

Radial Gradient

struct RadialGradientExample: View {
    var body: some View {
        Circle()
            .fill(
                RadialGradient(
                    colors: [
                        Color(hex: "FF6B35"),
                        Color(hex: "F7C948"),
                        Color(hex: "FF6B35").opacity(0.3),
                    ],
                    center: .center,
                    startRadius: 20,
                    endRadius: 150
                )
            )
            .frame(width: 300, height: 300)
            .shadow(color: Color(hex: "FF6B35").opacity(0.4), radius: 30, y: 10)
    }
}

Angular (Conic) Gradient

struct AngularGradientExample: View {
    var body: some View {
        Circle()
            .fill(
                AngularGradient(
                    colors: [
                        Color(hex: "8B5CF6"),
                        Color(hex: "EC4899"),
                        Color(hex: "06B6D4"),
                        Color(hex: "8B5CF6"),
                    ],
                    center: .center
                )
            )
            .frame(width: 200, height: 200)
    }
}

Mesh Gradient (iOS 18+)

@available(iOS 18.0, *)
struct MeshGradientExample: View {
    var body: some View {
        MeshGradient(
            width: 3,
            height: 3,
            points: [
                [0.0, 0.0], [0.5, 0.0], [1.0, 0.0],
                [0.0, 0.5], [0.5, 0.5], [1.0, 0.5],
                [0.0, 1.0], [0.5, 1.0], [1.0, 1.0],
            ],
            colors: [
                Color(hex: "6C63FF"), Color(hex: "8B5CF6"), Color(hex: "EC4899"),
                Color(hex: "3DA5F4"), Color(hex: "A78BFA"), Color(hex: "F472B6"),
                Color(hex: "06B6D4"), Color(hex: "22D3EE"), Color(hex: "F9A8D4"),
            ]
        )
        .frame(height: 400)
        .clipShape(RoundedRectangle(cornerRadius: 24))
        .ignoresSafeArea()
    }
}

8. Ten Stunning Gradient Combinations

Each gradient is named and ready to drop into any project.

enum StunningGradients {
    /// 1. Oceanic Depths -- deep sea to sky
    static let oceanicDepths = LinearGradient(
        colors: [Color(hex: "0A2463"), Color(hex: "1E88E5"), Color(hex: "00BCD4")],
        startPoint: .topLeading, endPoint: .bottomTrailing
    )

    /// 2. Sunset Boulevard -- golden hour warmth
    static let sunsetBoulevard = LinearGradient(
        colors: [Color(hex: "FF6B35"), Color(hex: "F7C948"), Color(hex: "FF8A5C")],
        startPoint: .leading, endPoint: .trailing
    )

    /// 3. Northern Lights -- aurora borealis
    static let northernLights = LinearGradient(
        colors: [Color(hex: "0F2027"), Color(hex: "203A43"), Color(hex: "2C5364"), Color(hex: "4ADE80")],
        startPoint: .top, endPoint: .bottom
    )

    /// 4. Rose Gold -- luxury feminine
    static let roseGold = LinearGradient(
        colors: [Color(hex: "F472B6"), Color(hex: "FBBF24"), Color(hex: "F9A8D4")],
        startPoint: .topLeading, endPoint: .bottomTrailing
    )

    /// 5. Electric Violet -- creative energy
    static let electricViolet = LinearGradient(
        colors: [Color(hex: "8B5CF6"), Color(hex: "6C63FF"), Color(hex: "EC4899")],
        startPoint: .topLeading, endPoint: .bottomTrailing
    )

    /// 6. Midnight City -- dark premium
    static let midnightCity = LinearGradient(
        colors: [Color(hex: "0B0B1A"), Color(hex: "1A1A2E"), Color(hex: "16213E")],
        startPoint: .top, endPoint: .bottom
    )

    /// 7. Fresh Mint -- health and clarity
    static let freshMint = LinearGradient(
        colors: [Color(hex: "2D9F6F"), Color(hex: "22D3EE"), Color(hex: "67E8F9")],
        startPoint: .leading, endPoint: .trailing
    )

    /// 8. Cyber Punk -- bold neon
    static let cyberPunk = LinearGradient(
        colors: [Color(hex: "F72585"), Color(hex: "7209B7"), Color(hex: "3A0CA3"), Color(hex: "4CC9F0")],
        startPoint: .topLeading, endPoint: .bottomTrailing
    )

    /// 9. Warm Ember -- cozy and inviting
    static let warmEmber = LinearGradient(
        colors: [Color(hex: "D32F2F"), Color(hex: "FF6B35"), Color(hex: "F7C948")],
        startPoint: .bottomLeading, endPoint: .topTrailing
    )

    /// 10. Iridescent Pearl -- subtle luxury shimmer
    static let iridescentPearl = LinearGradient(
        colors: [
            Color(hex: "E8D5F5"), Color(hex: "C4E0F9"),
            Color(hex: "D1FAE5"), Color(hex: "FEF3C7"), Color(hex: "E8D5F5"),
        ],
        startPoint: .topLeading, endPoint: .bottomTrailing
    )
}

// Usage in a view
struct GradientShowcase: View {
    var body: some View {
        ScrollView {
            VStack(spacing: 16) {
                gradientCard("Oceanic Depths", gradient: StunningGradients.oceanicDepths)
                gradientCard("Sunset Boulevard", gradient: StunningGradients.sunsetBoulevard)
                gradientCard("Northern Lights", gradient: StunningGradients.northernLights)
                gradientCard("Rose Gold", gradient: StunningGradients.roseGold)
                gradientCard("Electric Violet", gradient: StunningGradients.electricViolet)
                gradientCard("Midnight City", gradient: StunningGradients.midnightCity)
                gradientCard("Fresh Mint", gradient: StunningGradients.freshMint)
                gradientCard("Cyber Punk", gradient: StunningGradients.cyberPunk)
                gradientCard("Warm Ember", gradient: StunningGradients.warmEmber)
                gradientCard("Iridescent Pearl", gradient: StunningGradients.iridescentPearl)
            }
            .padding()
        }
    }

    func gradientCard(_ name: String, gradient: LinearGradient) -> some View {
        RoundedRectangle(cornerRadius: 20)
            .fill(gradient)
            .frame(height: 100)
            .overlay(
                Text(name)
                    .font(.title3.weight(.bold))
                    .foregroundStyle(.white)
                    .shadow(color: .black.opacity(0.3), radius: 4, y: 2)
            )
    }
}

9. Vibrancy and Blur Effects

struct VibrancyEffectDemo: View {
    var body: some View {
        ZStack {
            // Background image or gradient
            LinearGradient(
                colors: [Color(hex: "6C63FF"), Color(hex: "EC4899")],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()

            VStack(spacing: 20) {
                // Vibrant label on material
                Text("Vibrant Title")
                    .font(.largeTitle.weight(.bold))
                    .foregroundStyle(.primary)
                    .padding(20)
                    .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))

                // Hierarchical vibrancy
                VStack(alignment: .leading, spacing: 8) {
                    Label("Primary", systemImage: "star.fill")
                        .foregroundStyle(.primary)
                    Label("Secondary", systemImage: "star.leadinghalf.filled")
                        .foregroundStyle(.secondary)
                    Label("Tertiary", systemImage: "star")
                        .foregroundStyle(.tertiary)
                }
                .font(.headline)
                .padding(20)
                .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16))
            }
        }
    }
}

10. Color Accessibility

Contrast Ratios

WCAG 2.1 requires at least 4.5:1 contrast for normal text and 3:1 for large text. Use these helper utilities.

extension Color {
    /// Calculate relative luminance of a color.
    /// Returns a value between 0 (black) and 1 (white).
    func relativeLuminance() -> Double {
        // Approximate; for exact values resolve the UIColor components.
        // This is a conceptual guide -- use UIColor for runtime calculation:
        var r: CGFloat = 0; var g: CGFloat = 0; var b: CGFloat = 0; var a: CGFloat = 0
        UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a)

        func linearize(_ c: CGFloat) -> Double {
            let v = Double(c)
            return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4)
        }

        return 0.2126 * linearize(r) + 0.7152 * linearize(g) + 0.0722 * linearize(b)
    }

    /// Calculate WCAG contrast ratio between two colors.
    func contrastRatio(with other: Color) -> Double {
        let l1 = self.relativeLuminance()
        let l2 = other.relativeLuminance()
        let lighter = max(l1, l2)
        let darker = min(l1, l2)
        return (lighter + 0.05) / (darker + 0.05)
    }
}

struct ContrastChecker: View {
    let foreground = Color(hex: "1A2233")
    let background = Color(hex: "F4F7FA")

    var body: some View {
        let ratio = foreground.contrastRatio(with: background)
        VStack(spacing: 12) {
            Text("Contrast Ratio: \(ratio, specifier: "%.1f"):1")
                .font(.headline)
            Text(ratio >= 4.5 ? "WCAG AA Pass" : "WCAG AA Fail")
                .font(.subheadline)
                .foregroundStyle(ratio >= 4.5 ? .green : .red)

            Text("Sample Text on Background")
                .foregroundStyle(foreground)
                .padding()
                .background(background, in: RoundedRectangle(cornerRadius: 12))
        }
        .padding()
    }
}

Color Blind Friendly Design Tips

Never rely on color alone to convey meaning -- combine with icons, labels, or patterns.
Use high-contrast pairings that remain distinguishable under protanopia, deuteranopia, and tritanopia.
Test with Xcode Accessibility Inspector or Simulator color filters.
Prefer blue/orange pairings (distinguishable under all common types) over red/green.

struct ColorBlindFriendlyStatus: View {
    var body: some View {
        HStack(spacing: 16) {
            Label("Success", systemImage: "checkmark.circle.fill")
                .foregroundStyle(Color(hex: "2D9F6F"))
            Label("Warning", systemImage: "exclamationmark.triangle.fill")
                .foregroundStyle(Color(hex: "F59E0B"))
            Label("Error", systemImage: "xmark.circle.fill")
                .foregroundStyle(Color(hex: "DC2626"))
        }
        .font(.headline)
    }
}

Quick Reference

Category

Key Types

Semantic Labels

.primary, .secondary, .tertiary, .quaternary

System Backgrounds

Color(.systemBackground), .secondarySystemBackground, .tertiarySystemBackground

Grouped Backgrounds

Color(.systemGroupedBackground), .secondarySystemGroupedBackground, .tertiarySystemGroupedBackground

Fills

Color(.systemFill) through Color(.quaternarySystemFill)

Materials

.ultraThinMaterial through .ultraThickMaterial

Gradients

LinearGradient, RadialGradient, AngularGradient, MeshGradient

Scheme Override

.environment(\.colorScheme, .dark)

Asset Catalog

Color("AssetName")

Hex Init

Color(hex: "FF5733")

Body-text pairs
 — Text on Background
14.20:1
 light,
16.51:1
 dark · Text on Surface
15.24:1
 light,
15.36:1
 dark. All four clear the 4.5:1 body-text bar.

---

# Dark and increased-contrast app colors
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-dark-mode-colors.html

← Back to all frameworks & guides
All articles →Design · Reference guideDark and increased-contrast app colorsRepository guidance for Dark and increased-contrast app colors. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
Pattern
Verification sequence
Sources

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Choose base surfaces
↓2
Separate raised layers
↓3
Check both appearances
↓4
Inspect accessibility settings

02 / ArchitectureResponsibility boundariesBoundary 1
Named assetsBoundary 2
Appearance resolutionBoundary 3
Native containersConnected responsibilities, not a required class hierarchy or an execution trace.
Context

A dark palette is not an RGB inverse. The generator independently assigns a dark base, raised and grouped surfaces, text and state colors. The primary seed retains its hue identity while foreground roles are adjusted for contrast.

Pattern

Use named asset colors with Any and Dark values. The existing asset schema also requires highContrastLight and highContrastDark; these map to Xcode's contrast appearance selectors. Keep the same semantic role across appearances. A light brand blue can become lighter in dark mode while the button foreground changes to black.

OLED preference sets only the dark base to black. Elevated and grouped surfaces remain visible. Do not assume black alone reduces energy consumption for every device or screen composition. Shadows are not reliable separation on a black base; use native materials, spacing or appropriate boundaries.

For native lists prefer
UIColor.systemGroupedBackground
 and the secondary/tertiary system grouped backgrounds. In SwiftUI
.primary
,
.secondary
,
.background
 and system materials adapt to context. The custom
SurfaceOverlay
 token is an opaque fallback, not a substitute for blur or Liquid Glass.

Verification sequence

Inspect text, buttons, selection and status indicators in Any and Dark.
Turn on Increase Contrast; check the selected asset variants and native material changes.
Turn on Differentiate Without Color; selected and error states need icons, labels or shape cues.
Increase Dynamic Type and check truncation, alignment and touch targets separately.
Check toolbar, navigation and tab tint in actual native containers. Avoid coloring every symbol or toolbar background manually.

SwiftUI
tint
 is the preferred control-level customization where supported.
Color.accentColor
 reads the asset accent; it is not a guarantee that every control is tinted identically. For SF Symbols retain hierarchical/monochrome system rendering unless palette rendering carries meaning that remains understandable without color.

Sources

Apple UIColor
,
SwiftUI Color
, and
Xcode color-scheme setup
 describe adaptive color and appearance assets. Consult the
HIG Dark Mode page
 when evaluating the finished UI; its JavaScript-only content was not used as evidence of new requirements in this implementation.

---

# Design Tokens, Adaptive Color, and Liquid Glass
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-design-tokens.html

← Back to all frameworks & guides
All articles →Design · Reference guideDesign Tokens, Adaptive Color, and Liquid GlassRepository guidance for Design Tokens, Adaptive Color, and Liquid Glass. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

1. The Three-Tier Token Architecture

Implementation
Injecting the theme
Tier 3 — component tokens as ViewModifiers
Anti-patterns

2. Dark Mode Compliance

Prefer semantic system colors for surfaces and text
Custom brand colors need both variants
Elevation reads differently in each mode
Verify, don't assume

3. Dynamic Type Compliance

Never use a fixed point size
Layouts must reflow, not clip
Cap Dynamic Type only where it is genuinely unavoidable
Compliance checklist
Respect the other accessibility settings too

4. Materials and Liquid Glass

The one rule for any blur effect
Liquid Glass (iOS 26+, refined in iOS 27)
Availability fallback

5. Contrast Verification
Quick Reference
Palette tooling

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Define token names
↓2
Map values to appearances
↓3
Generate shared resources
↓4
Reject literal styling

02 / ArchitectureResponsibility boundariesBoundary 1
Token sourceBoundary 2
Generated resourcesBoundary 3
View consumersConnected responsibilities, not a required class hierarchy or an execution trace.
For executable token JSON → asset catalogs and editable SVG → PNG icons, see
asset generation
.

Load this when:
 building or reviewing an app's design system, adding a theme,
auditing dark-mode or Dynamic Type compliance, or applying glass/material
effects.

docs/design/color-system.md
 gives you
palettes
 (hex values, gradients).
This document gives you the
system
: how those values are structured so a single
change propagates everywhere, and how the result stays legible in dark mode, at
accessibility text sizes, and under increased-contrast settings.

1. The Three-Tier Token Architecture

Never let a raw color or number appear at a call site. Tokens flow in one
direction through three tiers:

Tier 1 — Primitive   Tier 2 — Semantic        Tier 3 — Component
(raw values)          (intent)                  (usage)
blue500  #0A84FF  →   accent                →   Button.background
gray900  #1C1C1E  →   textPrimary           →   Card.titleColor
space4   16pt     →   spacing.contentInset  →   Card.padding

Rules

Views reference
Tier 3 or Tier 2 only
. A view that names
blue500
 is a bug.
Tier 1 is
private
 to the token module. It has no dark-mode variant — it is
  literally just a number.
Tier 2 is where light/dark, high-contrast, and theme switching resolve.
Adding a theme means adding one Tier 2 implementation, not editing views.

Implementation

// DesignSystem/Tokens/Primitives.swift
// Tier 1 — raw values. Never referenced from a View.
enum Primitive {
    static let blue500  = Color(hex: 0x0A84FF)
    static let blue600  = Color(hex: 0x0060DF)
    static let indigo500 = Color(hex: 0x5E5CE6)
    static let red500   = Color(hex: 0xFF3B30)
    static let green500 = Color(hex: 0x34C759)
    static let amber500 = Color(hex: 0xFF9F0A)

    // Spacing scale — a 4pt rhythm. Nothing else is permitted.
    static let space1: CGFloat = 4
    static let space2: CGFloat = 8
    static let space3: CGFloat = 12
    static let space4: CGFloat = 16
    static let space5: CGFloat = 24
    static let space6: CGFloat = 32
    static let space7: CGFloat = 48

    // Radii
    static let radiusS: CGFloat = 8
    static let radiusM: CGFloat = 16
    static let radiusL: CGFloat = 24
}

// THE canonical hex initialiser for this skill. Defined here, in the token
// layer, and nowhere else — `color-system.md` and
// `templates/common-patterns/design-system.swift` reference this file rather
// than redeclaring it. Two files declaring `init(hex: String)` with different
// bodies is not a style disagreement: copy both into one target and the
// compiler rejects it with `invalid redeclaration of 'init(hex:)'`.
extension Color {
    /// Hex as an integer literal: `Color(hex: 0x6C63FF)`.
    ///
    /// Preferred over the string form because a typo is a compile error rather
    /// than a runtime surprise — `0x6C63FZ` does not build, `"6C63FZ"` does.
    init(hex: UInt32, opacity: Double = 1) {
        self.init(
            .sRGB,
            red:   Double((hex >> 16) & 0xFF) / 255,
            green: Double((hex >>  8) & 0xFF) / 255,
            blue:  Double( hex        & 0xFF) / 255,
            opacity: opacity
        )
    }

    /// Hex as a string: `Color(hex: "6C63FF")`, with or without `#`,
    /// 6 digits (RGB) or 8 (RRGGBBAA).
    ///
    /// Exists because designers hand over strings and remote themes arrive as
    /// JSON. A malformed value traps in DEBUG and renders **magenta** in
    /// release — never black. The earlier versions of this initialiser fell
    /// back to black, which is indistinguishable from a deliberate colour and
    /// so shipped unnoticed; magenta appears nowhere in any of these palettes
    /// and is impossible to mistake for intent.
    init(hex string: String, opacity: Double = 1) {
        let cleaned = string
            .trimmingCharacters(in: .whitespacesAndNewlines)
            .replacingOccurrences(of: "#", with: "")

        var value: UInt64 = 0
        let scanned = Scanner(string: cleaned).scanHexInt64(&value)

        switch (scanned, cleaned.count) {
        case (true, 6):
            self.init(hex: UInt32(truncatingIfNeeded: value), opacity: opacity)
        case (true, 8):
            let alpha = Double(value & 0xFF) / 255
            self.init(hex: UInt32(truncatingIfNeeded: value >> 8), opacity: opacity * alpha)
        default:
            assertionFailure("Malformed hex colour literal: \(string)")
            self.init(hex: 0xFF00FF, opacity: opacity)
        }
    }
}

// DesignSystem/Tokens/Theme.swift
// Tier 2 — semantic intent. This is the swappable layer.
protocol Theme: Sendable {
    // Surfaces
    var background: Color { get }        // the page
    var surface: Color { get }           // cards, sheets
    var surfaceElevated: Color { get }   // popovers, menus

    // Content — must meet contrast against the surface it sits on
    var textPrimary: Color { get }
    var textSecondary: Color { get }
    var textOnAccent: Color { get }

    // Intent
    var accent: Color { get }
    var accentPressed: Color { get }
    var destructive: Color { get }
    var success: Color { get }
    var warning: Color { get }

    // Separators and elevation
    var separator: Color { get }
    var shadow: Color { get }
}

struct OceanTheme: Theme {
    // Apple's semantic colors already resolve light/dark AND increased contrast.
    // Prefer them for surfaces and text; reserve custom hex for brand accents.
    var background      = Color(.systemBackground)
    var surface         = Color(.secondarySystemBackground)
    var surfaceElevated = Color(.tertiarySystemBackground)

    var textPrimary   = Color(.label)
    var textSecondary = Color(.secondaryLabel)
    var textOnAccent  = Color.white

    var accent        = Primitive.blue500
    var accentPressed = Primitive.blue600
    var destructive   = Color(.systemRed)
    var success       = Color(.systemGreen)
    var warning       = Color(.systemOrange)

    var separator = Color(.separator)
    var shadow    = Color.black.opacity(0.08)
}

// DesignSystem/Tokens/Spacing.swift — Tier 2 for layout
enum Space {
    static let hairline    = Primitive.space1   // icon-to-label
    static let tight       = Primitive.space2   // within a control
    static let element     = Primitive.space3   // between related elements
    static let contentInset = Primitive.space4  // card padding, screen margins
    static let section     = Primitive.space5   // between sections
    static let major       = Primitive.space6   // above a page title
}

enum Radius {
    static let control = Primitive.radiusS      // buttons, chips
    static let card    = Primitive.radiusM      // cards, tiles
    static let sheet   = Primitive.radiusL      // modals
}

Injecting the theme

private struct ThemeKey: EnvironmentKey {
    static let defaultValue: any Theme = OceanTheme()
}

extension EnvironmentValues {
    var theme: any Theme {
        get { self[ThemeKey.self] }
        set { self[ThemeKey.self] = newValue }
    }
}

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            RootView().environment(\.theme, OceanTheme())
        }
    }
}

Tier 3 — component tokens as ViewModifiers

struct CardStyle: ViewModifier {
    @Environment(\.theme) private var theme

    func body(content: Content) -> some View {
        content
            .padding(Space.contentInset)
            .background(theme.surface, in: .rect(cornerRadius: Radius.card))
            .shadow(color: theme.shadow, radius: 8, y: 4)
    }
}

extension View {
    func cardStyle() -> some View { modifier(CardStyle()) }
}

// Usage — no raw values anywhere.
VStack(alignment: .leading, spacing: Space.element) {
    Text("Monthly total").font(.headline).foregroundStyle(theme.textPrimary)
    Text("$1,240").font(.largeTitle.bold()).foregroundStyle(theme.accent)
}
.cardStyle()

Anti-patterns

// WRONG — raw values at the call site. Changing the brand means grepping.
.padding(16)
.background(Color(red: 0.04, green: 0.52, blue: 1.0))
.cornerRadius(16)

// WRONG — a Tier 1 primitive leaking into a view.
.foregroundStyle(Primitive.blue500)

// WRONG — a semantic name that describes appearance, not intent.
var lightGray: Color { … }        // what happens in dark mode?
var textSecondary: Color { … }    // correct

// RIGHT
.padding(Space.contentInset)
.background(theme.accent, in: .rect(cornerRadius: Radius.card))

2. Dark Mode Compliance

Prefer semantic system colors for surfaces and text

They adapt to light/dark
and
 to Increase Contrast and Reduce Transparency —
three accessibility settings for the price of one.

Role

Token

Light

Dark

Page

Color(.systemBackground)

white

black

Card

Color(.secondarySystemBackground)

light gray

dark gray

Elevated

Color(.tertiarySystemBackground)

white

lighter gray

Primary text

Color(.label)

near-black

near-white

Secondary text

Color(.secondaryLabel)

60%

60%

Divider

Color(.separator)

thin gray

thin gray

Custom brand colors need both variants

A brand accent tuned for white backgrounds usually fails on black. Define both
and resolve at render time:

extension Color {
    /// Resolves per trait collection — works in both modes without an asset catalog.
    static func adaptive(light: Color, dark: Color) -> Color {
        Color(UIColor { traits in
            traits.userInterfaceStyle == .dark ? UIColor(dark) : UIColor(light)
        })
    }
}

struct MidnightTheme: Theme {
    var accent = Color.adaptive(
        light: Primitive.blue500,   // vivid on white
        dark:  Color(hex: 0x64B5FF) // lightened so it stays visible on black
    )
    // …
}

The asset-catalog equivalent (
Color("Accent")
 with Any/Dark appearances) is
preferable when designers own the values; the code form is preferable when the
theme is swappable at runtime.

Elevation reads differently in each mode

Light mode:
 elevation = shadow. A card is white on light gray with

.shadow(color: theme.shadow, radius: 8, y: 4)
.
Dark mode:
 shadows are nearly invisible on black. Elevation = a
lighter

  surface.
secondarySystemBackground
 already does this; do not add a heavier
  shadow to compensate.

@Environment(\.colorScheme) private var scheme

.shadow(color: theme.shadow, radius: scheme == .dark ? 0 : 8, y: 4)
.overlay(                                  // a hairline stroke reads better in dark
    RoundedRectangle(cornerRadius: Radius.card)
        .strokeBorder(theme.separator, lineWidth: scheme == .dark ? 1 : 0)
)

Verify, don't assume

#Preview("Light") { ContentView().preferredColorScheme(.light) }
#Preview("Dark")  { ContentView().preferredColorScheme(.dark) }
#Preview("Increased Contrast") {
    ContentView().environment(\.colorSchemeContrast, .increased)
}

Every screen ships with all three previews. A pairing that only exists in one
mode is not done.

3. Dynamic Type Compliance

Never use a fixed point size

// WRONG — ignores the user's text size entirely.
.font(.system(size: 17))
.frame(height: 44)                       // clips at accessibility sizes

// RIGHT — semantic styles scale automatically.
.font(.headline)
.frame(minHeight: 44)                    // a floor, not a ceiling

// RIGHT — a custom font that still scales.
.font(.custom("Inter-SemiBold", size: 17, relativeTo: .headline))

Layouts must reflow, not clip

The accessibility sizes (
.accessibility1
 …
.accessibility5
) can triple text
height. Horizontal rows must become vertical stacks.

struct StatRow: View {
    @Environment(\.dynamicTypeSize) private var typeSize
    let label: String
    let value: String

    var body: some View {
        // ViewThatFits picks the first layout that fits — no manual breakpoint.
        ViewThatFits(in: .horizontal) {
            HStack(spacing: Space.element) {
                Text(label)
                Spacer()
                Text(value).fontWeight(.semibold)
            }
            VStack(alignment: .leading, spacing: Space.hairline) {
                Text(label)
                Text(value).fontWeight(.semibold)
            }
        }
    }
}

// Or branch explicitly when the two layouts differ structurally.
if typeSize.isAccessibilitySize {
    VStack(alignment: .leading) { icon; label }
} else {
    HStack { icon; label }
}

Cap Dynamic Type only where it is genuinely unavoidable

// Acceptable: a fixed-height chart axis label or a tab bar item.
.dynamicTypeSize(...DynamicTypeSize.accessibility1)

// NOT acceptable: body copy, form fields, buttons, or list rows.
.dynamicTypeSize(.large)   // hard-pins every user to one size — never do this

Compliance checklist

[ ] No
.font(.system(size:))
 without
relativeTo:
.
[ ] No fixed
.frame(height:)
 on a container holding text — use
minHeight
.
[ ] Every
HStack
 of label+value has a vertical fallback (
ViewThatFits
 or
      an
isAccessibilitySize
 branch).
[ ] Icons paired with text use
.imageScale(.medium)
 or a scaled symbol so
      they grow together.
[ ] Tap targets stay ≥ 44×44pt at every size.
[ ] Previewed at
.xSmall

and

.accessibility5
:

#Preview("XS")  { ContentView().dynamicTypeSize(.xSmall) }
#Preview("A11y5") { ContentView().dynamicTypeSize(.accessibility5) }

Respect the other accessibility settings too

@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency

.animation(reduceMotion ? nil : .spring(duration: 0.3), value: isExpanded)
.background(reduceTransparency ? AnyShapeStyle(theme.surface)
                               : AnyShapeStyle(.ultraThinMaterial))

4. Materials and Liquid Glass

The one rule for any blur effect

A material is only correct when there is content behind it.
 Applied to a
solid background it renders as flat gray mud — the single most common way a
SwiftUI UI looks unfinished.

// WRONG — nothing behind it. This is just a muddy gray rectangle.
VStack { … }
    .background(.ultraThinMaterial)
    .background(theme.background)

// RIGHT — content scrolls beneath a floating bar.
ScrollView { content }
    .safeAreaInset(edge: .bottom) {
        HStack { … }
            .padding(Space.contentInset)
            .background(.ultraThinMaterial)
    }

Material

Blur

Use for

.ultraThinMaterial

lightest

Floating toolbars over content

.thinMaterial

light

Sheet backgrounds, overlays

.regularMaterial

medium

Sidebars, popovers

.thickMaterial

heavy

Modal scrims

.bar

system

Custom nav/tab bars

Liquid Glass (iOS 26+, refined in iOS 27)

Adopting it in an existing app is a different job.
 This section is about
applying the material to a view you own. For what an SDK rebuild changes on
its own — custom bar backgrounds that now fight the system, the scroll edge
effect, title-case section headers, layered app icons, and the

UIDesignRequiresCompatibility
 escape hatch — see

docs/design/liquid-glass-adoption.md
.

Liquid Glass is a dynamic material that refracts and reflects what is behind it
and responds to motion. It supersedes hand-rolled "glassmorphism" (a blur plus a
white stroke plus a gradient), which you should stop writing.

Availability: guard on iOS 26, not iOS 27.
 The Liquid Glass APIs
(
glassEffect
,
GlassEffectContainer
,
glassEffectID
,
.buttonStyle(.glass)
)
were introduced in
iOS 26
. iOS 27 continues and refines the design system,
but it did not reintroduce the API. Writing
if #available(iOS 27, *)
 around

glassEffect
 would drop every iOS 26 device to the fallback path for no reason
— a silent regression for a large installed base.
Guard on the version where
the symbol became available, never on the newest version you happen to be
building with.
 That rule holds for every API, not just this one.

if #available(iOS 26.0, *) {
    Text("Now Playing")
        .padding(Space.contentInset)
        .glassEffect()                                    // .regular, in a Capsule
}

// Shape, tint, and interactivity
.glassEffect(
    .regular
        .tint(theme.accent.opacity(0.7))
        .interactive(),                                   // reacts to touch
    in: .rect(cornerRadius: Radius.card)
)

// Buttons
Button("Play") { … }
    .buttonStyle(.glass)                                  // standard glass
Button("Subscribe") { … }
    .buttonStyle(.glassProminent)                         // accent-filled glass

Grouping and morphing.
 Sibling glass elements must live in a

GlassEffectContainer
 so they blend and merge instead of stacking blurs — each
independent
.glassEffect()
 is a separate expensive render pass.

@available(iOS 26.0, *)
struct PlayerControls: View {
    @Namespace private var namespace
    @State private var isExpanded = false

    var body: some View {
        GlassEffectContainer(spacing: Space.tight) {
            HStack(spacing: Space.tight) {
                Button { … } label: { Image(systemName: "backward.fill") }
                    .glassEffect()
                    .glassEffectID("back", in: namespace)

                Button { isExpanded.toggle() } label: {
                    Image(systemName: isExpanded ? "pause.fill" : "play.fill")
                }
                .glassEffect()
                .glassEffectID("play", in: namespace)

                if isExpanded {
                    Button { … } label: { Image(systemName: "forward.fill") }
                        .glassEffect()
                        .glassEffectID("forward", in: namespace)
                }
            }
        }
        .animation(.spring(duration: 0.4), value: isExpanded)
    }
}

Guidelines

Glass goes on the
navigation layer
 — floating controls, toolbars, tab bars,
  overlays. Not on content itself, and never on a whole scrolling list.
Never place glass on glass. One layer, over content.
Text on glass uses
Color(.label)
, never a reduced opacity. The material
  already lowers effective contrast; do not lower it further.
Keep the count small. Every glass surface is a render pass; a grid of twenty
  glass cards will drop frames on older devices.

Availability fallback

Ship one modifier, branch once:

extension View {
    /// Liquid Glass on iOS 26+, an equivalent material treatment below it.
    func adaptiveGlass(cornerRadius: CGFloat = Radius.card) -> some View {
        modifier(AdaptiveGlass(cornerRadius: cornerRadius))
    }
}

private struct AdaptiveGlass: ViewModifier {
    @Environment(\.accessibilityReduceTransparency) private var reduceTransparency
    @Environment(\.theme) private var theme
    let cornerRadius: CGFloat

    func body(content: Content) -> some View {
        if reduceTransparency {
            // Accessibility wins over aesthetics — opaque surface, no blur.
            content.background(theme.surface, in: .rect(cornerRadius: cornerRadius))
        } else if #available(iOS 26.0, *) {
            content.glassEffect(.regular, in: .rect(cornerRadius: cornerRadius))
        } else {
            content
                .background(.ultraThinMaterial, in: .rect(cornerRadius: cornerRadius))
                .overlay(
                    RoundedRectangle(cornerRadius: cornerRadius)
                        .strokeBorder(.white.opacity(0.15), lineWidth: 1)
                )
        }
    }
}

5. Contrast Verification

The readability rules in
SKILL.md
 are testable. Compute the ratio rather than
eyeballing it:

extension Color {
    /// WCAG relative luminance.
    private var relativeLuminance: Double {
        let components = UIColor(self).cgColor.components ?? [0, 0, 0]
        func channel(_ value: CGFloat) -> Double {
            let v = Double(value)
            return v <= 0.03928 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4)
        }
        return 0.2126 * channel(components[0])
             + 0.7152 * channel(components[safe: 1] ?? components[0])
             + 0.0722 * channel(components[safe: 2] ?? components[0])
    }

    /// WCAG contrast ratio, 1.0 (identical) to 21.0 (black on white).
    func contrastRatio(against other: Color) -> Double {
        let a = relativeLuminance, b = other.relativeLuminance
        let lighter = max(a, b), darker = min(a, b)
        return (lighter + 0.05) / (darker + 0.05)
    }
}

private extension Array {
    subscript(safe index: Int) -> Element? {
        indices.contains(index) ? self[index] : nil
    }
}

Then assert it in the test target so a palette change cannot regress
accessibility:

@Test("theme meets WCAG AA in both modes")
func themeContrast() {
    let theme = OceanTheme()
    #expect(theme.textPrimary.contrastRatio(against: theme.surface) >= 4.5)
    #expect(theme.textOnAccent.contrastRatio(against: theme.accent) >= 4.5)
    #expect(theme.textSecondary.contrastRatio(against: theme.surface) >= 4.5)
}

Content

Minimum ratio

Body text

4.5:1

Large text (18pt+, or 14pt bold)

3:1

UI controls, icons, focus rings

3:1

Decorative, disabled

no requirement

Quick Reference

Need

Use

Page background

theme.background → Color(.systemBackground)

Card background

theme.surface → Color(.secondarySystemBackground)

Any spacing value

Space.* — never a literal

Any corner radius

Radius.* — never a literal

Brand accent

Color.adaptive(light:dark:) or an asset-catalog color

Floating bar over content

.ultraThinMaterial, or .glassEffect() on iOS 26+

Multiple glass elements

one GlassEffectContainer

Text size

semantic styles, or .custom(_:size:relativeTo:)

Row that must reflow

ViewThatFits or typeSize.isAccessibilitySize

Verifying a pairing

contrastRatio(against:) in a test

Palette tooling

Use
palette generation
 to produce a four-appearance token preview compatible with the existing assets CLI. Use
color accessibility
 to interpret measured pairs. Only write or merge a catalog when requested; retain system semantics when custom colors add no value.

---

# iOS Font Catalog — Ultimate Reference
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-fonts-catalog.html

← Back to all frameworks & guides
All articles →Design · Reference guideiOS Font Catalog — Ultimate ReferenceRepository guidance for iOS Font Catalog — Ultimate Reference. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Table of Contents
1. Apple System Fonts

SF Pro — The Default System Font
SF Pro Display vs SF Pro Text
SF Pro Rounded
SF Mono — Monospaced
SF Compact
New York — Serif Font
Width Variants (iOS 16+)
Complete System Font Design Matrix
All SwiftUI Text Styles

2. Built-in iOS Fonts

Sans-Serif Fonts

Helvetica Neue
Avenir
Avenir Next
Avenir Next Condensed
Gill Sans
Futura
Optima
Verdana
Trebuchet MS
Arial
Arial Rounded MT Bold
Academy Engraved LET
Al Nile
DIN Alternate / DIN Condensed
Helvetica

Serif Fonts

Georgia
Times New Roman
Palatino
Baskerville
Didot
Bodoni 72
Charter
Iowan Old Style
Cochin
Superclarendon

Monospaced Fonts

Courier New
Courier
Menlo
American Typewriter

Display and Decorative Fonts

Rockwell
Copperplate
Papyrus
Marker Felt
Chalkboard SE
Chalkduster
Noteworthy
Zapfino
Party LET
Savoye LET

Script and Handwriting Fonts

Snell Roundhand
Bradley Hand
Kefa
Kohinoor Telugu

Additional Built-in Fonts

Symbol / Dingbat Fonts
Additional Sans-Serif

International and Multi-script Fonts

Chinese
Japanese
Korean
Arabic
Devanagari (Hindi)
Bangla
Gujarati
Telugu
Gurmukhi (Punjabi)

3. Google Fonts -- Top 100 for iOS

Sans-Serif
Serif
Monospaced
Display
Handwriting and Script

4. How to Add Custom Fonts to iOS

Step 1: Obtain Font Files
Step 2: Add Files to Xcode Project
Step 3: Register Fonts in Info.plist
Step 4: Find the Exact Font Name
Step 5: Use in SwiftUI
Step 6: Use in UIKit
Dynamic Type with @ScaledMetric
Complete Custom Font Integration Example
Swift Package Manager Font Loading
Troubleshooting Custom Fonts

5. Font Pairing Recommendations

Pairing 1: SF Pro Display + SF Pro Text
Pairing 2: Playfair Display + Source Sans 3
Pairing 3: Montserrat + Lora
Pairing 4: Poppins + Inter
Pairing 5: Bebas Neue + Roboto
Pairing 6: DM Serif Display + DM Sans
Pairing 7: Space Grotesk + Inter
Pairing 8: Plus Jakarta Sans + Source Serif 4
Pairing 9: Outfit + Lato
Pairing 10: Oswald + Open Sans
Pairing 11: New York + SF Pro
Pairing 12: Archivo Black + Work Sans
Pairing 13: Cormorant Garamond + Montserrat
Pairing 14: Fredoka + Nunito
Pairing 15: Manrope + Merriweather

6. Font Management Utilities

FontManager: Register Custom Fonts Programmatically
App-Specific Type Scale Extension
Font Preview View
Dynamic Type Helper
List All Device Fonts (Utility Function)

7. Variable Fonts

What Are Variable Fonts?
Benefits
Common Axes
Using Variable Fonts in SwiftUI
Animating Variable Font Axes
Axis Tags Reference
Inspecting Variable Font Axes
Popular Variable Fonts for iOS
Variable Font with Dynamic Type

Quick Reference: Font Selection Decision Tree
Quick Reference: PostScript Names Cheat Sheet

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Choose a reading role
↓2
Check font license
↓3
Register bundled font
↓4
Test Dynamic Type

02 / ArchitectureResponsibility boundariesBoundary 1
Font filesBoundary 2
Typography rolesBoundary 3
Scaled textConnected responsibilities, not a required class hierarchy or an execution trace.

The definitive font reference for iOS and SwiftUI development.
Every font name listed here is the exact string to use in
Font.custom()
 or
UIFont(name:size:)
.

Table of Contents

Apple System Fonts
Built-in iOS Fonts
Google Fonts — Top 100 for iOS
How to Add Custom Fonts to iOS
Font Pairing Recommendations
Font Management Utilities
Variable Fonts

1. Apple System Fonts

Apple provides a family of proprietary system fonts that are optimized for every Apple platform.
These fonts are accessed through the SwiftUI
.system()
 modifier and cannot be referenced by
PostScript name in
Font.custom()
. They are available on-device without bundling.

SF Pro — The Default System Font

SF Pro is the default sans-serif used across iOS, iPadOS, macOS, and tvOS.
The system automatically selects
SF Pro Text
 for sizes below 20pt and

SF Pro Display
 for sizes at 20pt and above. You never need to handle this manually.

All Weights:

Weight

UIFont.Weight

SwiftUI

Ultra Light

.ultraLight

.ultraLight

Thin

.thin

.thin

Light

.light

.light

Regular

.regular

.regular

Medium

.medium

.medium

Semibold

.semibold

.semibold

Bold

.bold

.bold

Heavy

.heavy

.heavy

Black

.black

.black

SwiftUI Examples:

// Default system font at various text styles
Text("Headline").font(.headline)
Text("Body text").font(.body)
Text("Caption").font(.caption)

// Explicit size and weight
Text("Custom").font(.system(size: 24, weight: .bold))
Text("Light text").font(.system(size: 16, weight: .light))
Text("Heavy text").font(.system(size: 32, weight: .heavy))

// Using text style with weight
Text("Title").font(.system(.title, weight: .semibold))
Text("Footnote").font(.system(.footnote, weight: .medium))

UIKit Examples:

let headline = UIFont.systemFont(ofSize: 24, weight: .bold)
let body = UIFont.systemFont(ofSize: 17, weight: .regular)
let caption = UIFont.systemFont(ofSize: 12, weight: .light)
let preferredBody = UIFont.preferredFont(forTextStyle: .body)

SF Pro Display vs SF Pro Text

The system handles optical size switching automatically:

SF Pro Text
: Optimized for sizes below 20pt. Slightly wider letter spacing, more open counters for legibility at small sizes.
SF Pro Display
: Optimized for sizes 20pt and above. Tighter spacing, refined details that shine at large sizes.

You do NOT need to select these manually. The system font API handles the switch.

// The system chooses Text or Display automatically based on size
Text("Small").font(.system(size: 14))   // Uses SF Pro Text
Text("Large").font(.system(size: 28))   // Uses SF Pro Display

SF Pro Rounded

A rounded variant of SF Pro with softer terminals. Great for friendly, approachable UIs,
settings screens, and apps aimed at younger audiences.

// SwiftUI — Rounded design
Text("Rounded").font(.system(size: 20, weight: .bold, design: .rounded))
Text("Rounded body").font(.system(.body, design: .rounded))
Text("Rounded title").font(.system(.title, design: .rounded, weight: .semibold))
Text("Rounded caption").font(.system(.caption, design: .rounded, weight: .medium))

// UIKit — Rounded design
let descriptor = UIFont.systemFont(ofSize: 20, weight: .bold).fontDescriptor
    .withDesign(.rounded)!
let roundedFont = UIFont(descriptor: descriptor, size: 20)

When to use:
 Health apps, children's apps, casual games, notification badges, friendly onboarding flows, Apple Fitness-style interfaces.

SF Mono — Monospaced

The system monospaced font. Every character occupies the same horizontal space.
Essential for code editors, terminal UIs, data tables with aligned numbers, and
countdown timers.

// SwiftUI — Monospaced design
Text("0123456789").font(.system(size: 16, weight: .regular, design: .monospaced))
Text("func hello()").font(.system(.body, design: .monospaced))
Text("Code block").font(.system(.callout, design: .monospaced, weight: .semibold))

// Monospaced digit (only digits are monospaced, letters are proportional)
Text("$1,234.56").monospacedDigit()

// UIKit
let descriptor = UIFont.systemFont(ofSize: 14, weight: .regular).fontDescriptor
    .withDesign(.monospaced)!
let monoFont = UIFont(descriptor: descriptor, size: 14)

When to use:
 Code editors, terminal emulators, data tables, timers, counters, version numbers, financial figures, developer tools.

SF Compact

Designed for Apple Watch and compact UI contexts. Narrower than SF Pro to fit more
content in constrained spaces.

// watchOS uses SF Compact automatically
// On iOS, you can access it through font descriptors if needed

When to use:
 watchOS apps (used automatically), widgets, compact UI elements.

New York — Serif Font

Apple's serif typeface. Available in four optical sizes that the system selects automatically.
Gives an editorial, literary, or premium magazine feel.

Optical Sizes:

-
Small
: Optimized for caption and footnote sizes
-
Medium
: Optimized for body text
-
Large
: Optimized for titles and headlines
-
Extra Large
: Optimized for large display text

// SwiftUI — Serif design
Text("Editorial").font(.system(size: 32, weight: .bold, design: .serif))
Text("Article body").font(.system(.body, design: .serif))
Text("Book title").font(.system(.largeTitle, design: .serif, weight: .black))
Text("Byline").font(.system(.subheadline, design: .serif, weight: .light))
Text("Pull quote").font(.system(.title2, design: .serif, weight: .semibold))

// UIKit
let descriptor = UIFont.systemFont(ofSize: 24, weight: .bold).fontDescriptor
    .withDesign(.serif)!
let serifFont = UIFont(descriptor: descriptor, size: 24)

When to use:
 News apps, book readers, editorial content, magazine layouts, Apple News-style interfaces, premium branding, literary apps.

Width Variants (iOS 16+)

Starting with iOS 16, you can access compressed, condensed, and expanded width variants
of the system font.

// SwiftUI — Width variants (iOS 16+)
Text("Compressed").font(.system(size: 20, weight: .bold).width(.compressed))
Text("Condensed").font(.system(size: 20, weight: .bold).width(.condensed))
Text("Standard").font(.system(size: 20, weight: .bold).width(.standard))
Text("Expanded").font(.system(size: 20, weight: .bold).width(.expanded))

// Combine with design
Text("Rounded Condensed")
    .font(.system(size: 20, weight: .semibold, design: .rounded).width(.condensed))

// UIKit — Width traits
var traits = UIFontDescriptor.SymbolicTraits()
let descriptor = UIFont.systemFont(ofSize: 20, weight: .bold).fontDescriptor
let condensedDescriptor = descriptor.withSymbolicTraits(.traitCondensed)!
let condensedFont = UIFont(descriptor: condensedDescriptor, size: 20)

Width options:

| Width        | Description                              |
|--------------|------------------------------------------|
|
.compressed
| Narrowest, fits maximum content          |
|
.condensed
 | Narrower than standard                   |
|
.standard
  | Default width                            |
|
.expanded
  | Wider, more spacious letterforms         |

Complete System Font Design Matrix

// All four system font designs
Text("Default").font(.system(.title, design: .default))      // SF Pro
Text("Rounded").font(.system(.title, design: .rounded))      // SF Pro Rounded
Text("Serif").font(.system(.title, design: .serif))           // New York
Text("Monospaced").font(.system(.title, design: .monospaced)) // SF Mono

All SwiftUI Text Styles

Text("Large Title")  .font(.largeTitle)    // 34pt bold
Text("Title")        .font(.title)         // 28pt regular
Text("Title 2")      .font(.title2)        // 22pt regular
Text("Title 3")      .font(.title3)        // 20pt regular
Text("Headline")     .font(.headline)      // 17pt semibold
Text("Subheadline")  .font(.subheadline)   // 15pt regular
Text("Body")         .font(.body)          // 17pt regular
Text("Callout")      .font(.callout)       // 16pt regular
Text("Footnote")     .font(.footnote)      // 13pt regular
Text("Caption")      .font(.caption)       // 12pt regular
Text("Caption 2")    .font(.caption2)      // 11pt regular

2. Built-in iOS Fonts

Every font listed below is preinstalled on iOS. The string shown is the exact PostScript name
to pass to
Font.custom(_:size:)
 or
UIFont(name:size:)
.

Sans-Serif Fonts

Helvetica Neue

The classic Swiss typeface. Was the iOS system font before SF Pro (iOS 8 and earlier).

Variant

Font Name

Ultra Light

HelveticaNeue-UltraLight

Ultra Light Italic

HelveticaNeue-UltraLightItalic

Thin

HelveticaNeue-Thin

Thin Italic

HelveticaNeue-ThinItalic

Light

HelveticaNeue-Light

Light Italic

HelveticaNeue-LightItalic

Regular

HelveticaNeue

Italic

HelveticaNeue-Italic

Medium

HelveticaNeue-Medium

Medium Italic

HelveticaNeue-MediumItalic

Bold

HelveticaNeue-Bold

Bold Italic

HelveticaNeue-BoldItalic

Condensed Bold

HelveticaNeue-CondensedBold

Condensed Black

HelveticaNeue-CondensedBlack

Text("Helvetica Neue").font(.custom("HelveticaNeue", size: 17))
Text("Helvetica Bold").font(.custom("HelveticaNeue-Bold", size: 17))
Text("Helvetica Light").font(.custom("HelveticaNeue-Light", size: 24))

Best for:
 Clean UI text, legacy app compatibility, neutral typography.

Avenir

A geometric sans-serif with a warm, humanist feel. Excellent readability.

Variant

Font Name

Book

Avenir-Book

Book Oblique

Avenir-BookOblique

Roman

Avenir-Roman

Oblique

Avenir-Oblique

Medium

Avenir-Medium

Medium Oblique

Avenir-MediumOblique

Heavy

Avenir-Heavy

Heavy Oblique

Avenir-HeavyOblique

Black

Avenir-Black

Black Oblique

Avenir-BlackOblique

Light

Avenir-Light

Light Oblique

Avenir-LightOblique

Text("Avenir Body").font(.custom("Avenir-Book", size: 17))
Text("Avenir Heading").font(.custom("Avenir-Heavy", size: 28))

Best for:
 Modern app UIs, lifestyle apps, friendly body text.

Avenir Next

The successor to Avenir with improved legibility and a wider weight range.

Variant

Font Name

Ultra Light

AvenirNext-UltraLight

Ultra Light Italic

AvenirNext-UltraLightItalic

Regular

AvenirNext-Regular

Italic

AvenirNext-Italic

Medium

AvenirNext-Medium

Medium Italic

AvenirNext-MediumItalic

Demi Bold

AvenirNext-DemiBold

Demi Bold Italic

AvenirNext-DemiBoldItalic

Bold

AvenirNext-Bold

Bold Italic

AvenirNext-BoldItalic

Heavy

AvenirNext-Heavy

Heavy Italic

AvenirNext-HeavyItalic

Text("Avenir Next").font(.custom("AvenirNext-Regular", size: 17))
Text("Avenir Next Bold").font(.custom("AvenirNext-Bold", size: 24))

Best for:
 Professional apps, enterprise UIs, presentations, marketing content.

Avenir Next Condensed

The condensed variant of Avenir Next. Useful when horizontal space is limited.

Variant

Font Name

Ultra Light

AvenirNextCondensed-UltraLight

Ultra Light Italic

AvenirNextCondensed-UltraLightItalic

Regular

AvenirNextCondensed-Regular

Italic

AvenirNextCondensed-Italic

Medium

AvenirNextCondensed-Medium

Medium Italic

AvenirNextCondensed-MediumItalic

Demi Bold

AvenirNextCondensed-DemiBold

Demi Bold Italic

AvenirNextCondensed-DemiBoldItalic

Bold

AvenirNextCondensed-Bold

Bold Italic

AvenirNextCondensed-BoldItalic

Heavy

AvenirNextCondensed-Heavy

Heavy Italic

AvenirNextCondensed-HeavyItalic

Text("Condensed").font(.custom("AvenirNextCondensed-Bold", size: 20))

Best for:
 Navigation bars, tab labels, space-constrained UI, tags, badges.

Gill Sans

A classic British humanist sans-serif with distinctive character.

Variant

Font Name

Regular

GillSans

Italic

GillSans-Italic

Light

GillSans-Light

Light Italic

GillSans-LightItalic

Semibold

GillSans-SemiBold

Semibold Italic

GillSans-SemiBoldItalic

Bold

GillSans-Bold

Bold Italic

GillSans-BoldItalic

Ultra Bold

GillSans-UltraBold

Text("Gill Sans").font(.custom("GillSans", size: 17))
Text("Gill Sans Bold").font(.custom("GillSans-Bold", size: 24))

Best for:
 British branding, classic design, book covers, elegant headings.

Futura

A geometric sans-serif icon. Clean circles and triangles form its letterforms.

Variant

Font Name

Medium

Futura-Medium

Medium Italic

Futura-MediumItalic

Bold

Futura-Bold

Condensed Medium

Futura-CondensedMedium

Condensed Extra Bold

Futura-CondensedExtraBold

Text("FUTURA").font(.custom("Futura-Bold", size: 32))
Text("Futura body").font(.custom("Futura-Medium", size: 17))

Best for:
 Fashion apps, bold statements, modern branding, geometric design systems.

Optima

A humanist sans-serif with subtle stroke contrast, straddling serif and sans-serif.

Variant

Font Name

Regular

Optima-Regular

Italic

Optima-Italic

Bold

Optima-Bold

Bold Italic

Optima-BoldItalic

Extra Black

Optima-ExtraBlack

Text("Optima").font(.custom("Optima-Regular", size: 17))

Best for:
 Wellness apps, spa and beauty branding, elegant body text, high-end retail.

Verdana

Designed by Matthew Carter for screen legibility. Wide letterforms, generous x-height.

Variant

Font Name

Regular

Verdana

Italic

Verdana-Italic

Bold

Verdana-Bold

Bold Italic

Verdana-BoldItalic

Text("Verdana").font(.custom("Verdana", size: 17))

Best for:
 Maximum screen readability, accessible UIs, form labels.

Trebuchet MS

A humanist sans-serif designed for web and screen use.

Variant

Font Name

Regular

TrebuchetMS

Italic

TrebuchetMS-Italic

Bold

TrebuchetMS-Bold

Bold Italic

Trebuchet-BoldItalic

Text("Trebuchet").font(.custom("TrebuchetMS", size: 17))

Best for:
 Web-style UIs, cross-platform consistency.

Arial

The ubiquitous sans-serif. Near-identical to Helvetica in metrics.

Variant

Font Name

Regular

ArialMT

Italic

Arial-ItalicMT

Bold

Arial-BoldMT

Bold Italic

Arial-BoldItalicMT

Text("Arial").font(.custom("ArialMT", size: 17))

Best for:
 Cross-platform compatibility, documents, web views.

Arial Rounded MT Bold

A rounded, friendly variant of Arial.

Variant

Font Name

Bold

ArialRoundedMTBold

Text("Rounded").font(.custom("ArialRoundedMTBold", size: 20))

Best for:
 Friendly UI elements, buttons, badges.

Academy Engraved LET

Variant

Font Name

Plain

AcademyEngravedLetPlain

Al Nile

Variant

Font Name

Regular

AlNile

Bold

AlNile-Bold

DIN Alternate / DIN Condensed

Variant

Font Name

DIN Alternate Bold

DINAlternate-Bold

DIN Condensed Bold

DINCondensed-Bold

Text("DIN").font(.custom("DINAlternate-Bold", size: 20))

Best for:
 Road signs style, technical UIs, data displays, dashboards.

Helvetica

The original Helvetica (not Neue).

Variant

Font Name

Regular

Helvetica

Italic (Oblique)

Helvetica-Oblique

Light

Helvetica-Light

Light Oblique

Helvetica-LightOblique

Bold

Helvetica-Bold

Bold Oblique

Helvetica-BoldOblique

Serif Fonts

Georgia

A screen-optimized serif with generous proportions.

Variant

Font Name

Regular

Georgia

Italic

Georgia-Italic

Bold

Georgia-Bold

Bold Italic

Georgia-BoldItalic

Text("Georgia").font(.custom("Georgia", size: 17))
Text("Georgia Bold").font(.custom("Georgia-Bold", size: 24))

Best for:
 Long-form reading, news articles, blog content, editorial apps.

Times New Roman

The classic newspaper serif.

Variant

Font Name

Regular

TimesNewRomanPSMT

Italic

TimesNewRomanPS-ItalicMT

Bold

TimesNewRomanPS-BoldMT

Bold Italic

TimesNewRomanPS-BoldItalicMT

Text("Times").font(.custom("TimesNewRomanPSMT", size: 17))

Best for:
 Document viewers, academic apps, traditional editorial.

Palatino

A Renaissance-inspired serif with wide proportions and excellent readability.

Variant

Font Name

Roman

Palatino-Roman

Italic

Palatino-Italic

Bold

Palatino-Bold

Bold Italic

Palatino-BoldItalic

Text("Palatino").font(.custom("Palatino-Roman", size: 17))

Best for:
 Book reading apps, literary content, premium editorial.

Baskerville

A transitional serif with sharp contrast and refined details.

Variant

Font Name

Regular

Baskerville

Italic

Baskerville-Italic

Semibold

Baskerville-SemiBold

Semibold Italic

Baskerville-SemiBoldItalic

Bold

Baskerville-Bold

Bold Italic

Baskerville-BoldItalic

Text("Baskerville").font(.custom("Baskerville", size: 17))
Text("Baskerville Bold").font(.custom("Baskerville-Bold", size: 28))

Best for:
 Premium branding, luxury apps, classic literature, legal documents.

Didot

A high-contrast modern serif with dramatic thick-thin strokes.

Variant

Font Name

Regular

Didot

Italic

Didot-Italic

Bold

Didot-Bold

Text("DIDOT").font(.custom("Didot-Bold", size: 36))

Best for:
 Fashion, luxury branding, magazine covers, high-end product displays.

Bodoni 72

Another high-contrast modern serif. Multiple optical variants available.

Variant

Font Name

Book

BodoniSvtyTwoITCTT-Book

Book Italic

BodoniSvtyTwoITCTT-BookIta

Bold

BodoniSvtyTwoITCTT-Bold

OS Book

BodoniSvtyTwoOSITCTT-Book

OS Book Italic

BodoniSvtyTwoOSITCTT-BookIt

OS Bold

BodoniSvtyTwoOSITCTT-Bold

SC Book

BodoniSvtyTwoSCITCTT-Book

Ornaments

BodoniOrnamentsITCTT

Text("BODONI").font(.custom("BodoniSvtyTwoITCTT-Bold", size: 36))
Text("Small Caps").font(.custom("BodoniSvtyTwoSCITCTT-Book", size: 20))

Best for:
 Fashion editorial, luxury branding, poster-style headings, high-contrast display.

Charter

A highly legible serif designed for laser printers and screens.

Variant

Font Name

Roman

Charter-Roman

Italic

Charter-Italic

Bold

Charter-Bold

Bold Italic

Charter-BoldItalic

Black

Charter-Black

Black Italic

Charter-BlackItalic

Text("Charter").font(.custom("Charter-Roman", size: 17))

Best for:
 Long-form reading, documentation, e-books, RSS readers.

Iowan Old Style

A refined old-style serif optimized for extended reading on screens.

Variant

Font Name

Roman

IowanOldStyle-Roman

Italic

IowanOldStyle-Italic

Bold

IowanOldStyle-Bold

Bold Italic

IowanOldStyle-BoldItalic

Text("Iowan Old Style").font(.custom("IowanOldStyle-Roman", size: 17))

Best for:
 Apple Books-style reading, literary apps, elegant body text.

Cochin

An elegant old-style serif.

Variant

Font Name

Regular

Cochin

Italic

Cochin-Italic

Bold

Cochin-Bold

Bold Italic

Cochin-BoldItalic

Superclarendon

A bold slab serif with strong visual impact.

Variant

Font Name

Regular

Superclarendon-Regular

Italic

Superclarendon-Italic

Light

Superclarendon-Light

Light Italic

Superclarendon-LightItalic

Bold

Superclarendon-Bold

Bold Italic

Superclarendon-BoldItalic

Black

Superclarendon-Black

Black Italic

Superclarendon-BlackItalic

Monospaced Fonts

Courier New

The classic typewriter monospaced font.

Variant

Font Name

Regular

CourierNewPSMT

Italic

CourierNewPS-ItalicMT

Bold

CourierNewPS-BoldMT

Bold Italic

CourierNewPS-BoldItalicMT

Text("Courier").font(.custom("CourierNewPSMT", size: 14))

Best for:
 Retro terminal UIs, typewriter aesthetic, screenplay formatters.

Courier

The original Courier (slightly different from Courier New).

Variant

Font Name

Regular

Courier

Oblique

Courier-Oblique

Bold

Courier-Bold

Bold Oblique

Courier-BoldOblique

Menlo

A monospaced font based on Bitstream Vera Sans Mono. The default Xcode font before SF Mono.

Variant

Font Name

Regular

Menlo-Regular

Italic

Menlo-Italic

Bold

Menlo-Bold

Bold Italic

Menlo-BoldItalic

Text("func main()").font(.custom("Menlo-Regular", size: 14))
Text("// Bold comment").font(.custom("Menlo-Bold", size: 14))

Best for:
 Code display, terminal emulators, developer tools, log viewers.

American Typewriter

A monospaced font with typewriter charm and serifs.

Variant

Font Name

Regular

AmericanTypewriter

Light

AmericanTypewriter-Light

Semibold

AmericanTypewriter-Semibold

Bold

AmericanTypewriter-Bold

Condensed

AmericanTypewriter-Condensed

Condensed Light

AmericanTypewriter-CondensedLight

Condensed Bold

AmericanTypewriter-CondensedBold

Text("Typewriter").font(.custom("AmericanTypewriter", size: 17))

Best for:
 Note-taking apps, journal/diary UIs, retro aesthetics, creative writing tools.

Display and Decorative Fonts

Rockwell

A geometric slab serif with strong presence.

Variant

Font Name

Regular

Rockwell-Regular

Italic

Rockwell-Italic

Bold

Rockwell-Bold

Bold Italic

Rockwell-BoldItalic

Text("ROCKWELL").font(.custom("Rockwell-Bold", size: 32))

Best for:
 Bold headings, poster-style layouts, strong brand statements.

Copperplate

An all-caps engraved style font with small-cap lowercase.

Variant

Font Name

Regular

Copperplate

Light

Copperplate-Light

Bold

Copperplate-Bold

Text("COPPERPLATE").font(.custom("Copperplate-Bold", size: 24))

Best for:
 Formal invitations, certificates, luxury branding, restaurant menus.

Papyrus

A distressed, hand-drawn style font.

Variant

Font Name

Regular

Papyrus

Condensed

Papyrus-Condensed

Text("Papyrus").font(.custom("Papyrus", size: 20))

Best for:
 Themed apps (ancient, natural), generally avoid for professional UIs.

Marker Felt

A felt-tip marker style font.

Variant

Font Name

Thin

MarkerFelt-Thin

Wide

MarkerFelt-Wide

Text("Marker Felt").font(.custom("MarkerFelt-Thin", size: 20))

Best for:
 Whiteboard UIs, sketching apps, children's content.

Chalkboard SE

A clean chalkboard-style handwriting font.

Variant

Font Name

Regular

ChalkboardSE-Regular

Light

ChalkboardSE-Light

Bold

ChalkboardSE-Bold

Text("Chalkboard").font(.custom("ChalkboardSE-Regular", size: 17))

Best for:
 Education apps, children's apps, informal notes.

Chalkduster

A rougher chalkboard-style font.

Variant

Font Name

Regular

Chalkduster

Text("Chalkduster").font(.custom("Chalkduster", size: 20))

Noteworthy

A casual handwriting font.

Variant

Font Name

Light

Noteworthy-Light

Bold

Noteworthy-Bold

Text("Noteworthy").font(.custom("Noteworthy-Light", size: 17))

Best for:
 Personal notes, diary entries, sticky note UIs.

Zapfino

An elaborate calligraphic script with extreme flourishes.

Variant

Font Name

Regular

Zapfino

Text("Zapfino").font(.custom("Zapfino", size: 24))

Best for:
 Decorative headers only, wedding apps, formal invitations (use sparingly).

Party LET

A festive, playful display font.

Variant

Font Name

Plain

PartyLetPlain

Savoye LET

An elegant script font.

Variant

Font Name

Plain

SavoyeLetPlain

Text("Savoye LET").font(.custom("SavoyeLetPlain", size: 28))

Best for:
 Elegant signatures, wedding invitations, formal flourishes.

Script and Handwriting Fonts

Snell Roundhand

A flowing copperplate script with graceful strokes.

Variant

Font Name

Regular

SnellRoundhand

Bold

SnellRoundhand-Bold

Black

SnellRoundhand-Black

Text("Elegant Script").font(.custom("SnellRoundhand", size: 24))

Best for:
 Formal invitations, signatures, elegant accents.

Bradley Hand

A casual handwriting style.

Variant

Font Name

Bold

BradleyHandITCTT-Bold

Text("Handwritten").font(.custom("BradleyHandITCTT-Bold", size: 17))

Best for:
 Personal touches, note-style UIs, casual annotations.

Kefa

Variant

Font Name

Regular

Kefa-Regular

Kohinoor Telugu

Variant

Font Name

Regular

KohinoorTelugu-Regular

Medium

KohinoorTelugu-Medium

Light

KohinoorTelugu-Light

Additional Built-in Fonts

Symbol / Dingbat Fonts

Font Family

Font Name

Description

Symbol

Symbol

Greek and math symbols

Zapf Dingbats

ZapfDingbatsITC

Decorative symbols

Additional Sans-Serif

Variant

Font Name

Euphemia UCAS

EuphemiaUCAS

Euphemia UCAS Bold

EuphemiaUCAS-Bold

Euphemia UCAS Italic

EuphemiaUCAS-Italic

Galvji

Galvji

Galvji Bold

Galvji-Bold

Galvji Bold Oblique

Galvji-BoldOblique

Galvji Oblique

Galvji-Oblique

Grantha Sangam MN

GranthaSangamMN-Regular

Grantha Sangam MN Bold

GranthaSangamMN-Bold

Hoefler Text

HoeflerText-Regular

Hoefler Text Italic

HoeflerText-Italic

Hoefler Text Bold

HoeflerText-Black

Hoefler Text Bold Italic

HoeflerText-BlackItalic

Kailasa

Kailasa

Kailasa Bold

Kailasa-Bold

Khmer Sangam MN

KhmerSangamMN

Lao Sangam MN

LaoSangamMN

Malayalam Sangam MN

MalayalamSangamMN

Malayalam Sangam MN Bold

MalayalamSangamMN-Bold

Myanmar Sangam MN

MyanmarSangamMN

Myanmar Sangam MN Bold

MyanmarSangamMN-Bold

Noto Nastaliq Urdu

NotoNastaliqUrdu

Noto Nastaliq Urdu Bold

NotoNastaliqUrdu-Bold

Noto Sans Kannada

NotoSansKannada-Regular

Noto Sans Kannada Bold

NotoSansKannada-Bold

Noto Sans Kannada Light

NotoSansKannada-Light

Noto Sans Myanmar

NotoSansMyanmar-Regular

Noto Sans Myanmar Bold

NotoSansMyanmar-Bold

Noto Sans Myanmar Light

NotoSansMyanmar-Light

Noto Sans Oriya

NotoSansOriya

Noto Sans Oriya Bold

NotoSansOriya-Bold

Sinhala Sangam MN

SinhalaSangamMN

Sinhala Sangam MN Bold

SinhalaSangamMN-Bold

Tamil Sangam MN

TamilSangamMN

Tamil Sangam MN Bold

TamilSangamMN-Bold

Thonburi

Thonburi

Thonburi Light

Thonburi-Light

Thonburi Bold

Thonburi-Bold

International and Multi-script Fonts

Chinese

Variant

Font Name

Script

PingFang SC Regular

PingFangSC-Regular

Simplified Chinese

PingFang SC Medium

PingFangSC-Medium

Simplified Chinese

PingFang SC Semibold

PingFangSC-Semibold

Simplified Chinese

PingFang SC Light

PingFangSC-Light

Simplified Chinese

PingFang SC Thin

PingFangSC-Thin

Simplified Chinese

PingFang SC Ultralight

PingFangSC-Ultralight

Simplified Chinese

PingFang TC Regular

PingFangTC-Regular

Traditional Chinese

PingFang TC Medium

PingFangTC-Medium

Traditional Chinese

PingFang TC Semibold

PingFangTC-Semibold

Traditional Chinese

PingFang TC Light

PingFangTC-Light

Traditional Chinese

PingFang TC Thin

PingFangTC-Thin

Traditional Chinese

PingFang TC Ultralight

PingFangTC-Ultralight

Traditional Chinese

PingFang HK Regular

PingFangHK-Regular

Hong Kong Chinese

PingFang HK Medium

PingFangHK-Medium

Hong Kong Chinese

PingFang HK Semibold

PingFangHK-Semibold

Hong Kong Chinese

PingFang HK Light

PingFangHK-Light

Hong Kong Chinese

PingFang HK Thin

PingFangHK-Thin

Hong Kong Chinese

PingFang HK Ultralight

PingFangHK-Ultralight

Hong Kong Chinese

Japanese

Variant

Font Name

Hiragino Sans W3

HiraginoSans-W3

Hiragino Sans W6

HiraginoSans-W6

Hiragino Sans W7

HiraginoSans-W7

Hiragino Mincho ProN W3

HiraMinProN-W3

Hiragino Mincho ProN W6

HiraMinProN-W6

Text("Japanese text").font(.custom("HiraginoSans-W3", size: 17))

Korean

Variant

Font Name

Apple SD Gothic Neo Regular

AppleSDGothicNeo-Regular

Apple SD Gothic Neo Thin

AppleSDGothicNeo-Thin

Apple SD Gothic Neo UltraLight

AppleSDGothicNeo-UltraLight

Apple SD Gothic Neo Light

AppleSDGothicNeo-Light

Apple SD Gothic Neo Medium

AppleSDGothicNeo-Medium

Apple SD Gothic Neo Semibold

AppleSDGothicNeo-SemiBold

Apple SD Gothic Neo Bold

AppleSDGothicNeo-Bold

Text("Korean text").font(.custom("AppleSDGothicNeo-Regular", size: 17))

Arabic

Variant

Font Name

Geeza Pro Regular

GeezaPro

Geeza Pro Bold

GeezaPro-Bold

Mishafi Regular

DiwanMishafi

Baghdad Regular

Baghdad

Farah

Farah

Damascus

Damascus

Damascus Light

DamascusLight

Damascus Medium

DamascusMedium

Damascus Semibold

DamascusSemiBold

Damascus Bold

DamascusBold

Devanagari (Hindi)

Variant

Font Name

Kohinoor Devanagari Regular

KohinoorDevanagari-Regular

Kohinoor Devanagari Light

KohinoorDevanagari-Light

Kohinoor Devanagari Semibold

KohinoorDevanagari-Semibold

Devanagari Sangam MN

DevanagariSangamMN

Devanagari Sangam MN Bold

DevanagariSangamMN-Bold

Bangla

Variant

Font Name

Kohinoor Bangla Regular

KohinoorBangla-Regular

Kohinoor Bangla Light

KohinoorBangla-Light

Kohinoor Bangla Semibold

KohinoorBangla-Semibold

Gujarati

Variant

Font Name

Kohinoor Gujarati Regular

KohinoorGujarati-Regular

Kohinoor Gujarati Light

KohinoorGujarati-Light

Kohinoor Gujarati Bold

KohinoorGujarati-Bold

Gujarati Sangam MN

GujaratiSangamMN

Gujarati Sangam MN Bold

GujaratiSangamMN-Bold

Telugu

Variant

Font Name

Kohinoor Telugu Regular

KohinoorTelugu-Regular

Kohinoor Telugu Medium

KohinoorTelugu-Medium

Kohinoor Telugu Light

KohinoorTelugu-Light

Gurmukhi (Punjabi)

Variant

Font Name

Mukta Mahee Regular

MuktaMahee-Regular

Mukta Mahee Light

MuktaMahee-Light

Mukta Mahee Bold

MuktaMahee-Bold

Gurmukhi MN

GurmukhiMN

Gurmukhi MN Bold

GurmukhiMN-Bold

3. Google Fonts -- Top 100 for iOS

These are the most popular Google Fonts used in iOS apps. To use any of these, you must
download the font files and add them to your Xcode project (see Section 4).

Sans-Serif

#

Font Name

Weights Available

Best Use Case

1

Inter

100-900

UI text, dashboards, SaaS

2

Roboto

100, 300, 400, 500, 700, 900

Material Design, Android parity

3

Open Sans

300-800

Universal body text

4

Lato

100, 300, 400, 700, 900

Friendly body text, corporate

5

Montserrat

100-900

Modern headings, marketing

6

Poppins

100-900

SaaS, startup, geometric UI

7

Nunito

200-900

Rounded, friendly, children's apps

8

Raleway

100-900

Elegant headings, fashion

9

Source Sans 3

200-900

Technical docs, code-adjacent text

10

Work Sans

100-900

Clean UI, editorial

11

DM Sans

100-900

Minimal UI, dashboard

12

Manrope

200-800

Tech products, developer tools

13

Plus Jakarta Sans

200-800

Fintech, professional

14

Space Grotesk

300-700

Tech, developer, coding apps

15

Outfit

100-900

Modern branding, startup

16

Sora

100-800

Futuristic, crypto, web3

17

Urbanist

100-900

Modern geometric, luxury tech

18

Lexend

100-900

Accessibility, dyslexia-friendly

19

Albert Sans

100-900

Geometric, versatile UI

20

Figtree

300-900

Friendly, approachable UI

21

Geist

100-900

Vercel-style, developer UI

22

Satoshi

300-900

Minimal, modern branding

23

Nunito Sans

200-900

UI text, clean dashboards

24

Karla

200-800

Grotesque, editorial

25

Rubik

300-900

Rounded, playful

26

Barlow

100-900

Industrial, technical

27

Mulish

200-900

Clean, minimal

28

Quicksand

300-700

Rounded, friendly

29

Cabin

400-700

Humanist, warm

30

Josefin Sans

100-700

Elegant, geometric

31

PT Sans

400, 700

Universal text, multilingual

32

Noto Sans

100-900

Global multilingual support

33

Overpass

100-900

Highway signage inspired, clean

34

IBM Plex Sans

100-700

Enterprise, corporate, IBM design

35

Red Hat Display

300-900

Open source branding

36

Exo 2

100-900

Futuristic, geometric

37

Archivo

100-900

Bold headings, editorial

38

Hind

300-700

Devanagari + Latin body text

39

Public Sans

100-900

Government, accessible

40

General Sans

200-700

Modern grotesque, branding

Serif

#

Font Name

Weights Available

Best Use Case

41

Playfair Display

400-900

Editorial headings, magazine

42

Merriweather

300, 400, 700, 900

Long-form reading, blogs

43

Lora

400-700

Book text, literary

44

Source Serif 4

200-900

Technical docs, paired with Source Sans

45

Crimson Text

400, 600, 700

Book text, classic reading

46

Libre Baskerville

400, 700

Elegant body text, traditional

47

EB Garamond

400-800

Book typography, literary

48

Cormorant Garamond

300-700

High fashion, luxury headings

49

DM Serif Display

400

Bold editorial headings

50

Bitter

100-900

Screen reading, warm serif

51

Noto Serif

100-900

Global multilingual serif

52

PT Serif

400, 700

Multilingual reading

53

Spectral

200-800

Long-form, screen-optimized

54

Vollkorn

400-900

Warm, organic reading

55

Fraunces

100-900

Playful serif, retro

56

Instrument Serif

400

Minimal editorial

57

Newsreader

200-800

News, journalism

Monospaced

#

Font Name

Weights Available

Best Use Case

58

Fira Code

300-700

Code editor with ligatures

59

JetBrains Mono

100-800

IDE, code editor, terminal

60

Source Code Pro

200-900

Code display, terminal

61

IBM Plex Mono

100-700

Enterprise code, terminal

62

Space Mono

400, 700

Futuristic, retro-tech

63

Roboto Mono

100-700

Data tables, code blocks

64

Inconsolata

200-900

Code display, clean mono

65

Ubuntu Mono

400, 700

Linux-style terminal

66

Cascadia Code

200-700

Windows Terminal-style

67

Geist Mono

100-900

Vercel developer tools

68

Anonymous Pro

400, 700

Coding, terminal

69

Overpass Mono

300-700

Data, tables

70

Red Hat Mono

300-700

Enterprise code

Display

#

Font Name

Weights Available

Best Use Case

71

Bebas Neue

400

Bold headings, posters

72

Oswald

200-700

Condensed headings, news

73

Anton

400

Impact headings, bold statements

74

Archivo Black

400

Ultra bold display

75

Righteous

400

Retro, rounded display

76

Fredoka

300-700

Playful, children's content

77

Lilita One

400

Fun, casual headings

78

Passion One

400, 700, 900

Sports, energetic

79

Bungee

400

Signage, athletic

80

Abril Fatface

400

High-contrast display

81

Alfa Slab One

400

Slab serif display

82

Lobster

400

Retro script display

83

Permanent Marker

400

Hand-drawn, casual

84

Bangers

400

Comic book, energetic

85

Staatliches

400

Display, condensed sans

86

Comfortaa

300-700

Rounded, futuristic

87

Russo One

400

Tech, gaming

88

Press Start 2P

400

Pixel art, retro gaming

89

Orbitron

400-900

Sci-fi, space, futuristic

90

Teko

300-700

Sports, condensed display

Handwriting and Script

#

Font Name

Weights Available

Best Use Case

91

Dancing Script

400-700

Casual elegance, invitations

92

Pacifico

400

Retro surf, casual branding

93

Caveat

400-700

Handwritten notes, annotations

94

Sacramento

400

Elegant script, signatures

95

Great Vibes

400

Formal calligraphy

96

Satisfy

400

Retro script

97

Kalam

300, 400, 700

Informal handwriting, notes

98

Patrick Hand

400

Casual handwriting

99

Indie Flower

400

Playful handwriting, fun

100

Amatic SC

400, 700

Tall, narrow handwriting

4. How to Add Custom Fonts to iOS

Step 1: Obtain Font Files

Download
.ttf
 (TrueType) or
.otf
 (OpenType) font files. For Google Fonts,
download from https://fonts.google.com or use a package manager.

Step 2: Add Files to Xcode Project

Drag the font files into your Xcode project navigator.
In the dialog that appears, check
"Copy items if needed"
.
Ensure
"Add to targets"
 has your app target checked.
Verify the files appear under
Build Phases > Copy Bundle Resources
.

Step 3: Register Fonts in Info.plist

Add the font file names to your
Info.plist
:

<key>UIAppFonts</key>
<array>
    <string>Inter-Regular.ttf</string>
    <string>Inter-Medium.ttf</string>
    <string>Inter-Bold.ttf</string>
    <string>PlayfairDisplay-Bold.ttf</string>
    <string>PlayfairDisplay-Regular.ttf</string>
</array>

Or in the Xcode Info tab, add a row:
- Key:
Fonts provided by application

- Type: Array
- Items: each font filename (including extension)

Step 4: Find the Exact Font Name

The filename is NOT always the font name. Use this code to discover exact PostScript names:

// Run this once at app launch to print all available fonts
for family in UIFont.familyNames.sorted() {
    print("Family: \(family)")
    for name in UIFont.fontNames(forFamilyName: family) {
        print("  -- \(name)")
    }
}

Or find a specific family:

// Check a specific font family
let names = UIFont.fontNames(forFamilyName: "Inter")
print(names) // ["Inter-Regular", "Inter-Medium", "Inter-Bold", ...]

Step 5: Use in SwiftUI

// Basic usage
Text("Custom Font").font(.custom("Inter-Regular", size: 17))
Text("Bold Custom").font(.custom("Inter-Bold", size: 24))

// With Dynamic Type support (RECOMMENDED)
Text("Dynamic Type").font(.custom("Inter-Regular", size: 17, relativeTo: .body))
Text("Dynamic Title").font(.custom("Inter-Bold", size: 28, relativeTo: .title))
Text("Dynamic Caption").font(.custom("Inter-Regular", size: 12, relativeTo: .caption))

// Fixed size (does not scale with Dynamic Type)
Text("Fixed Size").font(.custom("Inter-Regular", fixedSize: 14))

Step 6: Use in UIKit

// Basic usage
let font = UIFont(name: "Inter-Regular", size: 17)

// With Dynamic Type metrics
let customFont = UIFont(name: "Inter-Regular", size: 17)!
let scaledFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: customFont)
label.font = scaledFont
label.adjustsFontForContentSizeCategory = true

Dynamic Type with @ScaledMetric

Use
@ScaledMetric
 for custom font sizes that scale with Dynamic Type settings:

struct ContentView: View {
    @ScaledMetric(relativeTo: .body) var bodySize: CGFloat = 17
    @ScaledMetric(relativeTo: .title) var titleSize: CGFloat = 28
    @ScaledMetric(relativeTo: .caption) var captionSize: CGFloat = 12
    @ScaledMetric var iconSize: CGFloat = 24

    var body: some View {
        VStack {
            Text("Title")
                .font(.custom("PlayfairDisplay-Bold", size: titleSize))
            Text("Body text here")
                .font(.custom("Inter-Regular", size: bodySize))
            Text("Caption")
                .font(.custom("Inter-Regular", size: captionSize))
            Image(systemName: "star.fill")
                .font(.system(size: iconSize))
        }
    }
}

Complete Custom Font Integration Example

// App entry point - register fonts
@main
struct MyApp: App {
    init() {
        // Fonts are auto-registered via Info.plist
        // But you can verify they loaded:
        #if DEBUG
        if UIFont(name: "Inter-Regular", size: 17) == nil {
            print("WARNING: Inter-Regular font not found. Check Info.plist and bundle.")
        }
        #endif
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Swift Package Manager Font Loading

If your fonts come from a Swift Package:

import SwiftUI

extension Font {
    static func registerFontsFromBundle(bundle: Bundle) {
        let fontURLs = bundle.urls(forResourcesWithExtension: "ttf", subdirectory: nil) ?? []
        let otfURLs = bundle.urls(forResourcesWithExtension: "otf", subdirectory: nil) ?? []

        for url in fontURLs + otfURLs {
            CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
        }
    }
}

Troubleshooting Custom Fonts

Problem

Solution

Font not appearing

Check Info.plist entry matches exact filename

Wrong weight showing

Use PostScript name, not display name

Font not in bundle

Verify target membership in File Inspector

Dynamic Type not working

Use relativeTo: parameter in Font.custom()

Font looks different on device

Simulator and device may render differently; always test both

5. Font Pairing Recommendations

Fifteen proven font pairings for iOS apps. Each pairing includes a heading font,
a body font, the design context, and ready-to-use SwiftUI code.

Pairing 1: SF Pro Display + SF Pro Text

Style:
 System default, clean, Apple-native

Use case:
 Any iOS app that wants to feel native and polished

VStack(alignment: .leading, spacing: 8) {
    Text("Welcome Back")
        .font(.system(size: 34, weight: .bold))
    Text("Here is what happened while you were away. Your projects have new updates and your team left comments.")
        .font(.system(size: 17, weight: .regular))
        .foregroundStyle(.secondary)
}

Pairing 2: Playfair Display + Source Sans 3

Style:
 Editorial, magazine

Use case:
 News apps, editorial content, blog readers

VStack(alignment: .leading, spacing: 8) {
    Text("The Art of Typography")
        .font(.custom("PlayfairDisplay-Bold", size: 32, relativeTo: .largeTitle))
    Text("Typography is the art and technique of arranging type to make written language legible, readable, and appealing when displayed.")
        .font(.custom("SourceSans3-Regular", size: 17, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 3: Montserrat + Lora

Style:
 Modern heading with classic body

Use case:
 Portfolio apps, creative agencies, lifestyle brands

VStack(alignment: .leading, spacing: 8) {
    Text("CREATIVE STUDIO")
        .font(.custom("Montserrat-Bold", size: 28, relativeTo: .title))
        .tracking(2)
    Text("We craft digital experiences that blend innovation with timeless design principles.")
        .font(.custom("Lora-Regular", size: 17, relativeTo: .body))
}

Pairing 4: Poppins + Inter

Style:
 SaaS, modern dashboard

Use case:
 Productivity apps, dashboards, admin panels, B2B tools

VStack(alignment: .leading, spacing: 8) {
    Text("Dashboard Overview")
        .font(.custom("Poppins-SemiBold", size: 24, relativeTo: .title2))
    Text("Your key metrics are performing above average this quarter with a 23% increase in engagement.")
        .font(.custom("Inter-Regular", size: 15, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 5: Bebas Neue + Roboto

Style:
 Bold impact with clean body

Use case:
 Sports apps, fitness trackers, event apps, bold branding

VStack(alignment: .leading, spacing: 4) {
    Text("GAME DAY")
        .font(.custom("BebasNeue-Regular", size: 48, relativeTo: .largeTitle))
    Text("Get ready for tonight's matchup. Here are the stats, lineups, and predictions you need.")
        .font(.custom("Roboto-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 6: DM Serif Display + DM Sans

Style:
 Luxury, minimal elegance

Use case:
 Premium products, luxury e-commerce, high-end hospitality

VStack(alignment: .leading, spacing: 8) {
    Text("Curated Collection")
        .font(.custom("DMSerifDisplay-Regular", size: 32, relativeTo: .largeTitle))
    Text("Each piece in our collection has been carefully selected for its craftsmanship and timeless appeal.")
        .font(.custom("DMSans-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 7: Space Grotesk + Inter

Style:
 Tech, developer-focused

Use case:
 Developer tools, coding apps, API documentation

VStack(alignment: .leading, spacing: 8) {
    Text("API Reference")
        .font(.custom("SpaceGrotesk-Bold", size: 28, relativeTo: .title))
    Text("Explore our comprehensive API documentation with interactive examples and detailed guides.")
        .font(.custom("Inter-Regular", size: 15, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 8: Plus Jakarta Sans + Source Serif 4

Style:
 Professional, trustworthy

Use case:
 Fintech, banking, insurance, professional services

VStack(alignment: .leading, spacing: 8) {
    Text("Financial Summary")
        .font(.custom("PlusJakartaSans-Bold", size: 24, relativeTo: .title2))
    Text("Your portfolio has grown 12.4% this quarter, outperforming the market benchmark by 3.2 percentage points.")
        .font(.custom("SourceSerif4-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 9: Outfit + Lato

Style:
 Startup, approachable

Use case:
 Startup landing pages, onboarding flows, consumer apps

VStack(alignment: .leading, spacing: 8) {
    Text("Get Started")
        .font(.custom("Outfit-SemiBold", size: 28, relativeTo: .title))
    Text("Set up your profile in just a few steps and start connecting with people who share your interests.")
        .font(.custom("Lato-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 10: Oswald + Open Sans

Style:
 News, content-heavy

Use case:
 News readers, content aggregators, media apps

VStack(alignment: .leading, spacing: 8) {
    Text("BREAKING NEWS")
        .font(.custom("Oswald-Bold", size: 28, relativeTo: .title))
    Text("Markets rally as economic indicators show stronger-than-expected growth in the third quarter.")
        .font(.custom("OpenSans-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 11: New York + SF Pro

Style:
 Apple editorial

Use case:
 Apple News-style apps, premium editorial, literary content

VStack(alignment: .leading, spacing: 8) {
    Text("The Future of Design")
        .font(.system(size: 32, weight: .bold, design: .serif))
    Text("As technology evolves, so does the language of design. New tools and paradigms are reshaping how we create.")
        .font(.system(size: 17, weight: .regular))
        .foregroundStyle(.secondary)
}

Pairing 12: Archivo Black + Work Sans

Style:
 Bold, sporty

Use case:
 Sports brands, fitness apps, bold product pages

VStack(alignment: .leading, spacing: 4) {
    Text("PUSH LIMITS")
        .font(.custom("ArchivoBlack-Regular", size: 36, relativeTo: .largeTitle))
    Text("Track your workouts, set new records, and compete with friends in weekly challenges.")
        .font(.custom("WorkSans-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 13: Cormorant Garamond + Montserrat

Style:
 High fashion, luxury

Use case:
 Fashion apps, luxury brands, art galleries, premium retail

VStack(alignment: .leading, spacing: 12) {
    Text("Autumn Collection")
        .font(.custom("CormorantGaramond-SemiBold", size: 36, relativeTo: .largeTitle))
    Text("EXPLORE THE LATEST ARRIVALS")
        .font(.custom("Montserrat-Medium", size: 12, relativeTo: .caption))
        .tracking(3)
        .foregroundStyle(.secondary)
}

Pairing 14: Fredoka + Nunito

Style:
 Kids, playful

Use case:
 Children's apps, educational games, family content

VStack(alignment: .leading, spacing: 8) {
    Text("Let's Learn!")
        .font(.custom("Fredoka-SemiBold", size: 32, relativeTo: .largeTitle))
    Text("Choose a fun activity below and start your learning adventure today.")
        .font(.custom("Nunito-Regular", size: 17, relativeTo: .body))
        .foregroundStyle(.secondary)
}

Pairing 15: Manrope + Merriweather

Style:
 Blog, long-form reading

Use case:
 Blog readers, RSS apps, knowledge bases, documentation

VStack(alignment: .leading, spacing: 8) {
    Text("Understanding Swift Concurrency")
        .font(.custom("Manrope-Bold", size: 24, relativeTo: .title2))
    Text("Swift concurrency introduces structured approaches to writing asynchronous code, making it safer and more readable than traditional callback patterns.")
        .font(.custom("Merriweather-Regular", size: 16, relativeTo: .body))
        .foregroundStyle(.secondary)
}

6. Font Management Utilities

FontManager: Register Custom Fonts Programmatically

import SwiftUI
import CoreText

final class FontManager {
    static let shared = FontManager()

    private var registeredFonts: Set<String> = []

    private init() {}

    /// Register all custom fonts from the main bundle.
    /// Call this in your App init or AppDelegate.
    func registerAllFonts() {
        registerFonts(from: .main, extensions: ["ttf", "otf"])
    }

    /// Register fonts from a specific bundle (useful for SPM packages).
    func registerFonts(from bundle: Bundle, extensions: [String] = ["ttf", "otf"]) {
        for ext in extensions {
            guard let urls = bundle.urls(forResourcesWithExtension: ext, subdirectory: nil) else {
                continue
            }
            for url in urls {
                registerFont(at: url)
            }
        }
    }

    /// Register a single font file by URL.
    func registerFont(at url: URL) {
        let fontName = url.lastPathComponent
        guard !registeredFonts.contains(fontName) else { return }

        var error: Unmanaged<CFError>?
        let success = CTFontManagerRegisterFontsForURL(url as CFURL, .process, &error)

        if success {
            registeredFonts.insert(fontName)
        } else if let error = error?.takeRetainedValue() {
            print("Failed to register font \(fontName): \(error)")
        }
    }

    /// Print all available font families and their font names (debug utility).
    func printAllFonts() {
        for family in UIFont.familyNames.sorted() {
            print("Family: \(family)")
            for name in UIFont.fontNames(forFamilyName: family).sorted() {
                print("  \(name)")
            }
        }
    }

    /// Check if a specific font is available.
    func isFontAvailable(_ fontName: String) -> Bool {
        return UIFont(name: fontName, size: 12) != nil
    }
}

App-Specific Type Scale Extension

Define a consistent type scale for your entire app:

import SwiftUI

extension Font {
    // MARK: - Display
    static let appDisplayLarge = Font.custom("Inter-Bold", size: 40, relativeTo: .largeTitle)
    static let appDisplayMedium = Font.custom("Inter-Bold", size: 34, relativeTo: .largeTitle)
    static let appDisplaySmall = Font.custom("Inter-SemiBold", size: 28, relativeTo: .title)

    // MARK: - Headings
    static let appHeading1 = Font.custom("Inter-Bold", size: 24, relativeTo: .title2)
    static let appHeading2 = Font.custom("Inter-SemiBold", size: 20, relativeTo: .title3)
    static let appHeading3 = Font.custom("Inter-SemiBold", size: 17, relativeTo: .headline)

    // MARK: - Body
    static let appBodyLarge = Font.custom("Inter-Regular", size: 17, relativeTo: .body)
    static let appBody = Font.custom("Inter-Regular", size: 15, relativeTo: .subheadline)
    static let appBodySmall = Font.custom("Inter-Regular", size: 13, relativeTo: .footnote)

    // MARK: - Labels
    static let appLabel = Font.custom("Inter-Medium", size: 14, relativeTo: .subheadline)
    static let appLabelSmall = Font.custom("Inter-Medium", size: 12, relativeTo: .caption)

    // MARK: - Caption
    static let appCaption = Font.custom("Inter-Regular", size: 12, relativeTo: .caption)
    static let appCaptionSmall = Font.custom("Inter-Regular", size: 11, relativeTo: .caption2)

    // MARK: - Monospaced
    static let appCode = Font.custom("JetBrainsMono-Regular", size: 14, relativeTo: .body)
    static let appCodeSmall = Font.custom("JetBrainsMono-Regular", size: 12, relativeTo: .caption)

    // MARK: - Special
    static let appButton = Font.custom("Inter-SemiBold", size: 16, relativeTo: .body)
    static let appTabBar = Font.custom("Inter-Medium", size: 10, relativeTo: .caption2)
    static let appBadge = Font.custom("Inter-Bold", size: 11, relativeTo: .caption2)
}

Usage:

VStack(alignment: .leading, spacing: 8) {
    Text("Page Title").font(.appHeading1)
    Text("Body content goes here.").font(.appBody)
    Text("12:34 PM").font(.appCaption)
    Text("let x = 42").font(.appCode)
}

Font Preview View

A debug view that displays all registered fonts:

import SwiftUI

struct FontPreviewView: View {
    @State private var families: [String] = []
    @State private var searchText = ""
    @State private var previewText = "The quick brown fox jumps over the lazy dog"
    @State private var previewSize: CGFloat = 17

    var filteredFamilies: [String] {
        if searchText.isEmpty {
            return families
        }
        return families.filter { $0.localizedCaseInsensitiveContains(searchText) }
    }

    var body: some View {
        NavigationStack {
            List {
                Section {
                    TextField("Preview text", text: $previewText)
                    Stepper("Size: \(Int(previewSize))pt", value: $previewSize, in: 8...72)
                }

                ForEach(filteredFamilies, id: \.self) { family in
                    Section(header: Text(family)) {
                        ForEach(UIFont.fontNames(forFamilyName: family).sorted(), id: \.self) { name in
                            VStack(alignment: .leading, spacing: 4) {
                                Text(previewText)
                                    .font(.custom(name, size: previewSize))
                                Text(name)
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                            }
                            .padding(.vertical, 2)
                        }
                    }
                }
            }
            .navigationTitle("Font Preview")
            .searchable(text: $searchText, prompt: "Search fonts...")
            .onAppear {
                families = UIFont.familyNames.sorted()
            }
        }
    }
}

#Preview {
    FontPreviewView()
}

Dynamic Type Helper

Ensure custom fonts respect the user's Dynamic Type preferences:

import SwiftUI

struct DynamicTypeFont: ViewModifier {
    let fontName: String
    let baseSize: CGFloat
    let textStyle: Font.TextStyle

    @ScaledMetric private var scaledSize: CGFloat

    init(fontName: String, baseSize: CGFloat, textStyle: Font.TextStyle = .body) {
        self.fontName = fontName
        self.baseSize = baseSize
        self.textStyle = textStyle
        self._scaledSize = ScaledMetric(wrappedValue: baseSize, relativeTo: textStyle)
    }

    func body(content: Content) -> some View {
        content.font(.custom(fontName, size: scaledSize))
    }
}

extension View {
    func dynamicFont(_ name: String, size: CGFloat, relativeTo style: Font.TextStyle = .body) -> some View {
        modifier(DynamicTypeFont(fontName: name, baseSize: size, textStyle: style))
    }
}

// Usage:
// Text("Hello").dynamicFont("Inter-Regular", size: 17, relativeTo: .body)

List All Device Fonts (Utility Function)

import UIKit

func getAllFonts() -> [(family: String, fonts: [String])] {
    UIFont.familyNames.sorted().map { family in
        (family: family, fonts: UIFont.fontNames(forFamilyName: family).sorted())
    }
}

func findFont(containing query: String) -> [String] {
    var results: [String] = []
    for family in UIFont.familyNames {
        for name in UIFont.fontNames(forFamilyName: family) {
            if name.localizedCaseInsensitiveContains(query) {
                results.append(name)
            }
        }
    }
    return results.sorted()
}

// Usage:
// let allFonts = getAllFonts()
// let interFonts = findFont(containing: "Inter")

7. Variable Fonts

What Are Variable Fonts?

A variable font is a single font file that contains an entire family of variations along
one or more design axes (weight, width, slant, optical size). Instead of shipping separate
files for Regular, Medium, Bold, and so on, a single variable font file can interpolate
smoothly between any values on its defined axes.

Benefits

Single file
: One
.ttf
 replaces 10-20 individual weight files
Smooth interpolation
: Animate between weights or widths fluidly
Smaller bundle size
: Typically smaller than the equivalent collection of static fonts
Fine-grained control
: Access any weight (e.g., 450, 550) not just named stops
Animation
: Smoothly transition font weight, width, or slant

Common Axes

Axis Code

Name

Typical Range

Description

wght

Weight

100-900

Thin to Black

wdth

Width

75-125

Condensed to Expanded

slnt

Slant

-12 to 0

Upright to slanted

ital

Italic

0 or 1

Roman or Italic

opsz

Optical Size

8-144

Small text to display

Using Variable Fonts in SwiftUI

SwiftUI does not natively support setting arbitrary axis values. You need to drop down
to
UIFont
 with font descriptors and bridge back to SwiftUI.

import SwiftUI
import UIKit

extension Font {
    /// Create a font from a variable font with a specific weight value.
    /// - Parameters:
    ///   - name: The PostScript name of the variable font
    ///   - size: Point size
    ///   - weight: Weight value (100-900, where 400 = regular, 700 = bold)
    static func variable(_ name: String, size: CGFloat, weight: CGFloat = 400) -> Font {
        let descriptor = UIFontDescriptor(fontAttributes: [
            .name: name,
            kCTFontVariationAttribute as UIFontDescriptor.AttributeName: [
                // Weight axis tag: 2003265652 = 'wght'
                2003265652: weight
            ]
        ])
        let uiFont = UIFont(descriptor: descriptor, size: size)
        return Font(uiFont)
    }

    /// Create a font from a variable font with weight and width axes.
    static func variable(
        _ name: String,
        size: CGFloat,
        weight: CGFloat = 400,
        width: CGFloat = 100
    ) -> Font {
        let descriptor = UIFontDescriptor(fontAttributes: [
            .name: name,
            kCTFontVariationAttribute as UIFontDescriptor.AttributeName: [
                2003265652: weight,  // wght
                2003072104: width    // wdth
            ]
        ])
        let uiFont = UIFont(descriptor: descriptor, size: size)
        return Font(uiFont)
    }
}

Usage:

VStack(spacing: 8) {
    Text("Weight 300").font(.variable("Inter", size: 17, weight: 300))
    Text("Weight 400").font(.variable("Inter", size: 17, weight: 400))
    Text("Weight 500").font(.variable("Inter", size: 17, weight: 500))
    Text("Weight 600").font(.variable("Inter", size: 17, weight: 600))
    Text("Weight 700").font(.variable("Inter", size: 17, weight: 700))
}

Animating Variable Font Axes

You can animate weight or width changes for smooth transitions:

import SwiftUI

struct AnimatedWeightText: View {
    @State private var weight: CGFloat = 400
    let fontName: String
    let text: String

    var body: some View {
        VStack(spacing: 20) {
            Text(text)
                .font(.variable(fontName, size: 32, weight: weight))
                .animation(.easeInOut(duration: 0.3), value: weight)

            Slider(value: $weight, in: 100...900, step: 1)
                .padding(.horizontal)

            Text("Weight: \(Int(weight))")
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }
}

Axis Tags Reference

When working with variable fonts programmatically, you need numeric axis tags.
These are computed from the 4-character axis tag string:

func axisTag(from string: String) -> Int {
    let chars = Array(string.utf8)
    guard chars.count == 4 else { return 0 }
    return Int(chars[0]) << 24 | Int(chars[1]) << 16 | Int(chars[2]) << 8 | Int(chars[3])
}

// Common axis tags:
// axisTag(from: "wght") = 2003265652
// axisTag(from: "wdth") = 2003072104
// axisTag(from: "slnt") = 1936486004
// axisTag(from: "ital") = 1769234796
// axisTag(from: "opsz") = 1869640570

Inspecting Variable Font Axes

Discover which axes and ranges a variable font supports:

import CoreText

func inspectVariableFont(named fontName: String, size: CGFloat = 17) {
    guard let uiFont = UIFont(name: fontName, size: size) else {
        print("Font not found: \(fontName)")
        return
    }

    let ctFont = uiFont as CTFont
    guard let axes = CTFontCopyVariationAxes(ctFont) as? [[String: Any]] else {
        print("\(fontName) is not a variable font (no variation axes).")
        return
    }

    print("Variable font: \(fontName)")
    print("Axes:")
    for axis in axes {
        let name = axis[kCTFontVariationAxisNameKey as String] ?? "Unknown"
        let tag = axis[kCTFontVariationAxisIdentifierKey as String] ?? 0
        let min = axis[kCTFontVariationAxisMinimumValueKey as String] ?? 0
        let max = axis[kCTFontVariationAxisMaximumValueKey as String] ?? 0
        let def = axis[kCTFontVariationAxisDefaultValueKey as String] ?? 0
        print("  \(name): tag=\(tag), range=\(min)...\(max), default=\(def)")
    }
}

Popular Variable Fonts for iOS

Font Name

Axes Available

Weight Range

Width Range

Notes

Inter

wght

100-900

--

Best all-around UI variable font

Roboto Flex

wght, wdth, slnt, opsz

100-1000

75-151

Most versatile variable font

Source Sans 3

wght, ital

200-900

--

Excellent for technical content

Outfit

wght

100-900

--

Modern, geometric

Work Sans

wght, ital

100-900

--

Clean, editorial

DM Sans

wght, ital, opsz

100-1000

--

Minimal, versatile

Manrope

wght

200-800

--

Tech-focused

Plus Jakarta Sans

wght, ital

200-800

--

Professional, fintech

Space Grotesk

wght

300-700

--

Developer tools

Sora

wght

100-800

--

Futuristic, web3

Montserrat

wght, ital

100-900

--

Popular geometric sans

Nunito

wght, ital

200-900

--

Rounded, friendly

Raleway

wght, ital

100-900

--

Elegant sans-serif

Playfair Display

wght, ital

400-900

--

Editorial serif

Lora

wght, ital

400-700

--

Literary serif

Fraunces

wght, opsz, SOFT, WONK

100-900

--

Playful serif with custom axes

Variable Font with Dynamic Type

Combine variable font control with Dynamic Type support:

import SwiftUI

struct VariableDynamicTypeFont: ViewModifier {
    let fontName: String
    let baseSize: CGFloat
    let weight: CGFloat
    let textStyle: Font.TextStyle

    @ScaledMetric private var scaledSize: CGFloat

    init(fontName: String, baseSize: CGFloat, weight: CGFloat, textStyle: Font.TextStyle) {
        self.fontName = fontName
        self.baseSize = baseSize
        self.weight = weight
        self.textStyle = textStyle
        self._scaledSize = ScaledMetric(wrappedValue: baseSize, relativeTo: textStyle)
    }

    func body(content: Content) -> some View {
        content.font(.variable(fontName, size: scaledSize, weight: weight))
    }
}

extension View {
    func variableFont(
        _ name: String,
        size: CGFloat,
        weight: CGFloat = 400,
        relativeTo style: Font.TextStyle = .body
    ) -> some View {
        modifier(VariableDynamicTypeFont(
            fontName: name,
            baseSize: size,
            weight: weight,
            textStyle: style
        ))
    }
}

// Usage:
// Text("Dynamic Variable").variableFont("Inter", size: 17, weight: 500, relativeTo: .body)

Quick Reference: Font Selection Decision Tree

What kind of content?
|
+-- System / Native feel
|   +-- Default        -> .system()
|   +-- Friendly       -> .system(design: .rounded)
|   +-- Editorial      -> .system(design: .serif)       (New York)
|   +-- Code/Data      -> .system(design: .monospaced)  (SF Mono)
|
+-- Custom branding
|   +-- Need variable weight control?
|   |   +-- Yes -> Inter, Roboto Flex, Outfit (variable fonts)
|   |   +-- No  -> Static font files
|   |
|   +-- What style?
|       +-- Clean modern        -> Inter, DM Sans, Plus Jakarta Sans
|       +-- Geometric           -> Poppins, Montserrat, Urbanist
|       +-- Humanist            -> Lato, Open Sans, Source Sans 3
|       +-- Tech / Developer    -> Space Grotesk, Manrope, Geist
|       +-- Rounded / Friendly  -> Nunito, Quicksand, Fredoka
|       +-- Editorial serif     -> Playfair Display, DM Serif Display
|       +-- Reading serif       -> Merriweather, Lora, EB Garamond
|       +-- Monospaced code     -> JetBrains Mono, Fira Code
|       +-- Bold display        -> Bebas Neue, Oswald, Anton
|
+-- Built-in (no bundling needed)
    +-- Sans-serif body     -> Avenir Next, Helvetica Neue, Gill Sans
    +-- Serif body          -> Georgia, Palatino, Charter, Iowan Old Style
    +-- Luxury display      -> Didot, Bodoni
    +-- Monospaced          -> Menlo, Courier New
    +-- Decorative          -> Copperplate, Rockwell, Zapfino

Quick Reference: PostScript Names Cheat Sheet

The most commonly needed PostScript names for
Font.custom()
:

// Sans-Serif (built-in)
"HelveticaNeue"                   "HelveticaNeue-Bold"
"AvenirNext-Regular"              "AvenirNext-Bold"
"AvenirNext-DemiBold"             "AvenirNext-Medium"
"GillSans"                        "GillSans-Bold"
"Futura-Medium"                   "Futura-Bold"
"Optima-Regular"                  "Optima-Bold"

// Serif (built-in)
"Georgia"                         "Georgia-Bold"
"TimesNewRomanPSMT"               "TimesNewRomanPS-BoldMT"
"Palatino-Roman"                  "Palatino-Bold"
"Baskerville"                     "Baskerville-Bold"
"Didot"                           "Didot-Bold"
"BodoniSvtyTwoITCTT-Bold"        "BodoniSvtyTwoSCITCTT-Book"
"Charter-Roman"                   "Charter-Bold"
"IowanOldStyle-Roman"             "IowanOldStyle-Bold"

// Monospaced (built-in)
"Menlo-Regular"                   "Menlo-Bold"
"CourierNewPSMT"                  "CourierNewPS-BoldMT"
"AmericanTypewriter"              "AmericanTypewriter-Bold"

// Display (built-in)
"Copperplate"                     "Copperplate-Bold"
"Rockwell-Regular"                "Rockwell-Bold"
"DINAlternate-Bold"               "DINCondensed-Bold"

// Script (built-in)
"SnellRoundhand"                  "SnellRoundhand-Bold"
"BradleyHandITCTT-Bold"           "Zapfino"
"SavoyeLetPlain"                  "ChalkboardSE-Regular"

// International (built-in)
"PingFangSC-Regular"              "PingFangSC-Semibold"
"HiraginoSans-W3"                 "HiraginoSans-W6"
"AppleSDGothicNeo-Regular"        "AppleSDGothicNeo-Bold"
"KohinoorDevanagari-Regular"      "KohinoorBangla-Regular"

This catalog covers the Apple system fonts, all major built-in iOS fonts with exact PostScript names, the top 100 Google Fonts for mobile development, custom font integration guides, proven font pairings, management utilities, and variable font techniques. Every font name string is ready to use directly in Font.custom() or UIFont(name:size:).

---

# Layer-by-Layer Icons with Icon Composer
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-icon-composer.html

← Back to all frameworks & guides
All articles →Design · Reference guideLayer-by-Layer Icons with Icon ComposerRepository guidance for Layer-by-Layer Icons with Icon Composer. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
Pattern

Make the layer plan first
Compose and annotate
Integrate with the app
What an agent must report

Anti-Patterns

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Separate foreground layers
↓2
Export editable artwork
↓3
Assemble in Icon Composer
↓4
Inspect rendered variants

02 / ArchitectureResponsibility boundariesBoundary 1
Layer sourcesBoundary 2
Icon compositionBoundary 3
App icon assetConnected responsibilities, not a required class hierarchy or an execution trace.
Context

Use this for app identity, layered app icons, Liquid Glass icon treatments, and Xcode icon integration. The output should include separately editable artwork, a layer manifest, appearance decisions, and a verified Icon Composer document when the tool is available.

Official sources checked 2026-09-10:

Icon Composer
Create an app icon in Icon Composer
App icon design guidance
Create icons with Icon Composer

Pattern

Make the layer plan first

Define the app’s recognizable symbol and a single visual idea. Separate the background, supporting shape, primary symbol, and optional accent. Use semantic names and stable ordering. For example, a reading app can use a background fill, a book silhouette, a page mark, and a small bookmark accent; each foreground shape remains editable independently.

Prefer clean vector foregrounds. Use SVG or transparent PNG assets for import, exported on the same canvas so positions remain aligned. Keep text outlined. For raster art, retain transparent backgrounds around foreground artwork. Keep imported background art opaque and full bleed. Do not rasterize all pieces into one image.

A useful project handoff contains:

IconLayers/
  01-base.svg
  02-symbol.svg
  03-accent.svg
  manifest.json
  README.md

These filenames are a recommended project convention, not an Apple file-format requirement. The CLI’s
--xcodegen
 starter creates editable layers as a starting point; replace their shapes and colors for the actual brand.

Compose and annotate

Launch Icon Composer from Xcode’s developer tools menu or the standalone app. Start a new document, choose the supported platforms, and set its background fill. Import foreground artwork, organize it into no more than four groups, and arrange depth from back to front. Use its material controls for highlights, refraction, translucency, and shadow rather than painting those effects into every asset.

Tune Default, Dark, and Mono appearances. Inspect at small sizes and against different surrounding backgrounds. Keep the silhouette recognizable when decorative detail disappears. Save the native
.icon
 document, reopen it, and check that its layers and appearance settings remain editable.

Integrate with the app

Add the saved icon document to the Xcode project and associate it with the app target’s icon setting. Build with the actual SDK and inspect the installed icon. Use Apple’s asset-catalog image-stack workflow for platforms whose icon format differs, including tvOS and visionOS; do not assume the same Composer workflow applies to every platform.

Export flattened images only for marketing, previews, or compatibility workflows that specifically require them. Retain the editable source and native document alongside those exports.

What an agent must report

List the artwork files, layer ordering, appearance variants checked, native document path if created, and Xcode verification performed. If Icon Composer is unavailable, deliver the SVG/PNG layer pack plus import instructions and explicitly leave native
.icon
 verification pending. Do not invent an undocumented
.icon
 schema or rename a JSON file to make it appear native.

Anti-Patterns

One flattened image presented as a layered icon project.
System shadows, corner masks, or highlights baked into foreground layers and then applied again by the compositor.
A tiny detailed logo that loses its identity at home-screen size.
A generic starter icon described as finished brand artwork.
Claiming a native
.icon
 file was verified without opening it in Icon Composer.

---

# Interaction Standards
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-interaction-standards.html

← Back to all frameworks & guides
All articles →Design · Reference guideInteraction StandardsRepository guidance for Interaction Standards. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

1. Animation Standards

Default Curves and Durations
When to Use Which Animation
Transition Standards

2. Haptic Feedback Rules

Haptic Selection Guide
SensoryFeedback (iOS 17+)
UIImpactFeedbackGenerator (iOS 16 and Below)
Best Practices

3. SF Symbols Guidelines

Size Guidelines
Rendering Modes
Variable Value Symbols
Symbol Effects
Preferred Symbol Weight

4. Button Style Standards

Complete Button Style System

5. Loading, Empty, and Error State Patterns

ViewState Enum
Loading View
Empty State View
Error State View
AsyncContentView Wrapper

6. Localization Approach

String Catalogs Setup
Date and Number Formatting
RTL and Layout Considerations

7. Privacy Manifest

PrivacyInfo.xcprivacy Structure
Required Reason API Categories
Third-Party SDK Manifests

8. Device Support and Adaptive Layout

Size Class Detection
NavigationSplitView for iPad Sidebar
ViewThatFits
iPad-Specific Features

9. Preview Provider Standards

Modern Previews (iOS 17+)
Interactive Previews
Preview with Mock Data
SwiftData Preview Container
Legacy PreviewProvider (iOS 16 and Below)

Quick Reference Summary

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Identify an interaction
↓2
Choose native behavior
↓3
Add accessible feedback
↓4
Test alternate inputs

02 / ArchitectureResponsibility boundariesBoundary 1
User inputBoundary 2
Interaction stateBoundary 3
Feedback and accessibilityConnected responsibilities, not a required class hierarchy or an execution trace.

Comprehensive reference for animations, haptics, symbols, button styles, state patterns, localization, privacy, adaptive layout, and preview standards.

1. Animation Standards

Default Curves and Durations

Category

Duration

Curve

Usage

Micro interaction

0.2s

.easeOut

Toggles, button presses, icon changes

Navigation transition

0.35s

.spring(response: 0.35, dampingFraction: 0.85)

Push, pop, tab switches

Content loading

0.3s

.easeInOut

Skeleton to content, fade-in

Dismissal

0.25s

.easeIn

Sheet dismiss, alert close, toast exit

Bouncy spring

--

.bouncy

Playful UI: reactions, badges, celebrations

Snappy spring

--

.snappy

Responsive controls: sliders, toggles

Smooth spring

--

.smooth

Elegant reveals: cards, overlays

When to Use Which Animation

Use Case

Animation

Rationale

Button tap feedback

.easeOut, 0.2s

Quick acknowledgment, no lingering

Toggle switch

.snappy

Responsive mechanical feel

Card expand/collapse

.spring(response: 0.35, dampingFraction: 0.85)

Natural, physical motion

Pull-to-refresh

.bouncy

Playful rubber-band feel

Modal presentation

.smooth

Elegant, unhurried entrance

Error shake

.default.repeatCount(3)

Attention-grabbing without being jarring

Skeleton shimmer

.easeInOut, 1.2s, repeat

Smooth continuous loop

Item deletion

.easeIn, 0.25s

Quick exit, attention moves forward

List reorder

.snappy

Keeps up with the finger

Hero transition

matchedGeometryEffect

Spatial continuity between screens

Transition Standards

// MARK: - Sheet Presentation (use system default)
.sheet(isPresented: $showSettings) {
    SettingsView()
}

// MARK: - Full Screen Cover with Custom Transition
.fullScreenCover(isPresented: $showOnboarding) {
    OnboardingView()
        .transition(.opacity.combined(with: .move(edge: .bottom)))
}

// MARK: - Navigation Push (system default)
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            ItemRow(item: item)
        }
    }
    .navigationDestination(for: Item.self) { item in
        ItemDetailView(item: item)
    }
}

// MARK: - Hero Transition with matchedGeometryEffect
struct HeroTransitionExample: View {
    @Namespace private var heroNamespace
    @State private var isExpanded = false

    var body: some View {
        if isExpanded {
            DetailCard(namespace: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                        isExpanded = false
                    }
                }
        } else {
            ThumbnailCard(namespace: heroNamespace)
                .onTapGesture {
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.85)) {
                        isExpanded = true
                    }
                }
        }
    }
}

struct ThumbnailCard: View {
    var namespace: Namespace.ID

    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(.blue.gradient)
            .matchedGeometryEffect(id: "card", in: namespace)
            .frame(width: 120, height: 120)
            .overlay {
                Text("Tap")
                    .matchedGeometryEffect(id: "title", in: namespace)
            }
    }
}

struct DetailCard: View {
    var namespace: Namespace.ID

    var body: some View {
        RoundedRectangle(cornerRadius: 24)
            .fill(.blue.gradient)
            .matchedGeometryEffect(id: "card", in: namespace)
            .frame(maxWidth: .infinity, maxHeight: 400)
            .overlay {
                Text("Detail View")
                    .matchedGeometryEffect(id: "title", in: namespace)
            }
            .padding()
    }
}

// MARK: - Custom Asymmetric Transition
extension AnyTransition {
    static var slideAndFade: AnyTransition {
        .asymmetric(
            insertion: .move(edge: .trailing).combined(with: .opacity),
            removal: .move(edge: .leading).combined(with: .opacity)
        )
    }
}

// Usage:
struct CustomTransitionExample: View {
    @State private var showContent = false

    var body: some View {
        VStack {
            if showContent {
                ContentView()
                    .transition(.slideAndFade)
            }
            Button("Toggle") {
                withAnimation(.easeInOut(duration: 0.3)) {
                    showContent.toggle()
                }
            }
        }
    }
}

// MARK: - Phased Animation for Multi-Step Effects
struct PhasedAnimationExample: View {
    @State private var trigger = false

    var body: some View {
        Image(systemName: "bell.fill")
            .font(.system(size: 32))
            .phaseAnimator([false, true], trigger: trigger) { content, phase in
                content
                    .scaleEffect(phase ? 1.2 : 1.0)
                    .rotationEffect(.degrees(phase ? 15 : 0))
            } animation: { phase in
                phase ? .bouncy : .snappy
            }
            .onTapGesture { trigger.toggle() }
    }
}

2. Haptic Feedback Rules

Haptic Selection Guide

Haptic Type

When to Use

Examples

.success

Completed action

Save confirmed, message sent, purchase complete

.warning

Destructive action confirmation

Delete dialog appears, irreversible action prompt

.error

Failed action

Validation error, network failure, permission denied

.light

Toggle or selection change

Switch toggled, radio selected, checkbox tapped

.medium

Snap to position

Pull-to-refresh threshold reached, snap point hit

.heavy

Long press activation

Context menu triggered, drag-and-drop pickup

.selection

Scrolling through values

Picker scroll, date wheel spin, segment change

SensoryFeedback (iOS 17+)

// MARK: - SensoryFeedback Modifier (Preferred for iOS 17+)
struct HapticExamples: View {
    @State private var isFavorited = false
    @State private var taskCompleted = false
    @State private var showError = false
    @State private var sliderValue = 0.5

    var body: some View {
        VStack(spacing: 24) {
            // Success haptic on task completion
            Button("Complete Task") {
                taskCompleted = true
            }
            .sensoryFeedback(.success, trigger: taskCompleted)

            // Light haptic on toggle
            Toggle("Favorite", isOn: $isFavorited)
                .sensoryFeedback(.selection, trigger: isFavorited)

            // Error haptic on failure
            Button("Submit") {
                showError = true
            }
            .sensoryFeedback(.error, trigger: showError)

            // Impact haptic with weight
            Button("Heavy Action") { }
                .sensoryFeedback(.impact(weight: .heavy), trigger: taskCompleted)
        }
    }
}

UIImpactFeedbackGenerator (iOS 16 and Below)

// MARK: - Haptic Manager for Pre-iOS 17
final class HapticManager {
    static let shared = HapticManager()
    private init() {}

    func impact(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
        let generator = UIImpactFeedbackGenerator(style: style)
        generator.prepare()
        generator.impactOccurred()
    }

    func notification(_ type: UINotificationFeedbackGenerator.FeedbackType) {
        let generator = UINotificationFeedbackGenerator()
        generator.prepare()
        generator.notificationOccurred(type)
    }

    func selection() {
        let generator = UISelectionFeedbackGenerator()
        generator.prepare()
        generator.selectionChanged()
    }
}

// Usage:
Button("Save") {
    performSave()
    HapticManager.shared.notification(.success)
}

Button("Delete") {
    HapticManager.shared.notification(.warning)
    showDeleteConfirmation = true
}

Best Practices

Never fire haptics on passive events (scrolling, appearing, background refresh).
Respect the system setting: haptics are automatically suppressed when the user disables them.
Prepare generators early (
.prepare()
) before the moment of feedback for zero-latency response.
Do not chain multiple haptics in rapid succession; one feedback per gesture.
Test on a real device -- the Simulator does not produce haptic output.

3. SF Symbols Guidelines

Size Guidelines

Context

Point Size

Example

Inline with text

17pt

Label icons, list item accessories

Tab bar

24pt

Bottom navigation icons

Buttons

28-32pt

Toolbar actions, floating action buttons

Feature icons

44-64pt

Empty states, onboarding, settings headers

Rendering Modes

Mode

When to Use

Description

.monochrome

Default, single-tone UI elements

One color applied uniformly

.hierarchical

Icons needing depth

Primary color with opacity layers

.palette

Brand-specific multi-color

You control each layer color

.multicolor

System-defined rich icons

Weather, file types, flags

// MARK: - Rendering Modes
struct SymbolRenderingExamples: View {
    var body: some View {
        VStack(spacing: 20) {
            // Monochrome (default)
            Image(systemName: "heart.fill")
                .symbolRenderingMode(.monochrome)
                .foregroundStyle(.red)

            // Hierarchical — automatic depth
            Image(systemName: "square.stack.3d.up.fill")
                .symbolRenderingMode(.hierarchical)
                .foregroundStyle(.blue)
                .font(.system(size: 44))

            // Palette — explicit layer colors
            Image(systemName: "person.crop.circle.badge.checkmark")
                .symbolRenderingMode(.palette)
                .foregroundStyle(.blue, .green)
                .font(.system(size: 44))

            // Multicolor — system-defined
            Image(systemName: "cloud.sun.rain.fill")
                .symbolRenderingMode(.multicolor)
                .font(.system(size: 44))
        }
    }
}

Variable Value Symbols

// MARK: - Variable Value (0.0 to 1.0) for Progress
struct VariableSymbolExample: View {
    @State private var progress: Double = 0.0

    var body: some View {
        VStack(spacing: 16) {
            Image(systemName: "speaker.wave.3.fill", variableValue: progress)
                .font(.system(size: 48))
                .foregroundStyle(.blue)
                .contentTransition(.symbolEffect(.replace))

            Image(systemName: "wifi", variableValue: progress)
                .font(.system(size: 48))
                .foregroundStyle(.green)

            Slider(value: $progress, in: 0...1)
                .padding(.horizontal)
        }
    }
}

Symbol Effects

// MARK: - Symbol Effects (iOS 17+)
struct SymbolEffectsExample: View {
    @State private var bellTapped = false
    @State private var isActive = false
    @State private var downloadComplete = false

    var body: some View {
        VStack(spacing: 24) {
            // Bounce on tap
            Image(systemName: "bell.fill")
                .font(.system(size: 32))
                .symbolEffect(.bounce, value: bellTapped)
                .onTapGesture { bellTapped.toggle() }

            // Continuous pulse while active
            Image(systemName: "antenna.radiowaves.left.and.right")
                .font(.system(size: 32))
                .symbolEffect(.pulse, isActive: isActive)

            // Variable color animation (iterating layers)
            Image(systemName: "wifi")
                .font(.system(size: 32))
                .symbolEffect(.variableColor.iterative, isActive: isActive)

            // Appear / disappear
            Image(systemName: "checkmark.circle.fill")
                .font(.system(size: 32))
                .symbolEffect(.appear, isActive: downloadComplete)

            // Replace transition between symbols
            Image(systemName: isActive ? "pause.fill" : "play.fill")
                .contentTransition(.symbolEffect(.replace))
                .font(.system(size: 32))
                .onTapGesture {
                    withAnimation { isActive.toggle() }
                }

            // Breathe effect
            Image(systemName: "heart.fill")
                .font(.system(size: 32))
                .foregroundStyle(.red)
                .symbolEffect(.breathe, isActive: isActive)

            // Wiggle effect
            Image(systemName: "bell.badge.fill")
                .font(.system(size: 32))
                .symbolEffect(.wiggle, value: bellTapped)

            // Rotate effect
            Image(systemName: "gear")
                .font(.system(size: 32))
                .symbolEffect(.rotate, isActive: isActive)
        }
    }
}

Preferred Symbol Weight

Use
.medium
 weight by default to match the system HIG:

Image(systemName: "gear")
    .fontWeight(.medium)

4. Button Style Standards

Complete Button Style System

// MARK: - Primary Button Style
struct PrimaryButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled
    var isLoading: Bool = false

    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 8) {
            if isLoading {
                ProgressView()
                    .tint(.white)
            }
            configuration.label
        }
        .font(.body.weight(.semibold))
        .foregroundStyle(.white)
        .frame(maxWidth: .infinity, minHeight: 50)
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(
                    isEnabled
                        ? AnyShapeStyle(LinearGradient(
                            colors: [.blue, .blue.opacity(0.8)],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing))
                        : AnyShapeStyle(.gray.opacity(0.4))
                )
        )
        .scaleEffect(configuration.isPressed ? 0.97 : 1.0)
        .opacity(isLoading ? 0.9 : 1.0)
        .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
        .allowsHitTesting(!isLoading)
    }
}

// MARK: - Secondary Button Style
struct SecondaryButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled
    var isLoading: Bool = false

    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 8) {
            if isLoading {
                ProgressView()
                    .tint(.accentColor)
            }
            configuration.label
        }
        .font(.body.weight(.semibold))
        .foregroundStyle(isEnabled ? .accentColor : .gray)
        .frame(maxWidth: .infinity, minHeight: 50)
        .background(
            RoundedRectangle(cornerRadius: 12)
                .stroke(isEnabled ? Color.accentColor : .gray, lineWidth: 1.5)
        )
        .scaleEffect(configuration.isPressed ? 0.97 : 1.0)
        .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

// MARK: - Destructive Button Style
struct DestructiveButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled
    var isLoading: Bool = false

    func makeBody(configuration: Configuration) -> some View {
        HStack(spacing: 8) {
            if isLoading {
                ProgressView()
                    .tint(.white)
            }
            configuration.label
        }
        .font(.body.weight(.semibold))
        .foregroundStyle(.white)
        .frame(maxWidth: .infinity, minHeight: 50)
        .background(
            RoundedRectangle(cornerRadius: 12)
                .fill(isEnabled ? Color.red : .gray.opacity(0.4))
        )
        .scaleEffect(configuration.isPressed ? 0.97 : 1.0)
        .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

// MARK: - Ghost Button Style
struct GhostButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.body.weight(.medium))
            .foregroundStyle(isEnabled ? .accentColor : .gray)
            .padding(.horizontal, 16)
            .padding(.vertical, 10)
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .opacity(configuration.isPressed ? 0.7 : 1.0)
            .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

// MARK: - Icon Button Style
struct IconButtonStyle: ButtonStyle {
    var size: CGFloat = 44
    @Environment(\.isEnabled) private var isEnabled

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.system(size: size * 0.45, weight: .medium))
            .foregroundStyle(isEnabled ? .accentColor : .gray)
            .frame(width: size, height: size)
            .background(Circle().fill(.ultraThinMaterial))
            .scaleEffect(configuration.isPressed ? 0.9 : 1.0)
            .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

// MARK: - Pill Button Style
struct PillButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(.subheadline.weight(.semibold))
            .foregroundStyle(isEnabled ? .white : .gray)
            .padding(.horizontal, 20)
            .padding(.vertical, 10)
            .background(
                Capsule()
                    .fill(isEnabled ? Color.accentColor : .gray.opacity(0.3))
            )
            .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
            .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
    }
}

// MARK: - Usage Examples
struct ButtonShowcase: View {
    @State private var isLoading = false

    var body: some View {
        VStack(spacing: 16) {
            Button { } label: {
                Label("Continue", systemImage: "arrow.right")
            }
            .buttonStyle(PrimaryButtonStyle())

            Button { } label: {
                Label("Edit Profile", systemImage: "pencil")
            }
            .buttonStyle(SecondaryButtonStyle())

            Button(role: .destructive) { } label: {
                Label("Delete Account", systemImage: "trash")
            }
            .buttonStyle(DestructiveButtonStyle())

            Button("Learn More") { }
                .buttonStyle(GhostButtonStyle())

            Button { } label: {
                Image(systemName: "plus")
            }
            .buttonStyle(IconButtonStyle())

            Button { } label: {
                Label("Subscribe", systemImage: "star.fill")
            }
            .buttonStyle(PillButtonStyle())

            // Disabled state
            Button("Disabled") { }
                .buttonStyle(PrimaryButtonStyle())
                .disabled(true)

            // Loading state
            Button("Saving...") { }
                .buttonStyle(PrimaryButtonStyle(isLoading: true))
        }
        .padding()
    }
}

5. Loading, Empty, and Error State Patterns

ViewState Enum

// MARK: - Generic View State
enum ViewState<T> {
    case loading
    case loaded(T)
    case empty
    case error(Error)
}

Loading View

// MARK: - Shimmer Modifier
struct ShimmerModifier: ViewModifier {
    @State private var phase: CGFloat = 0

    func body(content: Content) -> some View {
        content
            .overlay(
                LinearGradient(
                    colors: [.clear, .white.opacity(0.4), .clear],
                    startPoint: .leading,
                    endPoint: .trailing
                )
                .offset(x: phase)
                .mask(content)
            )
            .onAppear {
                withAnimation(.easeInOut(duration: 1.2).repeatForever(autoreverses: false)) {
                    phase = UIScreen.main.bounds.width
                }
            }
    }
}

extension View {
    func shimmer() -> some View {
        modifier(ShimmerModifier())
    }
}

// MARK: - Skeleton Loading View
struct SkeletonRow: View {
    var body: some View {
        HStack(spacing: 12) {
            RoundedRectangle(cornerRadius: 8)
                .fill(.gray.opacity(0.2))
                .frame(width: 48, height: 48)

            VStack(alignment: .leading, spacing: 8) {
                RoundedRectangle(cornerRadius: 4)
                    .fill(.gray.opacity(0.2))
                    .frame(height: 14)
                    .frame(maxWidth: 160)

                RoundedRectangle(cornerRadius: 4)
                    .fill(.gray.opacity(0.2))
                    .frame(height: 12)
                    .frame(maxWidth: 100)
            }
        }
        .shimmer()
    }
}

// MARK: - Spinner Loading
struct LoadingSpinnerView: View {
    var message: String = "Loading..."

    var body: some View {
        VStack(spacing: 16) {
            ProgressView()
                .controlSize(.large)
            Text(message)
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

Empty State View

// MARK: - Empty State Presets
enum EmptyStatePreset {
    case noData
    case noSearchResults
    case noConnection
    case firstTimeUse

    var icon: String {
        switch self {
        case .noData: "tray"
        case .noSearchResults: "magnifyingglass"
        case .noConnection: "wifi.slash"
        case .firstTimeUse: "sparkles"
        }
    }

    var title: String {
        switch self {
        case .noData: "Nothing Here Yet"
        case .noSearchResults: "No Results Found"
        case .noConnection: "No Connection"
        case .firstTimeUse: "Get Started"
        }
    }

    var message: String {
        switch self {
        case .noData: "Items you add will appear here."
        case .noSearchResults: "Try a different search term."
        case .noConnection: "Check your internet connection and try again."
        case .firstTimeUse: "Tap the button below to create your first item."
        }
    }
}

struct EmptyStateView: View {
    var preset: EmptyStatePreset
    var actionTitle: String?
    var action: (() -> Void)?

    var body: some View {
        ContentUnavailableView {
            Label(preset.title, systemImage: preset.icon)
        } description: {
            Text(preset.message)
        } actions: {
            if let actionTitle, let action {
                Button(actionTitle, action: action)
                    .buttonStyle(.borderedProminent)
            }
        }
    }
}

Error State View

// MARK: - Error State View
struct ErrorStateView: View {
    let error: Error
    var retryAction: (() -> Void)?
    var reportAction: (() -> Void)?

    var body: some View {
        ContentUnavailableView {
            Label("Something Went Wrong", systemImage: "exclamationmark.triangle")
        } description: {
            Text(error.localizedDescription)
        } actions: {
            VStack(spacing: 12) {
                if let retryAction {
                    Button("Try Again", action: retryAction)
                        .buttonStyle(.borderedProminent)
                }
                if let reportAction {
                    Button("Report Issue", action: reportAction)
                        .font(.footnote)
                }
            }
        }
    }
}

AsyncContentView Wrapper

// MARK: - Async Content View
struct AsyncContentView<T, LoadedContent: View>: View {
    @Binding var state: ViewState<T>
    var loadingMessage: String = "Loading..."
    var emptyPreset: EmptyStatePreset = .noData
    var emptyAction: (() -> Void)?
    var retryAction: (() -> Void)?
    @ViewBuilder var content: (T) -> LoadedContent

    var body: some View {
        switch state {
        case .loading:
            LoadingSpinnerView(message: loadingMessage)
                .transition(.opacity)
        case .loaded(let data):
            content(data)
                .transition(.opacity)
        case .empty:
            EmptyStateView(
                preset: emptyPreset,
                actionTitle: emptyAction != nil ? "Add Item" : nil,
                action: emptyAction
            )
            .transition(.opacity)
        case .error(let error):
            ErrorStateView(error: error, retryAction: retryAction)
                .transition(.opacity)
        }
    }
}

// MARK: - Usage with Pull-to-Refresh
struct ItemListView: View {
    @State private var state: ViewState<[Item]> = .loading

    var body: some View {
        AsyncContentView(
            state: $state,
            emptyPreset: .noData,
            retryAction: { Task { await loadItems() } }
        ) { items in
            List(items) { item in
                Text(item.name)
            }
            .refreshable {
                await loadItems()
            }
        }
        .animation(.easeInOut(duration: 0.3), value: stateKey)
        .task { await loadItems() }
    }

    private var stateKey: String {
        switch state {
        case .loading: "loading"
        case .loaded: "loaded"
        case .empty: "empty"
        case .error: "error"
        }
    }

    private func loadItems() async {
        state = .loading
        do {
            let items = try await ItemService.fetchAll()
            state = items.isEmpty ? .empty : .loaded(items)
        } catch {
            state = .error(error)
        }
    }
}

6. Localization Approach

String Catalogs Setup

All user-facing strings must use
String(localized:)
. Never hardcode display text.

Xcode creates a
Localizable.xcstrings
 file (String Catalog) that automatically extracts strings.

// MARK: - Correct: Localized Strings
let title = String(localized: "welcome.title")
let message = String(localized: "welcome.message")

// With default value
let greeting = String(localized: "greeting", defaultValue: "Hello there!")

// MARK: - String Interpolation
let itemCount = 5
let label = String(localized: "\(itemCount) items remaining")

// MARK: - Pluralization (handled in .xcstrings catalog)
// In the String Catalog, define plural variants:
//   "item_count" -> one: "%lld item", other: "%lld items"
let countLabel = String(localized: "\(itemCount) items")

// MARK: - Table-based organization
let settingsTitle = String(localized: "title", table: "Settings")

Date and Number Formatting

// MARK: - Locale-Aware Formatting
struct FormattingExamples: View {
    let price: Decimal = 49.99
    let eventDate = Date()
    let progress = 0.756

    var body: some View {
        VStack(alignment: .leading) {
            // Currency — adapts to user locale
            Text(price, format: .currency(code: "USD"))

            // Date — adapts to locale conventions
            Text(eventDate, format: .dateTime.month(.wide).day().year())

            // Relative date
            Text(eventDate, format: .relative(presentation: .named))

            // Percentage
            Text(progress, format: .percent.precision(.fractionLength(1)))

            // Measurement
            Text(Measurement(value: 72, unit: UnitTemperature.fahrenheit),
                 format: .measurement(width: .abbreviated))
        }
    }
}

RTL and Layout Considerations

// MARK: - RTL-Safe Layout
struct RTLSafeView: View {
    @Environment(\.layoutDirection) var layoutDirection

    var body: some View {
        HStack {
            // Use .leading/.trailing, never .left/.right
            Image(systemName: "arrow.forward")
                .flipsForRightToLeftLayoutDirection(true)
            Text(String(localized: "next"))
        }
        .frame(maxWidth: .infinity, alignment: .leading) // Flips automatically in RTL
    }
}

7. Privacy Manifest

PrivacyInfo.xcprivacy Structure

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <!-- Tracking declaration -->
    <key>NSPrivacyTracking</key>
    <false/>
    <key>NSPrivacyTrackingDomains</key>
    <array/>

    <!-- Required Reason APIs -->
    <key>NSPrivacyAccessedAPITypes</key>
    <array>
        <!-- File timestamp APIs -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>C617.1</string> <!-- Access within app container -->
            </array>
        </dict>
        <!-- System boot time APIs -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategorySystemBootTime</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>35F9.1</string> <!-- Measure elapsed time -->
            </array>
        </dict>
        <!-- Disk space APIs -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryDiskSpace</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>E174.1</string> <!-- Check available disk space -->
            </array>
        </dict>
        <!-- User defaults APIs -->
        <dict>
            <key>NSPrivacyAccessedAPIType</key>
            <string>NSPrivacyAccessedAPICategoryUserDefaults</string>
            <key>NSPrivacyAccessedAPITypeReasons</key>
            <array>
                <string>CA92.1</string> <!-- Access within app -->
            </array>
        </dict>
    </array>

    <!-- Collected data types -->
    <key>NSPrivacyCollectedDataTypes</key>
    <array>
        <dict>
            <key>NSPrivacyCollectedDataType</key>
            <string>NSPrivacyCollectedDataTypeCrashData</string>
            <key>NSPrivacyCollectedDataTypeLinked</key>
            <false/>
            <key>NSPrivacyCollectedDataTypeTracking</key>
            <false/>
            <key>NSPrivacyCollectedDataTypePurposes</key>
            <array>
                <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string>
            </array>
        </dict>
    </array>
</dict>
</plist>

Required Reason API Categories

Category

Common APIs

Typical Reason Code

File Timestamp

NSFileCreationDate, NSFileModificationDate, NSURLContentModificationDateKey

C617.1 (app container), DDA9.1 (user-presented)

System Boot Time

systemUptime, ProcessInfo.processInfo.systemUptime

35F9.1 (measure elapsed time)

Disk Space

volumeAvailableCapacityKey, volumeAvailableCapacityForImportantUsageKey

E174.1 (check space before write)

User Defaults

UserDefaults

CA92.1 (access within app)

Active Keyboard

activeInputModes

3EC4.1 (customize UI)

Third-Party SDK Manifests

Each third-party SDK must include its own
PrivacyInfo.xcprivacy
. Verify during dependency audits:

// In Package.swift or Podfile, verify SDK authors ship privacy manifests.
// Xcode aggregates all manifests during App Store submission.
// Run: Product > Generate Privacy Report to audit before submission.

8. Device Support and Adaptive Layout

Size Class Detection

// MARK: - Adaptive Layout with Size Classes
struct AdaptiveView: View {
    @Environment(\.horizontalSizeClass) private var horizontalSizeClass
    @Environment(\.verticalSizeClass) private var verticalSizeClass

    var body: some View {
        if horizontalSizeClass == .regular {
            // iPad or iPhone landscape wide
            HStack(spacing: 0) {
                SidebarView()
                    .frame(width: 320)
                DetailView()
            }
        } else {
            // iPhone portrait
            NavigationStack {
                ListView()
            }
        }
    }
}

NavigationSplitView for iPad Sidebar

// MARK: - Navigation Split View
struct SplitLayoutView: View {
    @State private var selectedItem: Item?
    @State private var columnVisibility = NavigationSplitViewVisibility.automatic

    var body: some View {
        NavigationSplitView(columnVisibility: $columnVisibility) {
            List(items, selection: $selectedItem) { item in
                NavigationLink(value: item) {
                    ItemRow(item: item)
                }
            }
            .navigationTitle("Items")
        } detail: {
            if let selectedItem {
                ItemDetailView(item: selectedItem)
            } else {
                ContentUnavailableView("Select an Item",
                    systemImage: "sidebar.left",
                    description: Text("Choose an item from the sidebar."))
            }
        }
    }
}

ViewThatFits

// MARK: - ViewThatFits for Adaptive Components
struct AdaptiveActionBar: View {
    var body: some View {
        ViewThatFits(in: .horizontal) {
            // First choice: full horizontal layout
            HStack(spacing: 16) {
                Button("Save Draft") { }
                    .buttonStyle(SecondaryButtonStyle())
                Button("Preview") { }
                    .buttonStyle(SecondaryButtonStyle())
                Button("Publish") { }
                    .buttonStyle(PrimaryButtonStyle())
            }

            // Fallback: stacked layout
            VStack(spacing: 8) {
                Button("Publish") { }
                    .buttonStyle(PrimaryButtonStyle())
                HStack(spacing: 8) {
                    Button("Save Draft") { }
                        .buttonStyle(SecondaryButtonStyle())
                    Button("Preview") { }
                        .buttonStyle(SecondaryButtonStyle())
                }
            }
        }
        .padding()
    }
}

iPad-Specific Features

// MARK: - Pointer Hover Effect (iPad)
struct HoverableCard: View {
    @State private var isHovered = false

    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .fill(.background)
            .shadow(radius: isHovered ? 8 : 2)
            .scaleEffect(isHovered ? 1.02 : 1.0)
            .onHover { hovering in
                withAnimation(.easeOut(duration: 0.2)) {
                    isHovered = hovering
                }
            }
            .hoverEffect(.lift) // System pointer lift effect
    }
}

// MARK: - Keyboard Shortcuts (iPad with hardware keyboard)
struct ShortcutView: View {
    var body: some View {
        VStack {
            Text("Press Cmd+N to create")
        }
        .keyboardShortcut("n", modifiers: .command)
    }
}

9. Preview Provider Standards

Modern Previews (iOS 17+)

// MARK: - Basic Preview
#Preview {
    ContentView()
}

// MARK: - Named Preview
#Preview("Dark Mode") {
    ContentView()
        .preferredColorScheme(.dark)
}

// MARK: - Light and Dark Side by Side
#Preview("Color Schemes") {
    VStack {
        ContentView()
            .preferredColorScheme(.light)
        ContentView()
            .preferredColorScheme(.dark)
    }
}

// MARK: - Dynamic Type Sizes
#Preview("Large Text") {
    ContentView()
        .dynamicTypeSize(.xxxLarge)
}

#Preview("Accessibility Sizes") {
    ContentView()
        .dynamicTypeSize(.accessibility3)
}

// MARK: - Device Variations
#Preview("iPhone SE", traits: .fixedLayout(width: 375, height: 667)) {
    ContentView()
}

#Preview("iPad", traits: .fixedLayout(width: 1024, height: 768)) {
    ContentView()
}

// MARK: - Size That Fits
#Preview("Component", traits: .sizeThatFitsLayout) {
    PillButtonExample()
        .padding()
}

Interactive Previews

// MARK: - Interactive Preview with @Previewable
#Preview("Toggle Demo") {
    @Previewable @State var isOn = false

    Toggle("Notifications", isOn: $isOn)
        .padding()
}

#Preview("Counter") {
    @Previewable @State var count = 0

    VStack {
        Text("Count: \(count)")
            .font(.largeTitle)
        Button("Increment") { count += 1 }
            .buttonStyle(PrimaryButtonStyle())
    }
    .padding()
}

Preview with Mock Data

// MARK: - Preview with Mock Data
struct Item: Identifiable {
    let id: UUID
    let name: String
    let subtitle: String

    static let samples: [Item] = [
        Item(id: UUID(), name: "Morning Run", subtitle: "5.2 km"),
        Item(id: UUID(), name: "Yoga Session", subtitle: "45 min"),
        Item(id: UUID(), name: "Cycling", subtitle: "12.8 km"),
    ]
}

#Preview {
    List(Item.samples) { item in
        VStack(alignment: .leading) {
            Text(item.name).font(.headline)
            Text(item.subtitle).font(.caption).foregroundStyle(.secondary)
        }
    }
}

SwiftData Preview Container

// MARK: - SwiftData Preview Container
struct PreviewContainer {
    static var shared: ModelContainer {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try! ModelContainer(
            for: Task.self,
            configurations: config
        )
        // Insert sample data
        let context = container.mainContext
        for task in Task.sampleTasks {
            context.insert(task)
        }
        return container
    }
}

#Preview {
    TaskListView()
        .modelContainer(PreviewContainer.shared)
}

Legacy PreviewProvider (iOS 16 and Below)

// MARK: - Legacy PreviewProvider
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        Group {
            ContentView()
                .previewDisplayName("Light")

            ContentView()
                .preferredColorScheme(.dark)
                .previewDisplayName("Dark")

            ContentView()
                .dynamicTypeSize(.xxxLarge)
                .previewDisplayName("Large Text")
        }
    }
}

Quick Reference Summary

Area

Key Rule

Micro interaction

0.2s .easeOut

Navigation

0.35s .spring(response: 0.35, dampingFraction: 0.85)

Dismissal

0.25s .easeIn

Haptic on success

.success via SensoryFeedback

Haptic on toggle

.selection

SF Symbol weight

.medium

SF Symbol inline size

17pt

Button press scale

0.97

Strings

Always String(localized:)

Layout direction

.leading/.trailing, never .left/.right

Privacy

Ship PrivacyInfo.xcprivacy with every target

Previews

Use #Preview macro, test light/dark/large text

---

# Adopting Liquid Glass
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-liquid-glass-adoption.html

← Back to all frameworks & guides
All articles →Design and Motion · Reference guideAdopting Liquid GlassRepository guidance for Liquid Glass. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

1. The decision you make before writing any code
2. What changes with no code, and what you must audit
3. Scroll edge effect
4. Navigation
5. Toolbars and menus
6. Controls and shape
7. Lists, forms, and a silent text change
8. App icons
9. Test matrix
Anti-Patterns
Checklist

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Locate navigation surfaces
↓2
Check SDK availability
↓3
Apply supported materials
↓4
Review legibility

02 / ArchitectureResponsibility boundariesBoundary 1
Content layerBoundary 2
Navigation chromeBoundary 3
Accessibility settingsConnected responsibilities, not a required class hierarchy or an execution trace.
Load this when:
 rebuilding an existing app against the iOS 26 SDK or later,
auditing an interface after the rebuild, or deciding whether to take the new
look at all.

This is the migration half.
 For applying the material to a custom view —

glassEffect
,
GlassEffectContainer
, the availability guard, the fallback —
see
docs/design/design-tokens.md
 §4. That document is about opting
in
 on a
view you own. This one is about what happens to the app you already shipped.

1. The decision you make before writing any code

Rebuilding against the iOS 26 SDK
adopts the new design automatically
.
Standard components from SwiftUI, UIKit, and AppKit pick up Liquid Glass with no
code change. That is the whole point, and it is also the risk: an app you have
not re-audited ships a changed interface.

There is one escape hatch:

<!-- Info.plist -->
<key>UIDesignRequiresCompatibility</key>
<true/>

This keeps the app looking as it did when built against the previous SDK, while
still letting you build with the current one.

Treat it as a stopgap, not a decision.
 It buys a release cycle to do the
audit properly; it does not remove the work. An app frozen on the compatibility
key looks progressively more dated as the rest of the system moves, and Apple
has historically retired these keys. Set a date to remove it.

Situation

Do this

You can audit the interface this cycle

Rebuild, adopt, work through §2–§7

You must ship urgently and cannot audit

UIDesignRequiresCompatibility, with a tracked removal date

You are on the previous SDK entirely

Nothing yet — but the audit list still tells you what to expect

2. What changes with no code, and what you must audit

The rule that decides most of the work:
the system now owns the background of
controls and navigation.
 Custom backgrounds you added to those elements do not
merely look dated — they sit on top of Liquid Glass and the scroll edge effect
and interfere with both.

Audit and, in most cases, delete custom backgrounds on:

NavigationStack
,
NavigationSplitView
, and split-view columns
toolbars (
toolbar(content:)
) and title bars
tab bars
sheets and popovers — including any
visualEffect
 view you put behind popover
  content, which is now duplicated work

// WRONG — a hand-rolled bar background, from before the system provided one.
// It overlays Liquid Glass and defeats the scroll edge effect, so content
// scrolling underneath loses the contrast the system would have given it.
.toolbarBackground(Color.appSurface, for: .navigationBar)
.toolbarBackground(.visible, for: .navigationBar)

// RIGHT — let the system decide. It adapts to overlap, focus, and scroll
// position in ways a static color cannot.
// (no modifier)

Do not hard-code control metrics.
 Controls adopt rounder, larger forms, and
an extra-large size option. Standard controls resize themselves; a control with
a pinned
.frame(width:height:)
 will not, and will sit wrong next to the ones
that did.

3. Scroll edge effect

Scroll views obscure content passing beneath bars so controls stay legible.
System bars get this for free.
A custom bar does not
 — it gets content
sliding under it at full contrast.

// A custom bottom bar. Register it so the system applies the scroll edge
// effect, rather than leaving your controls to fight the content behind them.
ScrollView {
    ArticleList(articles: model.articles)
}
.safeAreaBar(edge: .bottom) {
    PlaybackControls(model: model)
}

Use
scrollEdgeEffectStyle(_:for:)
 when you need to choose the style rather
than accept the default.

4. Navigation

Navigation is the layer Liquid Glass lives in, so the separation between
navigation and content has to be real. If your content and your chrome are
interleaved, the new material has nothing coherent to float above.

// Tab bar that becomes a sidebar where there is room for one.
TabView {
    Tab("Library", systemImage: "books.vertical") { LibraryScreen() }
    Tab("Browse", systemImage: "square.grid.2x2") { BrowseScreen() }

    // The SEARCH ROLE, not a tab that happens to contain search. The system
    // pulls it to the trailing end and styles it as search — matching where
    // users have learned to look for it in every other app.
    Tab(role: .search) { SearchScreen() }
}
.tabViewStyle(.sidebarAdaptable)

// Let the tab bar recede while reading, and come back on the reverse scroll.
.tabBarMinimizeBehavior(.onScrollDown)

Background extension
 makes a hero image read as continuing beneath a
sidebar or inspector, without actually placing content under it — the system
mirrors and blurs the adjacent content so the sidebar stays legible:

NavigationSplitView {
    LibrarySidebar(selection: $selection)
} detail: {
    ScrollView {
        HeroImage(article: article)
            .backgroundExtensionEffect()
        ArticleBody(article: article)
    }
}

After adding either,
check the safe areas of the content beside the sidebar
and inspector
 — that is where "peeking through" either works or clips.

5. Toolbars and menus

Toolbar items now group, and the grouping is meaningful: items sharing a
background read as related.

.toolbar {
    ToolbarItemGroup(placement: .primaryAction) {
        Button("Bookmark", systemImage: "bookmark") { model.bookmark() }
        Button("Share", systemImage: "square.and.arrow.up") { model.share() }
    }

    // Separates groups that must not read as one control.
    ToolbarSpacer(.fixed, placement: .primaryAction)

    ToolbarItem(placement: .primaryAction) {
        Button("Delete", systemImage: "trash", role: .destructive) { model.delete() }
    }
}

Three rules that are easy to get wrong:

Do not mix text and icons inside one group.
 Across items sharing a
  background it reads as an inconsistency, not a distinction.
Every icon-only item needs an accessibility label
, regardless of what is
  on screen. Someone using VoiceOver or Voice Control gets nothing from the
  glyph.
review_swiftui
 and
audit_app_store_readiness
 both flag this.
Hide the item, not its content.
 A toolbar item whose
view
 is hidden
  leaves an empty slot the system still lays out.

// WRONG — an empty toolbar item the system reserves space for.
ToolbarItem { if canEdit { EditButton() } }

// RIGHT — the item itself goes away.
ToolbarItem { EditButton() }
    .hidden(!canEdit)

6. Controls and shape

Reach for the built-in glass button styles before building anything custom:

Button("Continue") { model.advance() }
    .buttonStyle(.glass)

Button("Buy Now") { model.purchase() }
    .buttonStyle(.glassProminent)

Nested shapes should be
concentric
 with their container — the hardware's
corner radius informs the whole chain of rounded elements:

// Concentric with whatever contains it, rather than a guessed radius that
// looks subtly wrong at one screen size and badly wrong at another.
CardContent(article: article)
    .clipShape(ConcentricRectangle())

Be sparing with colour on controls and navigation.
 Colour on a glass surface
costs legibility. When you do use it, use a system colour or a custom one with
light, dark, and increased-contrast variants — the same rule as every other
token in
docs/design/design-tokens.md
.

7. Lists, forms, and a silent text change

Rows and sections gained height, padding, and corner radius. The change that
bites is quieter:

Section headers are now title-case, not upper-case.
 The system no longer
force-capitalises them. A header you wrote as
"recently played"
 — relying on
the old behaviour to render
RECENTLY PLAYED
 — now renders exactly as written.

// WRONG — depended on the system shouting it for you.
Section("recently played") { … }

// RIGHT — write the capitalisation you want to see.
Section("Recently Played") { … }

Grep for lowercase
Section(
 string literals. Nothing warns about this; it just
ships.

8. App icons

Icons are now layered, and the system applies reflection, refraction, shadow,
blur, and highlights across light, dark, clear, and tinted variants.

Separate your artwork into foreground / middle / background layers.
Do not bake in effects.
 Shadows and blurs you paint yourself get composited
  on top of the system's, and the result is muddy.
Prefer solid, filled, overlapping semi-transparent shapes over fine detail.
Compose in
Icon Composer
 (ships with Xcode; also on Apple Design
  Resources), which previews the system effects and appearance variants.
Keep elements centred — the system masks to a rounded rectangle on
  iOS/iPadOS/macOS and a circle on watchOS. An irregular icon gets a
  system-provided background.

9. Test matrix

Liquid Glass adapts to user settings, and those settings remove or change the
effects you designed around. Standard components adapt on their own;
anything
custom is yours to verify.

Setting

What to check

Reduce Transparency

Custom glass surfaces stay legible when the blur is gone

Reduce Motion

Morphing and fluid transitions degrade to something sensible

Increase Contrast

Custom colours still meet contrast against a glass background

Dark mode

Every custom surface and colour pairing

Accessibility text sizes

Controls and bars reflow rather than clip

The user's Liquid Glass appearance preference

Custom elements follow it

Per platform:

watchOS
 — changes are minimal and appear even without rebuilding. Adopt
  the watchOS 10 toolbar APIs and standard button styles to pick them up.
tvOS
 — controls take on glass
when focused
. Adopt the standard focus
  APIs (
focusable(_:)
,
isFocused
) so custom controls match. Only Apple TV 4K
  (2nd generation) and newer render the effects; older devices keep the current
  appearance, which is a fallback you do not have to write.
iPadOS
 — windows resize continuously to a minimum size rather than
  snapping between presets. See
docs/tooling/device-hub.md
; rebuilding against
  the iOS 27 SDK also opts you into resizability.

Anti-Patterns

// WRONG — glass on every custom control in the app.
// The material exists to draw attention to content. Applied everywhere it
// competes with the content and flattens the hierarchy it was meant to create.
ForEach(filters) { filter in
    FilterChip(filter).glassEffect()
}

// RIGHT — reserve it for the few genuinely functional elements.

// WRONG — separate glass effects stacked next to each other.
// Each is its own render pass, and they will not morph into one another.
HStack {
    BackButton().glassEffect()
    PlayButton().glassEffect()
}

// RIGHT — one container, so they blend and merge.
GlassEffectContainer(spacing: Space.tight) {
    HStack {
        BackButton().glassEffect().glassEffectID("back", in: namespace)
        PlayButton().glassEffect().glassEffectID("play", in: namespace)
    }
}

// WRONG — an action sheet with no source.
// It now originates from the control that triggered it. With no anchor it
// appears detached from the thing it acts on.
.confirmationDialog("Delete?", isPresented: $isConfirming) { … }

// RIGHT — anchor it to its source so the relationship is visible.
.confirmationDialog("Delete?", isPresented: $isConfirming, presenting: item) { … }

// WRONG — shipping UIDesignRequiresCompatibility with no removal plan.
// It is a deferral. Left in place the app drifts further from the system every
// release, and the audit it postponed only grows.

// RIGHT — set it, file the work, remove it next cycle.

// WRONG — assuming every adoption API shares one availability floor.
// Liquid Glass arrived in iOS 26, but these APIs did not all land together.
// A blanket #available(iOS 26, *) around a symbol introduced later fails to
// compile; one at iOS 27 around an iOS 26 symbol silently drops every iOS 26
// device to the fallback — the mistake this skill flags most often.
if #available(iOS 26, *) { /* every new API */ }

// RIGHT — check each symbol's own floor in Xcode's documentation and guard on
// THAT version. `check_availability_guards` catches the over-restrictive case.

Checklist

[ ] Rebuilt against the current SDK and reviewed every screen
[ ]
UIDesignRequiresCompatibility
 either absent, or present with a removal date
[ ] Custom backgrounds removed from bars, split views, sheets, popovers
[ ] No hard-coded control metrics
[ ] Custom bars registered for the scroll edge effect (
safeAreaBar
)
[ ] Navigation clearly separated from content
[ ] Search uses
Tab(role: .search)
, not an ordinary tab
[ ] Toolbar items grouped meaningfully;
ToolbarSpacer
 between unrelated groups
[ ] No group mixing text and icon items
[ ] Every icon-only control has an accessibility label
[ ] Hidden toolbar items hide the item, not the view
[ ] Section headers written in the capitalisation you want to see
[ ] Nested shapes concentric with their containers
[ ] Glass reserved for the few most important elements
[ ] Multiple glass elements share one
GlassEffectContainer
[ ] App icon rebuilt as layers in Icon Composer, with no baked-in effects
[ ] Verified under Reduce Transparency, Reduce Motion, Increase Contrast, dark
      mode, and accessibility text sizes
[ ] Each new API guarded on
its own
 introduction version

---

# Generate an iOS palette from a brief
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-palette-generation.html

← Back to all frameworks & guides
All articles →Design · Reference guideGenerate an iOS palette from a briefRepository guidance for Generate an iOS palette from a brief. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
Workflow
Inputs and precedence
Families and strategies
Perceptual derivation
Implementation
Verification
Native preview

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Describe the app
↓2
Derive OKLCH roles
↓3
Validate appearance pairs
↓4
Review asset preview

02 / ArchitectureResponsibility boundariesBoundary 1
Intent and seedBoundary 2
Pure palette engineBoundary 3
Asset CLI handoffConnected responsibilities, not a required class hierarchy or an execution trace.
Context

generate_color_system
 returns an offline, deterministic preview. It does not call a model, fetch an image, write tokens, or change an asset catalog. An explicit
imagePath
 lets the MCP adapter read a bounded local PNG; the pure generator itself accepts sample colors. This capability is local source work, not a claim about the currently published npm version. Start from the existing
design-token contract
, and keep native system semantics wherever custom branding adds no value.

Workflow

Ask for the app task, a seed if available, and the emotional intent. A description is sufficient; do not make users fill every field.
Generate a preview, inspect the rationale and contrast pairs, then compare it in the app's actual UI.
Choose or revise the seed, family and harmony. Color meaning depends on the audience; finance is not required to be blue.
Only when requested, save
assetTokens
 to a JSON file and run the existing
asset generator
. It refuses to overwrite a catalog. Review and merge the intended assets.
Build the app, inspect both appearances and Increase Contrast, and test non-color state indicators.

{
  "description": "Minimal finance app for young professionals. Calm, trustworthy, premium. Prefer blue.",
  "category": "finance",
  "mood": ["calm", "trustworthy", "premium"],
  "family": "Blue",
  "strategy": "dominant-plus-supporting",
  "appearance": "both",
  "accessibility": "standard"
}

The result includes the seed and its provenance, rationale, all semantic tokens, light/dark palettes, high-contrast variants, eleven-step tonal scales, contrast rows, restrained gradient samples, SwiftUI/UIKit snippets, and compatible asset-token JSON.
appearance
 records a preference; both modes are always returned.
oled
 makes the dark base black but keeps raised surfaces distinguishable.

Inputs and precedence

A valid seed is opaque sRGB
#RRGGBB
. Priority is
primaryColor
, first
brandColors
,
existingTokens.primary
 or
.Primary
, first
existingColors
, extracted sample candidate, then the intent heuristic. All values are validated and bounded. Existing tokens are seed input,
not
 an automatic migration preserving every role. Explicit brand input is kept as
seed
; UI variants may differ to meet measured contrast constraints.

Image samples are
{ "color": "#2457DB", "weight": 20 }
 in
imageSamples
. Read
brand extraction
 before interpreting them. A project reviewer inventory can supply
existingColors
; project files are never read by the generation tool itself.

Families and strategies

Hue families: Neutral / Minimal, Blue, Indigo, Violet, Purple, Magenta, Teal, Cyan, Green, Emerald, Mint, Yellow, Amber, Orange, Red, Rose, Pink, Brown / Earth, Navy / Midnight, Slate / Graphite.

Treatment families: Pastel, Muted, High Contrast, Monochrome, Gradient-led, Dark OLED, Warm Light, Cool Light, Glass / Translucent, Dynamic Brand, Category-aware palettes. Families are heuristic starting points, not copyrighted themes or a catalog of fixed RGB tables. Some are treatment hints: Glass returns an opaque fallback and material guidance, not a simulated Apple material. High Contrast sets the enhanced contrast target; Gradient-led defaults to restrained gradient. Both can also be requested with the explicit accessibility and strategy inputs.

Available harmonies are monochromatic, analogous, complementary, split-complementary, triadic, neutral-plus-accent, dominant-plus-supporting and restrained gradient. Supporting hues use limited chroma. The intent hash varies automatic seeds within the suggested hue area, so a category does not force a single color. Free-form categories and moods are accepted; heuristics recognize broad groups and fall back to restrained neutrals. There is no semantic language model interpreting every word.

Perceptual derivation

sRGB is decoded to linear RGB, converted to OKLab/OKLCH, and mapped back to sRGB by reducing out-of-gamut chroma while holding lightness and hue. Tonal steps are 50, 100, 200, 300, 400, 500, 600, 700, 800, 900 and 950, with intentionally tapered chroma at the ends. Near-neutral colors have unstable perceptual hue; they are treated as neutrals.

Contrast repair searches lightness while preserving hue and as much chroma as the gamut allows. This is an engineering constraint, not proof of visual quality. Gradient stops follow a short OKLCH hue arc; stop samples do not prove contrast at every rendered pixel or interpolation space. Use gradients decoratively unless the rendered foreground has been verified.

Implementation

The returned
implementation
 strings define named-color accessors. Add assets to the
same target
 that loads them. For a Swift package pass its resource bundle explicitly; see
samples/ColorSystem
.

Text("Continue")
    .foregroundStyle(AppColors.onPrimary)
    .padding()
    .background(AppColors.primary)

Do not apply
onPrimary
 to an arbitrary gradient, disabled fill or destructive fill. Each is a separate foreground/background relationship. Use native
Button(role: .destructive)
 when possible.

Verification

Regression tests cover determinism, all families, token completeness, gamut output, tonal ordering, OLED and high contrast, input bounds, image-sample filtering and direct project evidence. They do not establish beauty, cultural meaning, native material behavior or App Review acceptance.

Native preview

These SwiftUI renders use the sample's compiled named-color assets on macOS. They show the sample palette, not an iOS Simulator acceptance test. Reproduce with
samples/ColorSystem/render-preview.swift
; see the sample README for the asset compilation step.

The perceptual math follows
the published OKLab definition
; no third-party palette branding or fixed theme was copied.

---

# Professional UI/UX System
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-README.html

← Back to all frameworks & guides
All articles →Design · Reference guideProfessional UI/UX SystemRepository guidance for Professional UI/UX System. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Context
Design Stack
Review Order
Pattern
Anti-Patterns
Production Checklist
Palette tooling

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Define the product task
↓2
Choose semantic tokens
↓3
Compose adaptive screens
↓4
Review accessibility

02 / ArchitectureResponsibility boundariesBoundary 1
Product intentBoundary 2
Design systemBoundary 3
SwiftUI surfacesConnected responsibilities, not a required class hierarchy or an execution trace.
Context

Use this hub when a user asks for professional polish, better hierarchy, premium design, improved onboarding, stronger empty/loading/error states, better iPad adaptation, or a screen that feels native instead of merely functional.

Design Stack

Layer

Use

design-tokens.md

Source of truth for color, spacing, radius, typography, shadows

typography-system.md

Semantic text hierarchy and Dynamic Type

color-system.md

Contrast, semantic color roles, dark mode

interaction-standards.md

Touch targets, gestures, haptics, animation purpose

liquid-glass-adoption.md

Modern materials without contrast loss

stunning-ui-patterns.md

Full-screen composition patterns

Review Order

Primary task: can the user see what to do in two seconds?
Hierarchy: do size, weight, color, and position match importance?
Layout: are alignment, rhythm, gutters, and touch targets consistent?
Type: semantic styles first; fixed sizes only with a clear reason.
State: loading, empty, error, success, offline, disabled.
Adaptation: compact/regular widths, iPad resizability, keyboard, pointer.
Accessibility: VoiceOver labels, focus order, Dynamic Type, contrast, Reduce Motion.

Pattern

Prefer a compact state model over scattered booleans:

enum ScreenState<Content: Equatable>: Equatable {
    case loading
    case empty
    case failed(message: String)
    case loaded(Content)
}

Each state gets a designed view that keeps the same layout frame when possible. Loading should not resize the final surface; empty states should name the next action; errors should explain recovery.

Anti-Patterns

// WRONG: "premium" means adding gradients and cards everywhere.
Why: polish comes from hierarchy, restraint, rhythm, and state quality.

// RIGHT: define the primary task, reduce competing emphasis, then add motion/material only where it explains state.

// WRONG: a phone layout stretched full-width on iPad.
Why: density and reading length break.

// RIGHT: use navigation split, max readable width, sidebars, inspector panes, or two-column layouts.

Production Checklist

[ ] Primary action is visually dominant and reachable.
[ ] Every repeated spacing/color/type value comes from a token.
[ ] Text wraps at accessibility sizes.
[ ] Tap targets are at least 44x44pt.
[ ] Dark mode and contrast are checked.
[ ] Loading, empty, error, success, and offline states exist where relevant.
[ ] iPad layout is not a stretched phone screen.
[ ] Motion and haptics clarify state, not decoration.

Palette tooling

Use
palette generation
 to produce a four-appearance token preview compatible with the existing assets CLI. Use
color accessibility
 to interpret measured pairs. Only write or merge a catalog when requested; retain system semantics when custom colors add no value.

---

# Stunning UI Patterns -- Complete SwiftUI Pattern Library
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-stunning-ui-patterns.html

← Back to all frameworks & guides
All articles →Design and Motion · Reference guideStunning UI Patterns -- Complete SwiftUI Pattern LibraryRepository guidance for Human Interface Guidelines. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Overview
1. Glass Morphism Card
2. Neumorphic Card
3. Gradient Card with Floating Shadow
4. Animated Onboarding Screen
5. Hero Image Header with Parallax Scroll
6. Bottom Sheet with Snap Points
7. Animated Tab Bar
8. Profile Card
9. Dashboard Cards with Charts
10. Floating Action Button
11. Custom Toggle with Animation
12. Swipeable Card Stack
13. Pull-to-Refresh with Custom Animation
14. Skeleton Loading / Shimmer Effect
15. Toast / Snackbar Notification
16. Expandable Card with matchedGeometryEffect
17. Animated Gradient Background
18. Blurred Header that Changes on Scroll
19. Chip / Tag Flow Layout
20. Rating Stars Component
Bonus: Combining Patterns -- Premium App Screen
Quick Reference

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Choose information hierarchy
↓2
Compose layout and spacing
↓3
Add meaningful motion
↓4
Review all screen states

02 / ArchitectureResponsibility boundariesBoundary 1
Content hierarchyBoundary 2
Reusable componentsBoundary 3
Screen statesConnected responsibilities, not a required class hierarchy or an execution trace.
Overview

This file is a production-ready pattern library of 20 stunning UI components. Every example compiles, uses beautiful colors from the color palettes defined in
color-system.md
, and produces results worthy of a premium App Store feature. Copy, adapt, and compose these into world-class iOS applications.

All examples assume the
Color(hex:)
 extension from
color-system.md
 is available.

1. Glass Morphism Card

Frosted glass with a luminous border. Use over images or gradients for maximum effect.

import SwiftUI

struct GlassMorphismCard: View {
    var body: some View {
        ZStack {
            // Rich background
            LinearGradient(
                colors: [Color(hex: "6C63FF"), Color(hex: "EC4899"), Color(hex: "06B6D4")],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()

            VStack(alignment: .leading, spacing: 12) {
                HStack {
                    Image(systemName: "sparkles")
                        .font(.title2)
                        .foregroundStyle(.white)
                    Spacer()
                    Text("PRO")
                        .font(.caption.weight(.bold))
                        .kerning(1.5)
                        .foregroundStyle(.white)
                        .padding(.horizontal, 10)
                        .padding(.vertical, 4)
                        .background(.white.opacity(0.2), in: Capsule())
                }

                Text("Premium Plan")
                    .font(.title2.weight(.bold))
                    .foregroundStyle(.white)

                Text("Unlock all features and get early access to new content every week.")
                    .font(.subheadline)
                    .foregroundStyle(.white.opacity(0.8))
                    .lineSpacing(4)

                HStack {
                    Text("$9.99/mo")
                        .font(.title3.weight(.bold))
                        .foregroundStyle(.white)
                    Spacer()
                    Text("Subscribe")
                        .font(.subheadline.weight(.semibold))
                        .foregroundStyle(Color(hex: "6C63FF"))
                        .padding(.horizontal, 20)
                        .padding(.vertical, 10)
                        .background(.white, in: Capsule())
                }
                .padding(.top, 8)
            }
            .padding(24)
            .background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 24))
            .overlay(
                RoundedRectangle(cornerRadius: 24)
                    .stroke(
                        LinearGradient(
                            colors: [.white.opacity(0.5), .white.opacity(0.05)],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        ),
                        lineWidth: 1
                    )
            )
            .shadow(color: .black.opacity(0.15), radius: 20, y: 10)
            .padding(24)
        }
    }
}

2. Neumorphic Card

Soft shadows creating the illusion of raised or inset elements on a flat surface.

struct NeumorphicCard: View {
    @State private var isPressed = false

    var body: some View {
        let bgColor = Color(hex: "E8EDF2")

        ZStack {
            bgColor.ignoresSafeArea()

            VStack(spacing: 32) {
                // Raised card
                VStack(alignment: .leading, spacing: 8) {
                    Image(systemName: "wifi")
                        .font(.title)
                        .foregroundStyle(Color(hex: "6C63FF"))
                    Text("Network Status")
                        .font(.headline)
                    Text("Connected -- 120 Mbps")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(24)
                .background(bgColor)
                .cornerRadius(20)
                .shadow(color: .white.opacity(0.7), radius: 10, x: -5, y: -5)
                .shadow(color: .black.opacity(0.15), radius: 10, x: 5, y: 5)

                // Inset / pressed style
                VStack(alignment: .leading, spacing: 8) {
                    Image(systemName: "bolt.fill")
                        .font(.title)
                        .foregroundStyle(Color(hex: "F59E0B"))
                    Text("Quick Actions")
                        .font(.headline)
                    Text("Tap to toggle")
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                .padding(24)
                .background(
                    RoundedRectangle(cornerRadius: 20)
                        .fill(bgColor)
                        .overlay(
                            RoundedRectangle(cornerRadius: 20)
                                .stroke(Color.black.opacity(0.05), lineWidth: 1)
                        )
                        .innerShadow(bgColor)
                )

                // Neumorphic button
                Button {
                    withAnimation(.spring(response: 0.3)) {
                        isPressed.toggle()
                    }
                } label: {
                    Image(systemName: "power")
                        .font(.title)
                        .foregroundStyle(isPressed ? Color(hex: "2D9F6F") : .gray)
                        .frame(width: 80, height: 80)
                        .background(bgColor)
                        .cornerRadius(40)
                        .shadow(
                            color: isPressed ? .clear : .white.opacity(0.7),
                            radius: isPressed ? 0 : 8, x: -4, y: -4
                        )
                        .shadow(
                            color: isPressed ? .clear : .black.opacity(0.15),
                            radius: isPressed ? 0 : 8, x: 4, y: 4
                        )
                        .overlay(
                            RoundedRectangle(cornerRadius: 40)
                                .stroke(Color.black.opacity(isPressed ? 0.08 : 0), lineWidth: 1)
                        )
                }
            }
            .padding(24)
        }
    }
}

// Helper modifier for inner shadow
extension View {
    func innerShadow(_ bgColor: Color) -> some View {
        self.overlay(
            RoundedRectangle(cornerRadius: 20)
                .stroke(Color.black.opacity(0.08), lineWidth: 1)
                .shadow(color: .black.opacity(0.1), radius: 3, x: 2, y: 2)
                .clipShape(RoundedRectangle(cornerRadius: 20))
        )
        .overlay(
            RoundedRectangle(cornerRadius: 20)
                .stroke(Color.white.opacity(0.5), lineWidth: 1)
                .shadow(color: .white.opacity(0.5), radius: 3, x: -2, y: -2)
                .clipShape(RoundedRectangle(cornerRadius: 20))
        )
    }
}

3. Gradient Card with Floating Shadow

The shadow color matches the card gradient, creating a luminous glow beneath.

struct GradientShadowCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                Image(systemName: "chart.line.uptrend.xyaxis")
                    .font(.title2)
                Text("Revenue")
                    .font(.headline)
                Spacer()
                Text("+24.5%")
                    .font(.subheadline.weight(.bold))
            }
            .foregroundStyle(.white)

            Text("$48,290")
                .font(.system(size: 36, weight: .bold, design: .rounded))
                .foregroundStyle(.white)

            Text("Compared to $38,800 last month")
                .font(.caption)
                .foregroundStyle(.white.opacity(0.7))
        }
        .padding(24)
        .background(
            LinearGradient(
                colors: [Color(hex: "6C63FF"), Color(hex: "8B5CF6")],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            ),
            in: RoundedRectangle(cornerRadius: 20)
        )
        // Floating colored shadow
        .shadow(color: Color(hex: "6C63FF").opacity(0.4), radius: 20, y: 12)
        .padding(24)
    }
}

4. Animated Onboarding Screen

TabView with custom animated page indicators and fluid transitions.

struct OnboardingScreen: View {
    @State private var currentPage = 0

    let pages: [(icon: String, title: String, subtitle: String, colors: [Color])] = [
        ("sparkles", "Welcome", "Discover a new way to organize your life.", [Color(hex: "6C63FF"), Color(hex: "A78BFA")]),
        ("bolt.fill", "Lightning Fast", "Everything you need, instantly at your fingertips.", [Color(hex: "EC4899"), Color(hex: "F472B6")]),
        ("heart.fill", "Made with Love", "Crafted by a team that cares about every detail.", [Color(hex: "2D9F6F"), Color(hex: "22D3EE")]),
    ]

    var body: some View {
        ZStack {
            // Animated background gradient
            LinearGradient(
                colors: pages[currentPage].colors,
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            )
            .ignoresSafeArea()
            .animation(.easeInOut(duration: 0.6), value: currentPage)

            VStack(spacing: 0) {
                TabView(selection: $currentPage) {
                    ForEach(0..<pages.count, id: \.self) { index in
                        VStack(spacing: 24) {
                            Image(systemName: pages[index].icon)
                                .font(.system(size: 80))
                                .foregroundStyle(.white)
                                .shadow(color: .white.opacity(0.3), radius: 20)

                            Text(pages[index].title)
                                .font(.largeTitle.weight(.bold))
                                .foregroundStyle(.white)

                            Text(pages[index].subtitle)
                                .font(.body)
                                .foregroundStyle(.white.opacity(0.8))
                                .multilineTextAlignment(.center)
                                .padding(.horizontal, 40)
                        }
                        .tag(index)
                    }
                }
                .tabViewStyle(.page(indexDisplayMode: .never))

                // Custom page indicator
                HStack(spacing: 10) {
                    ForEach(0..<pages.count, id: \.self) { index in
                        Capsule()
                            .fill(.white.opacity(currentPage == index ? 1 : 0.4))
                            .frame(
                                width: currentPage == index ? 28 : 8,
                                height: 8
                            )
                            .animation(.spring(response: 0.4, dampingFraction: 0.7), value: currentPage)
                    }
                }
                .padding(.bottom, 32)

                // CTA button
                Button {
                    if currentPage < pages.count - 1 {
                        withAnimation { currentPage += 1 }
                    }
                } label: {
                    Text(currentPage == pages.count - 1 ? "Get Started" : "Continue")
                        .font(.headline)
                        .foregroundStyle(pages[currentPage].colors[0])
                        .frame(maxWidth: .infinity)
                        .padding(.vertical, 16)
                        .background(.white, in: RoundedRectangle(cornerRadius: 16))
                }
                .padding(.horizontal, 24)
                .padding(.bottom, 40)
            }
        }
    }
}

5. Hero Image Header with Parallax Scroll

struct ParallaxHeroHeader: View {
    @State private var scrollOffset: CGFloat = 0

    var body: some View {
        ScrollView {
            VStack(spacing: 0) {
                GeometryReader { geo in
                    let minY = geo.frame(in: .global).minY
                    ZStack(alignment: .bottomLeading) {
                        // Parallax image
                        Rectangle()
                            .fill(
                                LinearGradient(
                                    colors: [Color(hex: "0A2463"), Color(hex: "1E88E5")],
                                    startPoint: .topLeading,
                                    endPoint: .bottomTrailing
                                )
                            )
                            .overlay(
                                Image(systemName: "mountain.2.fill")
                                    .resizable()
                                    .scaledToFit()
                                    .foregroundStyle(.white.opacity(0.15))
                                    .padding(40)
                            )
                            .offset(y: minY > 0 ? -minY * 0.5 : 0)

                        // Gradient overlay for text legibility
                        LinearGradient(
                            colors: [.clear, .black.opacity(0.6)],
                            startPoint: .top,
                            endPoint: .bottom
                        )

                        // Title content
                        VStack(alignment: .leading, spacing: 8) {
                            Text("EXPLORE")
                                .font(.caption.weight(.bold))
                                .kerning(2)
                                .foregroundStyle(.white.opacity(0.7))

                            Text("Mountains of\nSwiftUI")
                                .font(.largeTitle.weight(.bold))
                                .foregroundStyle(.white)
                        }
                        .padding(24)
                    }
                    .frame(height: max(350 + (minY > 0 ? minY : 0), 350))
                    .clipped()
                }
                .frame(height: 350)

                // Content below hero
                VStack(spacing: 16) {
                    ForEach(0..<10, id: \.self) { i in
                        HStack(spacing: 16) {
                            RoundedRectangle(cornerRadius: 12)
                                .fill(Color(hex: "1E88E5").opacity(0.1))
                                .frame(width: 60, height: 60)
                                .overlay(
                                    Image(systemName: "photo")
                                        .foregroundStyle(Color(hex: "1E88E5"))
                                )
                            VStack(alignment: .leading, spacing: 4) {
                                Text("Article \(i + 1)")
                                    .font(.headline)
                                Text("A beautiful description of this content piece.")
                                    .font(.subheadline)
                                    .foregroundStyle(.secondary)
                            }
                            Spacer()
                        }
                        .padding(16)
                        .background(Color(.secondarySystemGroupedBackground))
                        .cornerRadius(16)
                    }
                }
                .padding(16)
            }
        }
        .ignoresSafeArea(edges: .top)
    }
}

6. Bottom Sheet with Snap Points

struct BottomSheetDemo: View {
    @State private var sheetOffset: CGFloat = 500
    @GestureState private var dragOffset: CGFloat = 0

    private let snapPoints: [CGFloat] = [100, 350, 600]

    var body: some View {
        ZStack {
            Color(hex: "0B0B1A").ignoresSafeArea()

            VStack {
                Text("Drag the sheet up")
                    .foregroundStyle(.white)
                    .font(.headline)
            }

            // Sheet
            VStack(spacing: 0) {
                // Handle
                Capsule()
                    .fill(Color(.systemGray3))
                    .frame(width: 40, height: 5)
                    .padding(.top, 10)
                    .padding(.bottom, 16)

                // Sheet content
                VStack(alignment: .leading, spacing: 16) {
                    Text("Discover Nearby")
                        .font(.title2.weight(.bold))

                    ForEach(0..<5, id: \.self) { i in
                        HStack(spacing: 12) {
                            Circle()
                                .fill(
                                    LinearGradient(
                                        colors: [Color(hex: "8B5CF6"), Color(hex: "EC4899")],
                                        startPoint: .topLeading,
                                        endPoint: .bottomTrailing
                                    )
                                )
                                .frame(width: 44, height: 44)
                                .overlay(
                                    Image(systemName: "mappin")
                                        .foregroundStyle(.white)
                                )
                            VStack(alignment: .leading) {
                                Text("Location \(i + 1)")
                                    .font(.subheadline.weight(.semibold))
                                Text("\(Double.random(in: 0.1...5.0), specifier: "%.1f") km away")
                                    .font(.caption)
                                    .foregroundStyle(.secondary)
                            }
                            Spacer()
                            Image(systemName: "chevron.right")
                                .foregroundStyle(.tertiary)
                        }
                    }
                }
                .padding(.horizontal, 24)

                Spacer()
            }
            .frame(maxWidth: .infinity)
            .background(Color(.systemBackground))
            .cornerRadius(24)
            .shadow(color: .black.opacity(0.2), radius: 20, y: -5)
            .offset(y: sheetOffset + dragOffset)
            .gesture(
                DragGesture()
                    .updating($dragOffset) { value, state, _ in
                        state = value.translation.height
                    }
                    .onEnded { value in
                        let projected = sheetOffset + value.translation.height
                        let nearest = snapPoints.min(by: {
                            abs($0 - projected) < abs($1 - projected)
                        }) ?? snapPoints[1]
                        withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                            sheetOffset = nearest
                        }
                    }
            )
        }
    }
}

7. Animated Tab Bar

struct AnimatedTabBar: View {
    @State private var selectedTab = 0
    @Namespace private var tabAnimation

    let tabs: [(icon: String, label: String)] = [
        ("house.fill", "Home"),
        ("magnifyingglass", "Search"),
        ("plus.circle.fill", "Add"),
        ("heart.fill", "Saved"),
        ("person.fill", "Profile"),
    ]

    var body: some View {
        VStack {
            Spacer()
            Text("Tab \(selectedTab)")
                .font(.largeTitle.weight(.bold))
                .foregroundStyle(.primary)
            Spacer()

            // Tab bar
            HStack {
                ForEach(0..<tabs.count, id: \.self) { index in
                    Button {
                        withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) {
                            selectedTab = index
                        }
                    } label: {
                        VStack(spacing: 4) {
                            ZStack {
                                if selectedTab == index {
                                    Capsule()
                                        .fill(Color(hex: "6C63FF").opacity(0.15))
                                        .frame(width: 56, height: 32)
                                        .matchedGeometryEffect(id: "tabBG", in: tabAnimation)
                                }

                                Image(systemName: tabs[index].icon)
                                    .font(.system(size: index == 2 ? 28 : 20))
                                    .foregroundStyle(
                                        selectedTab == index
                                            ? Color(hex: "6C63FF")
                                            : .gray
                                    )
                                    .scaleEffect(selectedTab == index ? 1.15 : 1.0)
                            }
                            .frame(height: 32)

                            Text(tabs[index].label)
                                .font(.system(size: 10, weight: .medium))
                                .foregroundStyle(
                                    selectedTab == index
                                        ? Color(hex: "6C63FF")
                                        : .gray
                                )
                        }
                        .frame(maxWidth: .infinity)
                    }
                }
            }
            .padding(.top, 8)
            .padding(.bottom, 4)
            .background(
                Rectangle()
                    .fill(.ultraThinMaterial)
                    .ignoresSafeArea(edges: .bottom)
            )
        }
    }
}

8. Profile Card

struct ProfileCard: View {
    var body: some View {
        VStack(spacing: 0) {
            // Gradient header
            ZStack(alignment: .bottom) {
                LinearGradient(
                    colors: [Color(hex: "8B5CF6"), Color(hex: "EC4899")],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
                .frame(height: 140)

                // Avatar
                Circle()
                    .fill(Color(.systemBackground))
                    .frame(width: 88, height: 88)
                    .overlay(
                        Circle()
                            .fill(
                                LinearGradient(
                                    colors: [Color(hex: "6C63FF"), Color(hex: "A78BFA")],
                                    startPoint: .topLeading,
                                    endPoint: .bottomTrailing
                                )
                            )
                            .frame(width: 80, height: 80)
                            .overlay(
                                Text("JA")
                                    .font(.title.weight(.bold))
                                    .foregroundStyle(.white)
                            )
                    )
                    .offset(y: 44)
            }

            VStack(spacing: 12) {
                Text("Jane Appleseed")
                    .font(.title3.weight(.bold))
                    .padding(.top, 48)

                Text("Senior iOS Engineer")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)

                HStack(spacing: 32) {
                    statItem(value: "234", label: "Posts")
                    statItem(value: "12.4K", label: "Followers")
                    statItem(value: "891", label: "Following")
                }
                .padding(.top, 8)

                HStack(spacing: 12) {
                    Button {} label: {
                        Text("Follow")
                            .font(.subheadline.weight(.semibold))
                            .foregroundStyle(.white)
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 10)
                            .background(Color(hex: "6C63FF"), in: RoundedRectangle(cornerRadius: 12))
                    }

                    Button {} label: {
                        Text("Message")
                            .font(.subheadline.weight(.semibold))
                            .foregroundStyle(Color(hex: "6C63FF"))
                            .frame(maxWidth: .infinity)
                            .padding(.vertical, 10)
                            .background(Color(hex: "6C63FF").opacity(0.1), in: RoundedRectangle(cornerRadius: 12))
                    }
                }
                .padding(.top, 8)
            }
            .padding(24)
        }
        .background(Color(.systemBackground))
        .cornerRadius(24)
        .shadow(color: .black.opacity(0.1), radius: 16, y: 8)
        .padding(20)
    }

    func statItem(value: String, label: String) -> some View {
        VStack(spacing: 2) {
            Text(value)
                .font(.headline)
            Text(label)
                .font(.caption)
                .foregroundStyle(.secondary)
        }
    }
}

9. Dashboard Cards with Charts

struct CircularProgressCard: View {
    let progress: Double
    let title: String
    let subtitle: String
    let color: Color

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                VStack(alignment: .leading, spacing: 4) {
                    Text(title)
                        .font(.subheadline.weight(.semibold))
                    Text(subtitle)
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Spacer()

                ZStack {
                    Circle()
                        .stroke(color.opacity(0.15), lineWidth: 6)
                    Circle()
                        .trim(from: 0, to: progress)
                        .stroke(color, style: StrokeStyle(lineWidth: 6, lineCap: .round))
                        .rotationEffect(.degrees(-90))
                    Text("\(Int(progress * 100))%")
                        .font(.caption2.weight(.bold))
                        .foregroundStyle(color)
                }
                .frame(width: 48, height: 48)
            }

            // Mini bar chart
            HStack(alignment: .bottom, spacing: 4) {
                ForEach(0..<7, id: \.self) { _ in
                    let height = CGFloat.random(in: 12...40)
                    RoundedRectangle(cornerRadius: 3)
                        .fill(color.opacity(Double.random(in: 0.3...1.0)))
                        .frame(height: height)
                }
            }
            .frame(height: 40)
        }
        .padding(20)
        .background(Color(.secondarySystemGroupedBackground))
        .cornerRadius(20)
    }
}

struct DashboardView: View {
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [
                GridItem(.flexible(), spacing: 12),
                GridItem(.flexible(), spacing: 12),
            ], spacing: 12) {
                CircularProgressCard(
                    progress: 0.78,
                    title: "Steps",
                    subtitle: "7,800 / 10,000",
                    color: Color(hex: "2D9F6F")
                )
                CircularProgressCard(
                    progress: 0.45,
                    title: "Calories",
                    subtitle: "1,350 / 3,000",
                    color: Color(hex: "FF6B35")
                )
                CircularProgressCard(
                    progress: 0.92,
                    title: "Sleep",
                    subtitle: "7.4 / 8.0 hrs",
                    color: Color(hex: "8B5CF6")
                )
                CircularProgressCard(
                    progress: 0.60,
                    title: "Water",
                    subtitle: "1.8 / 3.0 L",
                    color: Color(hex: "0A6EBD")
                )
            }
            .padding(16)
        }
    }
}

10. Floating Action Button

struct FloatingActionButton: View {
    @State private var isExpanded = false

    let actions: [(icon: String, color: Color, label: String)] = [
        ("camera.fill", Color(hex: "EC4899"), "Photo"),
        ("doc.fill", Color(hex: "F59E0B"), "Document"),
        ("link", Color(hex: "0A6EBD"), "Link"),
    ]

    var body: some View {
        ZStack(alignment: .bottomTrailing) {
            Color.clear // Takes full space

            VStack(spacing: 12) {
                if isExpanded {
                    ForEach(Array(actions.enumerated()), id: \.offset) { index, action in
                        HStack(spacing: 12) {
                            Text(action.label)
                                .font(.subheadline.weight(.medium))
                                .foregroundStyle(.primary)
                                .padding(.horizontal, 12)
                                .padding(.vertical, 6)
                                .background(.ultraThinMaterial, in: Capsule())

                            Button {} label: {
                                Image(systemName: action.icon)
                                    .font(.system(size: 18))
                                    .foregroundStyle(.white)
                                    .frame(width: 48, height: 48)
                                    .background(action.color, in: Circle())
                                    .shadow(color: action.color.opacity(0.3), radius: 8, y: 4)
                            }
                        }
                        .transition(.asymmetric(
                            insertion: .scale.combined(with: .opacity).combined(with: .offset(y: 20)),
                            removal: .scale.combined(with: .opacity)
                        ))
                    }
                }

                Button {
                    withAnimation(.spring(response: 0.35, dampingFraction: 0.7)) {
                        isExpanded.toggle()
                    }
                } label: {
                    Image(systemName: "plus")
                        .font(.title2.weight(.semibold))
                        .foregroundStyle(.white)
                        .frame(width: 60, height: 60)
                        .background(
                            LinearGradient(
                                colors: [Color(hex: "6C63FF"), Color(hex: "8B5CF6")],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing
                            ),
                            in: Circle()
                        )
                        .shadow(color: Color(hex: "6C63FF").opacity(0.4), radius: 12, y: 6)
                        .rotationEffect(.degrees(isExpanded ? 45 : 0))
                }
            }
            .padding(24)
        }
    }
}

11. Custom Toggle with Animation

struct PremiumToggle: View {
    @Binding var isOn: Bool

    var body: some View {
        Button {
            withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                isOn.toggle()
            }
        } label: {
            ZStack(alignment: isOn ? .trailing : .leading) {
                Capsule()
                    .fill(
                        isOn
                            ? LinearGradient(
                                colors: [Color(hex: "6C63FF"), Color(hex: "8B5CF6")],
                                startPoint: .leading, endPoint: .trailing
                              )
                            : LinearGradient(
                                colors: [Color(hex: "E2E8F0"), Color(hex: "CBD5E1")],
                                startPoint: .leading, endPoint: .trailing
                              )
                    )
                    .frame(width: 56, height: 32)

                Circle()
                    .fill(.white)
                    .frame(width: 26, height: 26)
                    .shadow(color: .black.opacity(0.15), radius: 4, y: 2)
                    .padding(3)
            }
        }
        .buttonStyle(.plain)
    }
}

struct ToggleShowcase: View {
    @State private var darkMode = false
    @State private var notifications = true

    var body: some View {
        VStack(spacing: 16) {
            HStack {
                Label("Dark Mode", systemImage: "moon.fill")
                Spacer()
                PremiumToggle(isOn: $darkMode)
            }
            Divider()
            HStack {
                Label("Notifications", systemImage: "bell.fill")
                Spacer()
                PremiumToggle(isOn: $notifications)
            }
        }
        .padding(20)
        .background(Color(.secondarySystemGroupedBackground))
        .cornerRadius(16)
        .padding(20)
    }
}

12. Swipeable Card Stack

struct SwipeableCardStack: View {
    @State private var cards: [CardData] = [
        CardData(title: "Explore Tokyo", color: Color(hex: "6C63FF"), icon: "airplane"),
        CardData(title: "Visit Paris", color: Color(hex: "EC4899"), icon: "building.columns.fill"),
        CardData(title: "Surf Bali", color: Color(hex: "06B6D4"), icon: "water.waves"),
        CardData(title: "Hike Patagonia", color: Color(hex: "2D9F6F"), icon: "mountain.2.fill"),
        CardData(title: "Safari Kenya", color: Color(hex: "F59E0B"), icon: "leaf.fill"),
    ]

    struct CardData: Identifiable {
        let id = UUID()
        let title: String
        let color: Color
        let icon: String
    }

    var body: some View {
        ZStack {
            ForEach(Array(cards.enumerated().reversed()), id: \.element.id) { index, card in
                SwipeCard(card: card) {
                    withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) {
                        cards.removeAll { $0.id == card.id }
                    }
                }
                .scaleEffect(1.0 - CGFloat(index) * 0.04)
                .offset(y: CGFloat(index) * 8)
                .allowsHitTesting(index == 0)
            }
        }
        .padding(32)
    }
}

struct SwipeCard: View {
    let card: SwipeableCardStack.CardData
    let onRemove: () -> Void

    @State private var offset: CGSize = .zero
    @State private var rotation: Double = 0

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: card.icon)
                .font(.system(size: 60))
                .foregroundStyle(.white)

            Text(card.title)
                .font(.title.weight(.bold))
                .foregroundStyle(.white)

            HStack(spacing: 40) {
                Image(systemName: "xmark")
                    .font(.title2.weight(.bold))
                    .foregroundStyle(.white.opacity(offset.width < -20 ? 1 : 0.3))
                Image(systemName: "heart.fill")
                    .font(.title2.weight(.bold))
                    .foregroundStyle(.white.opacity(offset.width > 20 ? 1 : 0.3))
            }
        }
        .frame(maxWidth: .infinity)
        .frame(height: 400)
        .background(
            LinearGradient(
                colors: [card.color, card.color.opacity(0.7)],
                startPoint: .topLeading,
                endPoint: .bottomTrailing
            ),
            in: RoundedRectangle(cornerRadius: 24)
        )
        .shadow(color: card.color.opacity(0.3), radius: 16, y: 8)
        .offset(offset)
        .rotationEffect(.degrees(rotation))
        .gesture(
            DragGesture()
                .onChanged { value in
                    offset = value.translation
                    rotation = Double(value.translation.width / 20)
                }
                .onEnded { value in
                    if abs(value.translation.width) > 120 {
                        withAnimation(.easeOut(duration: 0.3)) {
                            offset = CGSize(
                                width: value.translation.width > 0 ? 500 : -500,
                                height: 0
                            )
                        }
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                            onRemove()
                        }
                    } else {
                        withAnimation(.spring(response: 0.4, dampingFraction: 0.6)) {
                            offset = .zero
                            rotation = 0
                        }
                    }
                }
        )
    }
}

13. Pull-to-Refresh with Custom Animation

struct CustomRefreshView: View {
    @State private var items = (1...20).map { "Item \($0)" }
    @State private var isRefreshing = false

    var body: some View {
        NavigationStack {
            List {
                ForEach(items, id: \.self) { item in
                    HStack(spacing: 12) {
                        Circle()
                            .fill(
                                LinearGradient(
                                    colors: [Color(hex: "6C63FF"), Color(hex: "A78BFA")],
                                    startPoint: .topLeading,
                                    endPoint: .bottomTrailing
                                )
                            )
                            .frame(width: 40, height: 40)
                            .overlay(
                                Image(systemName: "star.fill")
                                    .foregroundStyle(.white)
                                    .font(.caption)
                            )
                        Text(item)
                            .font(.body)
                    }
                    .padding(.vertical, 4)
                }
            }
            .refreshable {
                isRefreshing = true
                try? await Task.sleep(for: .seconds(2))
                items.shuffle()
                isRefreshing = false
            }
            .navigationTitle("Feed")
        }
    }
}

14. Skeleton Loading / Shimmer Effect

struct ShimmerEffect: ViewModifier {
    @State private var phase: CGFloat = 0

    func body(content: Content) -> some View {
        content
            .overlay(
                LinearGradient(
                    stops: [
                        .init(color: .clear, location: phase - 0.2),
                        .init(color: .white.opacity(0.5), location: phase),
                        .init(color: .clear, location: phase + 0.2),
                    ],
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
                .mask(content)
            )
            .onAppear {
                withAnimation(.linear(duration: 1.5).repeatForever(autoreverses: false)) {
                    phase = 1.2
                }
            }
    }
}

extension View {
    func shimmer() -> some View {
        modifier(ShimmerEffect())
    }
}

struct SkeletonLoadingCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            // Avatar + name skeleton
            HStack(spacing: 12) {
                Circle()
                    .fill(Color(.systemGray5))
                    .frame(width: 44, height: 44)

                VStack(alignment: .leading, spacing: 6) {
                    RoundedRectangle(cornerRadius: 4)
                        .fill(Color(.systemGray5))
                        .frame(width: 120, height: 14)

                    RoundedRectangle(cornerRadius: 4)
                        .fill(Color(.systemGray5))
                        .frame(width: 80, height: 10)
                }
            }

            // Image placeholder
            RoundedRectangle(cornerRadius: 12)
                .fill(Color(.systemGray5))
                .frame(height: 180)

            // Text lines
            RoundedRectangle(cornerRadius: 4)
                .fill(Color(.systemGray5))
                .frame(height: 14)

            RoundedRectangle(cornerRadius: 4)
                .fill(Color(.systemGray5))
                .frame(width: 250, height: 14)

            RoundedRectangle(cornerRadius: 4)
                .fill(Color(.systemGray5))
                .frame(width: 180, height: 14)
        }
        .padding(16)
        .shimmer()
    }
}

struct SkeletonDemo: View {
    @State private var isLoading = true

    var body: some View {
        VStack {
            if isLoading {
                SkeletonLoadingCard()
                SkeletonLoadingCard()
            } else {
                Text("Content loaded!")
                    .font(.title)
            }
        }
        .onAppear {
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                withAnimation { isLoading = false }
            }
        }
    }
}

15. Toast / Snackbar Notification

struct ToastModifier: ViewModifier {
    @Binding var isShowing: Bool
    let message: String
    let icon: String
    let color: Color

    func body(content: Content) -> some View {
        ZStack(alignment: .top) {
            content

            if isShowing {
                HStack(spacing: 12) {
                    Image(systemName: icon)
                        .font(.body.weight(.semibold))
                        .foregroundStyle(.white)

                    Text(message)
                        .font(.subheadline.weight(.medium))
                        .foregroundStyle(.white)

                    Spacer()

                    Button {
                        withAnimation(.spring(response: 0.3)) { isShowing = false }
                    } label: {
                        Image(systemName: "xmark")
                            .font(.caption.weight(.bold))
                            .foregroundStyle(.white.opacity(0.7))
                    }
                }
                .padding(16)
                .background(color, in: RoundedRectangle(cornerRadius: 14))
                .shadow(color: color.opacity(0.3), radius: 12, y: 6)
                .padding(.horizontal, 16)
                .padding(.top, 8)
                .transition(.move(edge: .top).combined(with: .opacity))
                .onAppear {
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        withAnimation(.spring(response: 0.3)) { isShowing = false }
                    }
                }
            }
        }
        .animation(.spring(response: 0.4, dampingFraction: 0.8), value: isShowing)
    }
}

extension View {
    func toast(isShowing: Binding<Bool>, message: String, icon: String = "checkmark.circle.fill", color: Color = Color(hex: "2D9F6F")) -> some View {
        modifier(ToastModifier(isShowing: isShowing, message: message, icon: icon, color: color))
    }
}

struct ToastDemo: View {
    @State private var showSuccess = false
    @State private var showError = false

    var body: some View {
        VStack(spacing: 16) {
            Button("Show Success") {
                showSuccess = true
            }
            .buttonStyle(.borderedProminent)

            Button("Show Error") {
                showError = true
            }
            .buttonStyle(.bordered)
        }
        .toast(isShowing: $showSuccess, message: "Saved successfully!")
        .toast(isShowing: $showError, message: "Something went wrong.", icon: "exclamationmark.circle.fill", color: Color(hex: "DC2626"))
    }
}

16. Expandable Card with matchedGeometryEffect

struct ExpandableCardDemo: View {
    @Namespace private var animation
    @State private var selectedCard: Int? = nil

    let cards = [
        (title: "Design", icon: "paintbrush.fill", color: Color(hex: "8B5CF6")),
        (title: "Develop", icon: "chevron.left.forwardslash.chevron.right", color: Color(hex: "0A6EBD")),
        (title: "Deploy", icon: "rocket.fill", color: Color(hex: "2D9F6F")),
    ]

    var body: some View {
        ZStack {
            // Grid of collapsed cards
            if selectedCard == nil {
                VStack(spacing: 12) {
                    ForEach(0..<cards.count, id: \.self) { index in
                        let card = cards[index]
                        HStack(spacing: 16) {
                            Image(systemName: card.icon)
                                .font(.title2)
                                .foregroundStyle(.white)
                                .frame(width: 48, height: 48)
                                .background(card.color, in: RoundedRectangle(cornerRadius: 12))
                                .matchedGeometryEffect(id: "icon\(index)", in: animation)

                            Text(card.title)
                                .font(.headline)
                                .matchedGeometryEffect(id: "title\(index)", in: animation)

                            Spacer()

                            Image(systemName: "chevron.right")
                                .foregroundStyle(.tertiary)
                        }
                        .padding(16)
                        .background(
                            RoundedRectangle(cornerRadius: 16)
                                .fill(Color(.secondarySystemGroupedBackground))
                                .matchedGeometryEffect(id: "bg\(index)", in: animation)
                        )
                        .onTapGesture {
                            withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                                selectedCard = index
                            }
                        }
                    }
                }
                .padding(16)
            }

            // Expanded card
            if let selected = selectedCard {
                let card = cards[selected]
                VStack(spacing: 20) {
                    HStack {
                        Button {
                            withAnimation(.spring(response: 0.5, dampingFraction: 0.8)) {
                                selectedCard = nil
                            }
                        } label: {
                            Image(systemName: "xmark")
                                .font(.headline)
                                .foregroundStyle(.secondary)
                                .frame(width: 36, height: 36)
                                .background(.ultraThinMaterial, in: Circle())
                        }
                        Spacer()
                    }

                    Image(systemName: card.icon)
                        .font(.system(size: 48))
                        .foregroundStyle(.white)
                        .frame(width: 96, height: 96)
                        .background(card.color, in: RoundedRectangle(cornerRadius: 24))
                        .matchedGeometryEffect(id: "icon\(selected)", in: animation)

                    Text(card.title)
                        .font(.largeTitle.weight(.bold))
                        .matchedGeometryEffect(id: "title\(selected)", in: animation)

                    Text("This is the expanded view for the \(card.title) card. Here you can place detailed content, forms, or any additional UI elements.")
                        .font(.body)
                        .foregroundStyle(.secondary)
                        .multilineTextAlignment(.center)
                        .padding(.horizontal, 20)

                    Spacer()
                }
                .padding(24)
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .background(
                    RoundedRectangle(cornerRadius: 24)
                        .fill(Color(.secondarySystemGroupedBackground))
                        .matchedGeometryEffect(id: "bg\(selected)", in: animation)
                        .ignoresSafeArea()
                )
            }
        }
    }
}

17. Animated Gradient Background

struct AnimatedGradientBackground: View {
    @State private var animateGradient = false

    var body: some View {
        LinearGradient(
            colors: [
                Color(hex: "6C63FF"),
                Color(hex: "EC4899"),
                Color(hex: "06B6D4"),
                Color(hex: "8B5CF6"),
            ],
            startPoint: animateGradient ? .topLeading : .bottomLeading,
            endPoint: animateGradient ? .bottomTrailing : .topTrailing
        )
        .ignoresSafeArea()
        .onAppear {
            withAnimation(.easeInOut(duration: 5).repeatForever(autoreverses: true)) {
                animateGradient.toggle()
            }
        }
        .overlay(
            VStack(spacing: 16) {
                Text("Welcome Back")
                    .font(.largeTitle.weight(.bold))
                    .foregroundStyle(.white)
                Text("Your animated gradient background")
                    .font(.subheadline)
                    .foregroundStyle(.white.opacity(0.8))
            }
        )
    }
}

18. Blurred Header that Changes on Scroll

struct BlurredScrollHeader: View {
    @State private var scrollOffset: CGFloat = 0
    private let headerTitle = "Discover"

    var body: some View {
        ZStack(alignment: .top) {
            ScrollView {
                VStack(spacing: 0) {
                    // Spacer for the header
                    Color.clear.frame(height: 100)

                    // Content
                    LazyVStack(spacing: 12) {
                        ForEach(0..<25, id: \.self) { i in
                            HStack(spacing: 12) {
                                RoundedRectangle(cornerRadius: 10)
                                    .fill(
                                        LinearGradient(
                                            colors: [
                                                Color(hex: "6C63FF").opacity(Double(i % 5 + 1) / 5.0),
                                                Color(hex: "EC4899").opacity(Double(i % 5 + 1) / 5.0),
                                            ],
                                            startPoint: .topLeading,
                                            endPoint: .bottomTrailing
                                        )
                                    )
                                    .frame(width: 56, height: 56)
                                    .overlay(
                                        Image(systemName: "music.note")
                                            .foregroundStyle(.white)
                                    )
                                VStack(alignment: .leading, spacing: 4) {
                                    Text("Track \(i + 1)")
                                        .font(.headline)
                                    Text("Artist Name")
                                        .font(.subheadline)
                                        .foregroundStyle(.secondary)
                                }
                                Spacer()
                                Text("3:4\(i % 10)")
                                    .font(.caption)
                                    .foregroundStyle(.tertiary)
                            }
                            .padding(.horizontal, 16)
                            .padding(.vertical, 8)
                        }
                    }
                }
                .background(
                    GeometryReader { geo in
                        Color.clear.preference(
                            key: ScrollOffsetKey.self,
                            value: geo.frame(in: .named("scroll")).minY
                        )
                    }
                )
            }
            .coordinateSpace(name: "scroll")
            .onPreferenceChange(ScrollOffsetKey.self) { value in
                scrollOffset = value
            }

            // Floating header
            VStack(spacing: 0) {
                HStack {
                    Text(headerTitle)
                        .font(scrollOffset < -20 ? .headline : .largeTitle.weight(.bold))
                        .animation(.easeInOut(duration: 0.2), value: scrollOffset < -20)
                    Spacer()
                    Image(systemName: "magnifyingglass")
                        .font(.title3)
                        .foregroundStyle(.primary)
                }
                .padding(.horizontal, 16)
                .padding(.top, 56)
                .padding(.bottom, 12)
                .background(
                    Rectangle()
                        .fill(.ultraThinMaterial)
                        .opacity(scrollOffset < -10 ? 1 : 0)
                        .ignoresSafeArea(edges: .top)
                )
            }
        }
    }
}

struct ScrollOffsetKey: PreferenceKey {
    static var defaultValue: CGFloat = 0
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

19. Chip / Tag Flow Layout

struct FlowLayout: Layout {
    var spacing: CGFloat = 8

    func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
        let result = layout(proposal: proposal, subviews: subviews)
        return result.size
    }

    func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
        let result = layout(proposal: proposal, subviews: subviews)
        for (index, position) in result.positions.enumerated() {
            subviews[index].place(
                at: CGPoint(x: bounds.minX + position.x, y: bounds.minY + position.y),
                proposal: ProposedViewSize(result.sizes[index])
            )
        }
    }

    private func layout(proposal: ProposedViewSize, subviews: Subviews) -> LayoutResult {
        let maxWidth = proposal.width ?? .infinity
        var positions: [CGPoint] = []
        var sizes: [CGSize] = []
        var x: CGFloat = 0
        var y: CGFloat = 0
        var rowHeight: CGFloat = 0

        for subview in subviews {
            let size = subview.sizeThatFits(.unspecified)
            sizes.append(size)
            if x + size.width > maxWidth && x > 0 {
                x = 0
                y += rowHeight + spacing
                rowHeight = 0
            }
            positions.append(CGPoint(x: x, y: y))
            rowHeight = max(rowHeight, size.height)
            x += size.width + spacing
        }

        return LayoutResult(
            size: CGSize(width: maxWidth, height: y + rowHeight),
            positions: positions,
            sizes: sizes
        )
    }

    struct LayoutResult {
        var size: CGSize
        var positions: [CGPoint]
        var sizes: [CGSize]
    }
}

struct ChipView: View {
    let label: String
    let color: Color
    @State private var isSelected = false

    var body: some View {
        Button {
            withAnimation(.spring(response: 0.3)) {
                isSelected.toggle()
            }
        } label: {
            Text(label)
                .font(.subheadline.weight(.medium))
                .foregroundStyle(isSelected ? .white : color)
                .padding(.horizontal, 14)
                .padding(.vertical, 8)
                .background(
                    isSelected ? AnyShapeStyle(color) : AnyShapeStyle(color.opacity(0.1)),
                    in: Capsule()
                )
                .overlay(
                    Capsule()
                        .stroke(color.opacity(isSelected ? 0 : 0.3), lineWidth: 1)
                )
        }
        .buttonStyle(.plain)
    }
}

struct ChipFlowDemo: View {
    let tags = [
        ("SwiftUI", Color(hex: "6C63FF")),
        ("iOS 18", Color(hex: "EC4899")),
        ("Design", Color(hex: "2D9F6F")),
        ("Animation", Color(hex: "FF6B35")),
        ("Accessibility", Color(hex: "0A6EBD")),
        ("Performance", Color(hex: "8B5CF6")),
        ("Dark Mode", Color(hex: "1E1B4B")),
        ("Typography", Color(hex: "F59E0B")),
        ("Color System", Color(hex: "06B6D4")),
        ("Layout", Color(hex: "DC2626")),
    ]

    var body: some View {
        FlowLayout(spacing: 8) {
            ForEach(tags, id: \.0) { tag in
                ChipView(label: tag.0, color: tag.1)
            }
        }
        .padding(20)
    }
}

20. Rating Stars Component

struct RatingStars: View {
    @Binding var rating: Int
    let maxRating: Int
    let starSize: CGFloat
    let activeColor: Color
    let inactiveColor: Color

    init(
        rating: Binding<Int>,
        maxRating: Int = 5,
        starSize: CGFloat = 28,
        activeColor: Color = Color(hex: "F59E0B"),
        inactiveColor: Color = Color(hex: "E2E8F0")
    ) {
        self._rating = rating
        self.maxRating = maxRating
        self.starSize = starSize
        self.activeColor = activeColor
        self.inactiveColor = inactiveColor
    }

    var body: some View {
        HStack(spacing: 6) {
            ForEach(1...maxRating, id: \.self) { index in
                Image(systemName: index <= rating ? "star.fill" : "star")
                    .font(.system(size: starSize))
                    .foregroundStyle(index <= rating ? activeColor : inactiveColor)
                    .symbolEffect(.bounce, value: rating)
                    .onTapGesture {
                        withAnimation(.spring(response: 0.3, dampingFraction: 0.5)) {
                            rating = index
                        }
                    }
            }
        }
    }
}

struct RatingDemo: View {
    @State private var rating1 = 3
    @State private var rating2 = 4

    var body: some View {
        VStack(spacing: 32) {
            VStack(spacing: 8) {
                Text("Rate your experience")
                    .font(.headline)
                RatingStars(rating: $rating1)
                Text("\(rating1) out of 5")
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }

            VStack(spacing: 8) {
                Text("Custom style")
                    .font(.headline)
                RatingStars(
                    rating: $rating2,
                    starSize: 36,
                    activeColor: Color(hex: "EC4899"),
                    inactiveColor: Color(hex: "FCE7F3")
                )
            }
        }
        .padding(24)
    }
}

Bonus: Combining Patterns -- Premium App Screen

A full composition showing how multiple patterns work together.

struct PremiumHomeScreen: View {
    @State private var selectedTab = 0
    @State private var showToast = false

    var body: some View {
        ZStack {
            Color(.systemGroupedBackground)
                .ignoresSafeArea()

            ScrollView {
                VStack(spacing: 20) {
                    // Hero gradient header
                    ZStack(alignment: .bottomLeading) {
                        LinearGradient(
                            colors: [Color(hex: "6C63FF"), Color(hex: "8B5CF6"), Color(hex: "EC4899")],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                        .frame(height: 220)
                        .cornerRadius(24)

                        VStack(alignment: .leading, spacing: 8) {
                            Text("Good Morning")
                                .font(.subheadline)
                                .foregroundStyle(.white.opacity(0.8))
                            Text("Jane")
                                .font(.largeTitle.weight(.bold))
                                .foregroundStyle(.white)
                        }
                        .padding(24)
                    }
                    .padding(.horizontal, 16)

                    // Chip tags
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack(spacing: 8) {
                            ForEach(["All", "Design", "Code", "Health", "Finance"], id: \.self) { tag in
                                Text(tag)
                                    .font(.subheadline.weight(.medium))
                                    .foregroundStyle(tag == "All" ? .white : .primary)
                                    .padding(.horizontal, 16)
                                    .padding(.vertical, 8)
                                    .background(
                                        tag == "All"
                                            ? AnyShapeStyle(Color(hex: "6C63FF"))
                                            : AnyShapeStyle(Color(.secondarySystemGroupedBackground)),
                                        in: Capsule()
                                    )
                            }
                        }
                        .padding(.horizontal, 16)
                    }

                    // Dashboard grid
                    LazyVGrid(columns: [
                        GridItem(.flexible(), spacing: 12),
                        GridItem(.flexible(), spacing: 12),
                    ], spacing: 12) {
                        CircularProgressCard(
                            progress: 0.72,
                            title: "Tasks",
                            subtitle: "18 / 25",
                            color: Color(hex: "6C63FF")
                        )
                        CircularProgressCard(
                            progress: 0.45,
                            title: "Goals",
                            subtitle: "3 / 7",
                            color: Color(hex: "2D9F6F")
                        )
                    }
                    .padding(.horizontal, 16)

                    // Glass card
                    VStack(alignment: .leading, spacing: 12) {
                        HStack {
                            Image(systemName: "sparkles")
                                .foregroundStyle(Color(hex: "F59E0B"))
                            Text("Featured")
                                .font(.headline)
                            Spacer()
                            Text("NEW")
                                .font(.caption2.weight(.bold))
                                .foregroundStyle(.white)
                                .padding(.horizontal, 8)
                                .padding(.vertical, 3)
                                .background(Color(hex: "EC4899"), in: Capsule())
                        }
                        Text("Unlock premium features and take your productivity to the next level.")
                            .font(.subheadline)
                            .foregroundStyle(.secondary)

                        Button {
                            showToast = true
                        } label: {
                            Text("Upgrade Now")
                                .font(.subheadline.weight(.semibold))
                                .foregroundStyle(.white)
                                .frame(maxWidth: .infinity)
                                .padding(.vertical, 12)
                                .background(
                                    LinearGradient(
                                        colors: [Color(hex: "6C63FF"), Color(hex: "8B5CF6")],
                                        startPoint: .leading,
                                        endPoint: .trailing
                                    ),
                                    in: RoundedRectangle(cornerRadius: 12)
                                )
                        }
                    }
                    .padding(20)
                    .background(Color(.secondarySystemGroupedBackground))
                    .cornerRadius(20)
                    .padding(.horizontal, 16)

                    // Bottom spacing for tab bar
                    Color.clear.frame(height: 80)
                }
            }
        }
        .toast(isShowing: $showToast, message: "Welcome to Premium!")
    }
}

Quick Reference

Pattern

Key Techniques

Glass Morphism

.ultraThinMaterial, gradient border stroke

Neumorphism

Dual shadows (light + dark), same-as-background fill

Gradient Shadow

Shadow color matching card gradient

Onboarding

TabView(.page), custom indicators, matchedGeometryEffect

Parallax Hero

GeometryReader, offset based on minY

Bottom Sheet

DragGesture, snap points, spring animation

Animated Tab Bar

matchedGeometryEffect, @Namespace

Profile Card

Gradient header, overlapping avatar, stat row

Dashboard Cards

Circle().trim(), mini bar charts

FAB

Expand/collapse with spring, rotation

Custom Toggle

ZStack alignment toggle, spring animation

Swipe Cards

DragGesture, rotation, threshold-based removal

Pull-to-Refresh

.refreshable async modifier

Skeleton/Shimmer

ViewModifier, animated LinearGradient overlay

Toast

ViewModifier, auto-dismiss, slide transition

Expandable Card

matchedGeometryEffect, @Namespace

Animated Gradient

repeatForever animation on gradient points

Blurred Scroll Header

PreferenceKey, .ultraThinMaterial opacity

Chip Flow Layout

Custom Layout protocol, Capsule backgrounds

Rating Stars

symbolEffect(.bounce), tap gesture

---

# Third-Party Animation Integration
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-third-party-animations.html

← Back to all frameworks & guides
All articles →Design · Reference guideThird-Party Animation IntegrationRepository guidance for Third-Party Animation Integration. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Lottie Integration

Adding Lottie via SPM
UIKit: LottieAnimationView Basics
SwiftUI: UIViewRepresentable Wrapper
Complete SwiftUI LottieView Wrapper with Binding Controls
Playing Specific Frame Ranges
Color Value Providers for Dynamic Theming

Rive Integration

Adding Rive via SPM
RiveViewModel Basics
SwiftUI Integration
State Machines: Inputs, Triggers, Booleans, Numbers
Artboard and Animation Selection
Complete Interactive Rive Toggle Example

When to Use What

Decision Table
Summary Guidelines
File Size Comparison
Performance Characteristics

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Decide whether native is enough
↓2
Check dependency and license
↓3
Integrate an isolated effect
↓4
Test reduced motion

02 / ArchitectureResponsibility boundariesBoundary 1
Animation assetsBoundary 2
Integration boundaryBoundary 3
Native screenConnected responsibilities, not a required class hierarchy or an execution trace.
Guide for integrating Lottie and Rive animation libraries into iOS projects, with SwiftUI wrappers and usage patterns.

Lottie Integration

Lottie
 renders Adobe After Effects animations exported as JSON via the Bodymovin plugin. It is the industry standard for complex vector animations on iOS.

Adding Lottie via SPM

In Xcode: File > Add Package Dependencies, then enter:

https://github.com/airbnb/lottie-ios.git

Select the
Lottie
 library and add it to your target. Use the latest stable version (4.x+).

UIKit: LottieAnimationView Basics

import Lottie

let animationView = LottieAnimationView(name: "loading") // Loads loading.json from bundle
animationView.contentMode = .scaleAspectFit
animationView.loopMode = .loop
animationView.animationSpeed = 1.0
animationView.frame = CGRect(x: 0, y: 0, width: 200, height: 200)
view.addSubview(animationView)
animationView.play()

SwiftUI: UIViewRepresentable Wrapper

Lottie 4.x ships with a built-in
LottieView
 for SwiftUI. If you need more control, build a custom wrapper:

import SwiftUI
import Lottie

struct LottieAnimationUIView: UIViewRepresentable {
    let animationName: String
    var loopMode: LottieLoopMode = .loop
    var animationSpeed: CGFloat = 1.0
    @Binding var isPlaying: Bool

    func makeUIView(context: Context) -> LottieAnimationView {
        let view = LottieAnimationView(name: animationName)
        view.contentMode = .scaleAspectFit
        view.loopMode = loopMode
        view.animationSpeed = animationSpeed
        return view
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        uiView.loopMode = loopMode
        uiView.animationSpeed = animationSpeed

        if isPlaying {
            if !uiView.isAnimationPlaying {
                uiView.play()
            }
        } else {
            uiView.pause()
        }
    }
}

Complete SwiftUI LottieView Wrapper with Binding Controls

import SwiftUI
import Lottie

struct LottiePlayerView: UIViewRepresentable {
    let animationName: String
    var loopMode: LottieLoopMode = .loop
    var animationSpeed: CGFloat = 1.0
    @Binding var playbackState: PlaybackState

    enum PlaybackState {
        case playing
        case paused
        case stopped
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    func makeUIView(context: Context) -> LottieAnimationView {
        let animationView = LottieAnimationView(name: animationName)
        animationView.contentMode = .scaleAspectFit
        animationView.loopMode = loopMode
        animationView.animationSpeed = animationSpeed
        context.coordinator.animationView = animationView
        return animationView
    }

    func updateUIView(_ uiView: LottieAnimationView, context: Context) {
        uiView.loopMode = loopMode
        uiView.animationSpeed = animationSpeed

        switch playbackState {
        case .playing:
            if !uiView.isAnimationPlaying {
                uiView.play()
            }
        case .paused:
            uiView.pause()
        case .stopped:
            uiView.stop()
        }
    }

    class Coordinator {
        weak var animationView: LottieAnimationView?
    }
}

// Usage
struct LottieDemo: View {
    @State private var playbackState: LottiePlayerView.PlaybackState = .playing
    @State private var speed: CGFloat = 1.0

    var body: some View {
        VStack(spacing: 24) {
            LottiePlayerView(
                animationName: "confetti",
                loopMode: .loop,
                animationSpeed: speed,
                playbackState: $playbackState
            )
            .frame(width: 300, height: 300)

            HStack(spacing: 16) {
                Button("Play") { playbackState = .playing }
                    .buttonStyle(.borderedProminent)

                Button("Pause") { playbackState = .paused }
                    .buttonStyle(.bordered)

                Button("Stop") { playbackState = .stopped }
                    .buttonStyle(.bordered)
            }

            VStack {
                Text("Speed: \(speed, specifier: "%.1f")x")
                    .font(.subheadline)
                Slider(value: $speed, in: 0.1...3.0, step: 0.1)
            }
            .padding(.horizontal)
        }
        .padding()
    }
}

Playing Specific Frame Ranges

import Lottie

// Play frames 0 through 60
let animationView = LottieAnimationView(name: "multiSection")
animationView.play(fromFrame: 0, toFrame: 60, loopMode: .playOnce)

// Play a named marker range (markers set in After Effects)
animationView.play(fromMarker: "start", toMarker: "end", loopMode: .loop)

// Jump to a specific progress (0.0 to 1.0)
animationView.currentProgress = 0.5

Color Value Providers for Dynamic Theming

import Lottie

let animationView = LottieAnimationView(name: "icon")

// Override a specific color in the animation
let colorProvider = ColorValueProvider(UIColor.systemBlue.lottieColorValue)
animationView.setValueProvider(
    colorProvider,
    keypath: AnimationKeypath(keypath: "**.Fill 1.Color")
)

// Use a dynamic color block
let dynamicProvider = ColorValueProvider { _ in
    return UIColor.tintColor.lottieColorValue
}
animationView.setValueProvider(
    dynamicProvider,
    keypath: AnimationKeypath(keypath: "**.Stroke 1.Color")
)

Rive Integration

Rive
 is a real-time animation platform that supports state machines, making animations interactive and responsive to user input. Rive files are typically smaller than Lottie JSON.

Adding Rive via SPM

In Xcode: File > Add Package Dependencies, then enter:

https://github.com/rive-app/rive-ios.git

Select the
RiveRuntime
 library and add it to your target.

RiveViewModel Basics

import RiveRuntime

// Load a .riv file from the bundle
let riveViewModel = RiveViewModel(fileName: "animated_icon")

// In UIKit
let riveView = riveViewModel.createRiveView()
view.addSubview(riveView)

SwiftUI Integration

Rive provides a built-in SwiftUI view through
RiveViewModel
:

import SwiftUI
import RiveRuntime

struct RiveAnimationView: View {
    var viewModel = RiveViewModel(fileName: "loading_spinner")

    var body: some View {
        viewModel.view()
            .frame(width: 200, height: 200)
    }
}

State Machines: Inputs, Triggers, Booleans, Numbers

Rive state machines let you control animation states through inputs defined in the Rive editor.

import SwiftUI
import RiveRuntime

struct RiveStateMachineDemo: View {
    var viewModel = RiveViewModel(fileName: "interactive_button", stateMachineName: "State Machine 1")

    var body: some View {
        VStack(spacing: 20) {
            viewModel.view()
                .frame(width: 300, height: 200)

            // Trigger a one-shot input
            Button("Fire Trigger") {
                viewModel.triggerInput("pressed")
            }

            // Toggle a boolean input
            Button("Toggle Hover") {
                viewModel.setInput("isHovered", value: true)
            }

            // Set a numeric input
            Button("Set Progress") {
                viewModel.setInput("progress", value: 0.75)
            }
        }
    }
}

Artboard and Animation Selection

import RiveRuntime

// Select a specific artboard and animation
let viewModel = RiveViewModel(
    fileName: "multi_artboard",
    artboardName: "IconArtboard",
    animationName: "idle"
)

// Switch animation at runtime
viewModel.play(animationName: "active")
viewModel.pause()
viewModel.stop()

Complete Interactive Rive Toggle Example

import SwiftUI
import RiveRuntime

struct RiveToggle: View {
    @State private var isOn = false
    var viewModel = RiveViewModel(fileName: "toggle_switch", stateMachineName: "Toggle Machine")

    var body: some View {
        VStack(spacing: 24) {
            viewModel.view()
                .frame(width: 120, height: 60)
                .onTapGesture {
                    isOn.toggle()
                    viewModel.setInput("isOn", value: isOn)
                }

            Text(isOn ? "Enabled" : "Disabled")
                .font(.headline)
                .foregroundStyle(isOn ? .green : .secondary)
        }
    }
}

// A more complete settings screen with Rive toggles
struct RiveSettingsView: View {
    @State private var notificationsOn = true
    @State private var darkModeOn = false

    var notificationsVM = RiveViewModel(fileName: "toggle_switch", stateMachineName: "Toggle Machine")
    var darkModeVM = RiveViewModel(fileName: "toggle_switch", stateMachineName: "Toggle Machine")

    var body: some View {
        NavigationStack {
            List {
                HStack {
                    Label("Notifications", systemImage: "bell.fill")
                    Spacer()
                    notificationsVM.view()
                        .frame(width: 60, height: 30)
                        .onTapGesture {
                            notificationsOn.toggle()
                            notificationsVM.setInput("isOn", value: notificationsOn)
                        }
                }

                HStack {
                    Label("Dark Mode", systemImage: "moon.fill")
                    Spacer()
                    darkModeVM.view()
                        .frame(width: 60, height: 30)
                        .onTapGesture {
                            darkModeOn.toggle()
                            darkModeVM.setInput("isOn", value: darkModeOn)
                        }
                }
            }
            .navigationTitle("Settings")
        }
    }
}

When to Use What

Decision Table

Use Case

Recommended

Why

Complex vector animations from After Effects

Lottie

Direct Bodymovin export, massive community library

One-shot success/error/loading animations

Lottie

Easy to drop in, many free animations on LottieFiles

Splash screen or onboarding animations

Lottie

Smooth, high-fidelity playback

Interactive toggles, buttons, switches

Rive

State machines handle input-driven transitions

Animations that respond to data (progress, score)

Rive

Number inputs drive animations smoothly

Character animations with multiple states

Rive

State machine graph handles complex state logic

Simple fade, scale, slide transitions

Native SwiftUI

No dependency needed, GPU-accelerated

Layout-driven animations (list reorder, insert/remove)

Native SwiftUI

Built-in transition and matchedGeometryEffect

Spring physics and gesture-driven animations

Native SwiftUI

UIViewPropertyAnimator or SwiftUI springs

Animated app icons or dynamic backgrounds

Rive

Tiny file size, runtime compositing

Accessibility-sensitive animations

Native SwiftUI

Respects Reduce Motion automatically

Summary Guidelines

Choose Lottie when:

- You have a designer using After Effects who exports via Bodymovin
- You need to drop in pre-made animations from LottieFiles.com
- The animation is purely visual (no user interaction controls it)
- You need frame-accurate playback of complex vector art
- File size is not a primary concern (Lottie JSON can be large)

Choose Rive when:

- Animations need to react to user input (taps, drags, state changes)
- You want a single file with multiple animation states and transitions
- File size matters (Rive binary format is typically 5-10x smaller than Lottie JSON)
- Your designer uses the Rive editor (not After Effects)
- You need runtime color/property changes without value providers

Choose native SwiftUI/UIKit when:

- Animations are tied to state changes (show/hide, expand/collapse)
- You need gesture-driven interactive animations
- The animation is simple (fade, scale, slide, spring)
- You want zero third-party dependencies
- You need full accessibility support (Reduce Motion, VoiceOver)
- Performance is critical (native animations use Core Animation directly)

File Size Comparison

Format

Typical Size

Notes

Lottie JSON

10-500 KB

Can be compressed with dotLottie (.lottie)

Lottie dotLottie

2-100 KB

Compressed format, supported in lottie-ios 4.x

Rive (.riv)

2-50 KB

Binary format, very compact

Native code

0 KB

No additional assets needed

Performance Characteristics

Library

CPU Usage

GPU Usage

Memory

Best For

Lottie (Main Thread)

Medium-High

Low

Medium

Simple animations

Lottie (Core Animation)

Low

Medium

Low

Complex looping animations

Rive

Low

Medium

Low

Interactive animations

Native SwiftUI

Very Low

Low

Very Low

UI transitions

Core Animation

Very Low

Medium

Low

Custom layer animations

Lottie supports two rendering engines: the default Main Thread renderer and the Core Animation renderer. For looping animations, use Core Animation rendering (
LottieAnimationView.configuration = .init(renderingEngine: .coreAnimation)
) for better performance.

---

# iOS Typography System -- Complete Guide for Stunning…
https://nagarjuna2997.github.io/ios-agent-skill/guides/design-typography-system.html

← Back to all frameworks & guides
All articles →Design · Reference guideiOS Typography System -- Complete Guide for Stunning SwiftUI TextRepository guidance for iOS Typography System -- Complete Guide for Stunning SwiftUI Text. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

Overview
1. Apple's Built-In Text Styles

Combining Style with Weight

2. Font Designs
3. Custom Fonts

Registering Custom Fonts
Using Custom Fonts in SwiftUI
Font Extension for Clean Usage

4. SF Symbols Integration

Basic Usage
Symbol Rendering Modes
Variable Value Symbols
Symbol Effects (iOS 17+)

5. Dynamic Type Support

@ScaledMetric
minimumScaleFactor

6. Typography Hierarchy Best Practices
7. Text Effects

Gradient Text
Shadow Text
Outlined Text (Stroke)
Animated Text

8. Markdown Support in Text
9. AttributedString

AttributedString with Links and Dates

10. Stunning Text Treatment Compositions

Hero Header with Gradient and Blur
Pill Tag with Custom Typography
Statistic Display with Mixed Typography

Quick Reference

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Define text roles
↓2
Apply scalable styles
↓3
Test long localized text
↓4
Verify largest text sizes

02 / ArchitectureResponsibility boundariesBoundary 1
Semantic stylesBoundary 2
Font metricsBoundary 3
Adaptive layoutConnected responsibilities, not a required class hierarchy or an execution trace.
Overview

Typography accounts for roughly 80% of a UI's visual surface. Getting it right is the single most impactful design decision. This guide covers Apple's built-in type system, custom fonts, SF Symbols, Dynamic Type, and advanced text effects -- all with compilable SwiftUI code.

1. Apple's Built-In Text Styles

These styles scale automatically with Dynamic Type and ensure consistency across the system.

import SwiftUI

struct TextStyleCatalog: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 12) {
                Text("Large Title").font(.largeTitle)    // 34pt, used for top-level headers
                Text("Title").font(.title)               // 28pt, screen titles
                Text("Title 2").font(.title2)            // 22pt, section headers
                Text("Title 3").font(.title3)            // 20pt, sub-section headers
                Text("Headline").font(.headline)         // 17pt semibold, row labels
                Text("Subheadline").font(.subheadline)   // 15pt, secondary row labels
                Text("Body").font(.body)                 // 17pt, primary content
                Text("Callout").font(.callout)           // 16pt, annotation text
                Text("Footnote").font(.footnote)         // 13pt, timestamps, captions
                Text("Caption").font(.caption)           // 12pt, legal text
                Text("Caption 2").font(.caption2)        // 11pt, smallest readable
            }
            .padding(24)
        }
    }
}

Combining Style with Weight

struct WeightedTextExamples: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 10) {
            Text("Ultralight Title").font(.largeTitle.weight(.ultraLight))
            Text("Thin Title").font(.largeTitle.weight(.thin))
            Text("Light Title").font(.largeTitle.weight(.light))
            Text("Regular Title").font(.largeTitle.weight(.regular))
            Text("Medium Title").font(.largeTitle.weight(.medium))
            Text("Semibold Title").font(.largeTitle.weight(.semibold))
            Text("Bold Title").font(.largeTitle.weight(.bold))
            Text("Heavy Title").font(.largeTitle.weight(.heavy))
            Text("Black Title").font(.largeTitle.weight(.black))
        }
        .padding(24)
    }
}

2. Font Designs

Apple provides four design variants of San Francisco.

struct FontDesignShowcase: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            VStack(alignment: .leading, spacing: 4) {
                Text("Default (SF Pro)")
                    .font(.system(.title2, design: .default, weight: .bold))
                Text("Clean and neutral -- ideal for most apps")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text("Rounded (SF Rounded)")
                    .font(.system(.title2, design: .rounded, weight: .bold))
                Text("Friendly and approachable -- great for wellness, kids, casual")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text("Serif (New York)")
                    .font(.system(.title2, design: .serif, weight: .bold))
                Text("Editorial and elegant -- perfect for news, reading, luxury")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }

            VStack(alignment: .leading, spacing: 4) {
                Text("Monospaced (SF Mono)")
                    .font(.system(.title2, design: .monospaced, weight: .bold))
                Text("Technical and precise -- code editors, data, developer tools")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
        .padding(24)
    }
}

3. Custom Fonts

Registering Custom Fonts

Add
.ttf
 or
.otf
 files to your Xcode project.
Ensure they are added to the target's "Copy Bundle Resources" build phase.
Add each font filename to
Info.plist
 under the key
UIAppFonts
 (also called "Fonts provided by application").

<!-- Info.plist entry -->
<key>UIAppFonts</key>
<array>
    <string>Satoshi-Regular.otf</string>
    <string>Satoshi-Bold.otf</string>
    <string>Satoshi-Medium.otf</string>
</array>

Using Custom Fonts in SwiftUI

struct CustomFontDemo: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Custom Regular")
                .font(.custom("Satoshi-Regular", size: 17))

            Text("Custom Bold")
                .font(.custom("Satoshi-Bold", size: 28))

            // Relative to a text style (scales with Dynamic Type)
            Text("Custom with Dynamic Type")
                .font(.custom("Satoshi-Medium", size: 17, relativeTo: .body))
        }
        .padding(24)
    }
}

Font Extension for Clean Usage

extension Font {
    static func satoshi(_ weight: SatoshiWeight, size: CGFloat) -> Font {
        .custom(weight.rawValue, size: size)
    }

    static func satoshiRelative(_ weight: SatoshiWeight, size: CGFloat, relativeTo style: TextStyle) -> Font {
        .custom(weight.rawValue, size: size, relativeTo: style)
    }

    enum SatoshiWeight: String {
        case regular = "Satoshi-Regular"
        case medium = "Satoshi-Medium"
        case bold = "Satoshi-Bold"
    }
}

// Usage:
// Text("Hello").font(.satoshi(.bold, size: 24))

4. SF Symbols Integration

SF Symbols 5+ includes over 5,000 symbols that integrate seamlessly with text.

Basic Usage

struct SFSymbolsBasic: View {
    var body: some View {
        VStack(spacing: 16) {
            // Inline with text (symbols match font metrics automatically)
            Label("Favorites", systemImage: "heart.fill")
                .font(.title2)

            // Standalone image with font size
            Image(systemName: "arrow.up.right.circle.fill")
                .font(.system(size: 48))
                .foregroundStyle(.blue)

            // Symbol alongside text baseline
            HStack(alignment: .firstTextBaseline) {
                Image(systemName: "clock.fill")
                Text("5 minutes ago")
            }
            .font(.subheadline)
            .foregroundStyle(.secondary)
        }
    }
}

Symbol Rendering Modes

struct SymbolRenderingModes: View {
    var body: some View {
        VStack(spacing: 24) {
            // Monochrome -- single color
            Image(systemName: "cloud.sun.rain.fill")
                .symbolRenderingMode(.monochrome)
                .font(.system(size: 48))
                .foregroundStyle(.blue)

            // Hierarchical -- primary color with automatic opacity layers
            Image(systemName: "cloud.sun.rain.fill")
                .symbolRenderingMode(.hierarchical)
                .font(.system(size: 48))
                .foregroundStyle(.blue)

            // Palette -- explicit colors for each layer
            Image(systemName: "cloud.sun.rain.fill")
                .symbolRenderingMode(.palette)
                .font(.system(size: 48))
                .foregroundStyle(.gray, .yellow, .blue)

            // Multicolor -- system-defined colors
            Image(systemName: "cloud.sun.rain.fill")
                .symbolRenderingMode(.multicolor)
                .font(.system(size: 48))
        }
    }
}

Variable Value Symbols

struct VariableSymbolDemo: View {
    @State private var progress: Double = 0.7

    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "speaker.wave.3.fill", variableValue: progress)
                .font(.system(size: 48))
                .foregroundStyle(.blue)
                .contentTransition(.symbolEffect(.automatic))

            Slider(value: $progress, in: 0...1)
                .padding(.horizontal, 40)

            Image(systemName: "wifi", variableValue: progress)
                .font(.system(size: 48))
                .foregroundStyle(.green)
        }
        .padding()
    }
}

Symbol Effects (iOS 17+)

struct SymbolEffectsDemo: View {
    @State private var isFavorite = false
    @State private var bounceCount = 0

    var body: some View {
        VStack(spacing: 32) {
            // Bounce effect
            Image(systemName: "bell.fill")
                .font(.system(size: 44))
                .foregroundStyle(.orange)
                .symbolEffect(.bounce, value: bounceCount)
                .onTapGesture { bounceCount += 1 }

            // Pulse effect (continuous)
            Image(systemName: "heart.fill")
                .font(.system(size: 44))
                .foregroundStyle(.red)
                .symbolEffect(.pulse)

            // Replace transition
            Button {
                isFavorite.toggle()
            } label: {
                Image(systemName: isFavorite ? "heart.fill" : "heart")
                    .font(.system(size: 44))
                    .foregroundStyle(isFavorite ? .red : .gray)
                    .contentTransition(.symbolEffect(.replace))
            }

            // Breathe effect (continuous)
            Image(systemName: "lungs.fill")
                .font(.system(size: 44))
                .foregroundStyle(.teal)
                .symbolEffect(.breathe)

            // Scale effect
            Image(systemName: "star.fill")
                .font(.system(size: 44))
                .foregroundStyle(.yellow)
                .symbolEffect(.scale.up, isActive: isFavorite)
        }
        .padding()
    }
}

5. Dynamic Type Support

@ScaledMetric

Scale arbitrary numeric values proportionally to the user's Dynamic Type setting.

struct ScaledMetricDemo: View {
    @ScaledMetric(relativeTo: .title) var iconSize: CGFloat = 28
    @ScaledMetric(relativeTo: .body) var spacing: CGFloat = 12
    @ScaledMetric(relativeTo: .body) var cardPadding: CGFloat = 16

    var body: some View {
        HStack(spacing: spacing) {
            Image(systemName: "person.circle.fill")
                .font(.system(size: iconSize))
                .foregroundStyle(.blue)

            VStack(alignment: .leading, spacing: 4) {
                Text("John Appleseed")
                    .font(.headline)
                Text("iOS Developer")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
            }
        }
        .padding(cardPadding)
        .background(Color(.secondarySystemBackground))
        .cornerRadius(16)
    }
}

minimumScaleFactor

Prevent text from being clipped while still supporting Dynamic Type.

struct ScaleFactorDemo: View {
    var body: some View {
        Text("This very long title will shrink instead of truncating")
            .font(.title)
            .minimumScaleFactor(0.5)
            .lineLimit(1)
            .padding()
    }
}

6. Typography Hierarchy Best Practices

A clear hierarchy uses no more than 3-4 sizes with weight variation.

struct TypographyHierarchyCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            // Overline -- smallest, uppercase, colored
            Text("FEATURED")
                .font(.caption.weight(.bold))
                .foregroundStyle(Color(hex: "6C63FF"))
                .kerning(1.5)
                .padding(.bottom, 8)

            // Title -- largest element on card
            Text("The Art of Typography")
                .font(.title2.weight(.bold))
                .foregroundStyle(.primary)
                .padding(.bottom, 4)

            // Subtitle -- contextual info
            Text("Design Systems -- March 2026")
                .font(.subheadline)
                .foregroundStyle(.secondary)
                .padding(.bottom, 16)

            // Body -- main content
            Text("Great typography establishes visual hierarchy, guides the reader, and creates emotional resonance. In iOS, the San Francisco font family provides all the tools needed for world-class type.")
                .font(.body)
                .foregroundStyle(.primary)
                .lineSpacing(4)
                .padding(.bottom, 16)

            // Action -- button text
            Text("Read More")
                .font(.subheadline.weight(.semibold))
                .foregroundStyle(Color(hex: "6C63FF"))
        }
        .padding(24)
        .background(Color(.secondarySystemGroupedBackground))
        .cornerRadius(20)
        .padding(.horizontal, 16)
    }
}

7. Text Effects

Gradient Text

struct GradientTextView: View {
    var body: some View {
        Text("Gradient Text")
            .font(.system(size: 48, weight: .black, design: .rounded))
            .foregroundStyle(
                LinearGradient(
                    colors: [Color(hex: "8B5CF6"), Color(hex: "EC4899"), Color(hex: "06B6D4")],
                    startPoint: .leading,
                    endPoint: .trailing
                )
            )
    }
}

Shadow Text

struct ShadowTextView: View {
    var body: some View {
        VStack(spacing: 32) {
            // Subtle shadow
            Text("Soft Shadow")
                .font(.largeTitle.weight(.bold))
                .foregroundStyle(.white)
                .shadow(color: .black.opacity(0.3), radius: 8, y: 4)

            // Glow effect
            Text("Neon Glow")
                .font(.largeTitle.weight(.black))
                .foregroundStyle(Color(hex: "06B6D4"))
                .shadow(color: Color(hex: "06B6D4").opacity(0.6), radius: 12)
                .shadow(color: Color(hex: "06B6D4").opacity(0.3), radius: 24)
        }
        .padding(40)
        .background(.black)
    }
}

Outlined Text (Stroke)

struct OutlinedTextView: View {
    var body: some View {
        ZStack {
            // Stroke layer
            Text("BOLD")
                .font(.system(size: 72, weight: .black))
                .foregroundStyle(.clear)
                .overlay(
                    Text("BOLD")
                        .font(.system(size: 72, weight: .black))
                        .foregroundStyle(
                            LinearGradient(
                                colors: [Color(hex: "6C63FF"), Color(hex: "EC4899")],
                                startPoint: .topLeading,
                                endPoint: .bottomTrailing
                            )
                        )
                        .mask(
                            Text("BOLD")
                                .font(.system(size: 72, weight: .black))
                        )
                )
        }
    }
}

// Alternative approach using strokeBorder on custom shape
struct StrokedText: View {
    var body: some View {
        Text("OUTLINE")
            .font(.system(size: 64, weight: .black))
            .foregroundStyle(.clear)
            .overlay(
                Text("OUTLINE")
                    .font(.system(size: 64, weight: .black))
                    .foregroundStyle(
                        .linearGradient(
                            colors: [Color(hex: "FF6B35"), Color(hex: "F7C948")],
                            startPoint: .top,
                            endPoint: .bottom
                        )
                    )
            )
    }
}

Animated Text

struct AnimatedCounterText: View {
    @State private var value: Double = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("\(value, specifier: "%.0f")")
                .font(.system(size: 72, weight: .black, design: .rounded))
                .foregroundStyle(
                    LinearGradient(
                        colors: [Color(hex: "2D9F6F"), Color(hex: "22D3EE")],
                        startPoint: .top,
                        endPoint: .bottom
                    )
                )
                .contentTransition(.numericText())

            Button("Animate") {
                withAnimation(.spring(duration: 0.8, bounce: 0.2)) {
                    value = Double.random(in: 0...9999)
                }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

struct TypewriterText: View {
    let fullText: String
    @State private var displayedText = ""
    @State private var charIndex = 0

    var body: some View {
        Text(displayedText)
            .font(.title2.weight(.medium))
            .onAppear {
                Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
                    if charIndex < fullText.count {
                        let index = fullText.index(fullText.startIndex, offsetBy: charIndex)
                        displayedText += String(fullText[index])
                        charIndex += 1
                    } else {
                        timer.invalidate()
                    }
                }
            }
    }
}

8. Markdown Support in Text

SwiftUI Text views parse Markdown automatically starting in iOS 15.

struct MarkdownTextDemo: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("This is **bold** and this is *italic*.")

            Text("Visit [Apple](https://apple.com) for more.")

            Text("Use `code` in your text.")

            Text("~~Strikethrough~~ is supported too.")

            // Combine Markdown with font styling
            Text("**Premium Plan** -- $9.99/month")
                .font(.headline)

            // Multiline Markdown
            Text("""
            # Features
            - **Fast** performance
            - *Beautiful* design
            - `Clean` code
            """)
        }
        .padding(24)
    }
}

9. AttributedString

For rich text that goes beyond Markdown, use
AttributedString
.

struct AttributedStringDemo: View {
    var attributedGreeting: AttributedString {
        var hello = AttributedString("Hello ")
        hello.font = .title.weight(.light)
        hello.foregroundColor = .secondary

        var name = AttributedString("World")
        name.font = .title.weight(.bold)
        name.foregroundColor = .primary

        var emoji = AttributedString(" !")
        emoji.font = .title

        return hello + name + emoji
    }

    var highlightedText: AttributedString {
        var full = AttributedString("SwiftUI makes building beautiful apps incredibly fast and enjoyable.")
        full.font = .body

        if let range = full.range(of: "beautiful") {
            full[range].foregroundColor = Color(hex: "8B5CF6")
            full[range].font = .body.weight(.bold)
        }

        if let range = full.range(of: "fast") {
            full[range].foregroundColor = Color(hex: "2D9F6F")
            full[range].font = .body.weight(.bold)
        }

        return full
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text(attributedGreeting)
            Text(highlightedText)
                .lineSpacing(4)
        }
        .padding(24)
    }
}

AttributedString with Links and Dates

struct RichAttributedText: View {
    var formattedText: AttributedString {
        var text = AttributedString("Updated ")
        text.font = .footnote
        text.foregroundColor = .secondary

        var date = AttributedString(Date.now, format: .dateTime.month().day().year())
        date.font = .footnote.weight(.semibold)
        date.foregroundColor = .primary

        var separator = AttributedString(" -- ")
        separator.font = .footnote

        var link = AttributedString("View Source")
        link.font = .footnote.weight(.medium)
        link.foregroundColor = Color(hex: "0A6EBD")
        link.link = URL(string: "https://developer.apple.com")

        return text + date + separator + link
    }

    var body: some View {
        Text(formattedText)
            .padding()
    }
}

10. Stunning Text Treatment Compositions

Hero Header with Gradient and Blur

struct HeroTextHeader: View {
    var body: some View {
        ZStack {
            LinearGradient(
                colors: [Color(hex: "0B0B1A"), Color(hex: "1A1A2E")],
                startPoint: .top,
                endPoint: .bottom
            )
            .ignoresSafeArea()

            VStack(spacing: 12) {
                Text("Introducing")
                    .font(.title3.weight(.medium))
                    .foregroundStyle(.white.opacity(0.6))
                    .kerning(3)
                    .textCase(.uppercase)

                Text("Premium")
                    .font(.system(size: 64, weight: .black, design: .serif))
                    .foregroundStyle(
                        LinearGradient(
                            colors: [Color(hex: "F472B6"), Color(hex: "A78BFA"), Color(hex: "6C63FF")],
                            startPoint: .leading,
                            endPoint: .trailing
                        )
                    )

                Text("Crafted with care for those who\nappreciate the finer details.")
                    .font(.body)
                    .foregroundStyle(.white.opacity(0.5))
                    .multilineTextAlignment(.center)
                    .lineSpacing(4)
            }
        }
        .frame(height: 350)
    }
}

Pill Tag with Custom Typography

struct StyledTag: View {
    let text: String
    let color: Color

    var body: some View {
        Text(text.uppercased())
            .font(.caption2.weight(.bold))
            .kerning(1.2)
            .foregroundStyle(.white)
            .padding(.horizontal, 12)
            .padding(.vertical, 6)
            .background(color, in: Capsule())
    }
}

struct TagRow: View {
    var body: some View {
        HStack(spacing: 8) {
            StyledTag(text: "SwiftUI", color: Color(hex: "6C63FF"))
            StyledTag(text: "iOS 18", color: Color(hex: "EC4899"))
            StyledTag(text: "New", color: Color(hex: "2D9F6F"))
        }
    }
}

Statistic Display with Mixed Typography

struct StatDisplay: View {
    let value: String
    let unit: String
    let label: String

    var body: some View {
        VStack(spacing: 4) {
            HStack(alignment: .firstTextBaseline, spacing: 2) {
                Text(value)
                    .font(.system(size: 40, weight: .bold, design: .rounded))
                    .foregroundStyle(.primary)
                Text(unit)
                    .font(.title3.weight(.medium))
                    .foregroundStyle(.secondary)
            }
            Text(label)
                .font(.caption.weight(.medium))
                .foregroundStyle(.tertiary)
                .textCase(.uppercase)
                .kerning(1)
        }
    }
}

struct StatsRow: View {
    var body: some View {
        HStack(spacing: 32) {
            StatDisplay(value: "2.4", unit: "M", label: "Downloads")
            StatDisplay(value: "4.9", unit: "", label: "Rating")
            StatDisplay(value: "128", unit: "K", label: "Reviews")
        }
        .padding(24)
        .background(Color(.secondarySystemGroupedBackground))
        .cornerRadius(20)
    }
}

Quick Reference

Category

Key APIs

Text Styles

.largeTitle, .title, .title2, .title3, .headline, .subheadline, .body, .callout, .footnote, .caption, .caption2

Font Designs

.default, .rounded, .serif, .monospaced

Weights

.ultraLight through .black (9 levels)

Custom Fonts

.custom("Name", size:), .custom("Name", size:, relativeTo:)

SF Symbols

.symbolRenderingMode(), .symbolEffect(), variableValue:

Dynamic Type

@ScaledMetric, .minimumScaleFactor(), relativeTo:

Text Effects

.foregroundStyle() with gradients, .shadow(), .contentTransition()

Rich Text

Markdown in Text(), AttributedString

Kerning/Tracking

.kerning(), .tracking()

Line Spacing

.lineSpacing(), .lineLimit()

---

# ActivityKit & Live Activities
https://nagarjuna2997.github.io/ios-agent-skill/guides/frameworks-activitykit.html

← Back to all frameworks & guides
All articles →Core UI and Apps · Reference guideActivityKit & Live ActivitiesRepository guidance for ActivityKit. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

ActivityAttributes and ActivityContent
Starting a Live Activity
Updating a Live Activity
Ending a Live Activity
Dynamic Island Presentations
Lock Screen and Supporting Views
Push-to-Update with Push Tokens
ActivityKit Push Notification Payload
Timer and Progress Live Activities
StaleDate and Dismissal Policy
Complete Delivery Tracking Example
Widget Bundle Registration
Key Considerations

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Check activity availability
↓2
Request an activity
↓3
Update activity content
↓4
End the activity

02 / ArchitectureResponsibility boundariesBoundary 1
App stateBoundary 2
Activity contentBoundary 3
Live Activity presentationConnected responsibilities, not a required class hierarchy or an execution trace.
ActivityKit enables Live Activities that display real-time, glanceable content on the Lock Screen and Dynamic Island. Live Activities are ideal for tracking ongoing events like deliveries, sports scores, workouts, and ride-sharing trips.

ActivityAttributes and ActivityContent

ActivityAttributes define the static and dynamic data for a Live Activity. The nested
ContentState
 contains data that changes over time.

import ActivityKit
import Foundation

// Define the attributes for a delivery tracking Live Activity
struct DeliveryAttributes: ActivityAttributes {
    // Static data — set when the activity starts, never changes
    let orderNumber: String
    let restaurantName: String
    let estimatedDeliveryTime: Date

    // Dynamic data — updated throughout the activity lifecycle
    struct ContentState: Codable, Hashable {
        let status: DeliveryStatus
        let driverName: String
        let currentStep: Int
        let totalSteps: Int
        let estimatedMinutesRemaining: Int
    }
}

enum DeliveryStatus: String, Codable, Hashable {
    case preparing
    case pickedUp
    case onTheWay
    case nearbyDropoff
    case delivered

    var displayText: String {
        switch self {
        case .preparing: return "Preparing"
        case .pickedUp: return "Picked Up"
        case .onTheWay: return "On the Way"
        case .nearbyDropoff: return "Almost There"
        case .delivered: return "Delivered"
        }
    }

    var systemImage: String {
        switch self {
        case .preparing: return "fork.knife"
        case .pickedUp: return "bag.fill"
        case .onTheWay: return "car.fill"
        case .nearbyDropoff: return "mappin.and.ellipse"
        case .delivered: return "checkmark.circle.fill"
        }
    }
}

Starting a Live Activity

Request a Live Activity by providing initial content and a stale date. The system enforces a limit of one active Live Activity per app on iPhone.

import ActivityKit

class DeliveryTracker {
    var currentActivity: Activity<DeliveryAttributes>?

    func startTracking(orderNumber: String, restaurant: String, eta: Date) throws {
        // Check if Live Activities are enabled in Settings
        guard ActivityAuthorizationInfo().areActivitiesEnabled else {
            throw DeliveryError.activitiesDisabled
        }

        let attributes = DeliveryAttributes(
            orderNumber: orderNumber,
            restaurantName: restaurant,
            estimatedDeliveryTime: eta
        )

        let initialState = DeliveryAttributes.ContentState(
            status: .preparing,
            driverName: "",
            currentStep: 1,
            totalSteps: 4,
            estimatedMinutesRemaining: 45
        )

        let content = ActivityContent(
            state: initialState,
            staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: .now),
            relevanceScore: 75
        )

        do {
            currentActivity = try Activity.request(
                attributes: attributes,
                content: content,
                pushType: .token  // Enable push-to-update; use nil for local-only
            )
            print("Live Activity started: \(currentActivity?.id ?? "nil")")
        } catch {
            throw DeliveryError.failedToStart(error)
        }
    }
}

Updating a Live Activity

Update the dynamic content state at any time while the activity is active.

extension DeliveryTracker {
    func updateStatus(to status: DeliveryStatus, driver: String, step: Int, minutes: Int) async {
        guard let activity = currentActivity else { return }

        let updatedState = DeliveryAttributes.ContentState(
            status: status,
            driverName: driver,
            currentStep: step,
            totalSteps: 4,
            estimatedMinutesRemaining: minutes
        )

        // Set a new stale date with each update
        let staleDate = Calendar.current.date(byAdding: .minute, value: 10, to: .now)

        let updatedContent = ActivityContent(
            state: updatedState,
            staleDate: staleDate,
            relevanceScore: status == .nearbyDropoff ? 100 : 75
        )

        await activity.update(updatedContent)
    }
}

Ending a Live Activity

End activities with a final content state. The
dismissalPolicy
 controls how long the ended activity remains visible on the Lock Screen.

extension DeliveryTracker {
    func markDelivered() async {
        guard let activity = currentActivity else { return }

        let finalState = DeliveryAttributes.ContentState(
            status: .delivered,
            driverName: "Marcus",
            currentStep: 4,
            totalSteps: 4,
            estimatedMinutesRemaining: 0
        )

        let finalContent = ActivityContent(
            state: finalState,
            staleDate: nil
        )

        // .default: system decides when to remove (up to 4 hours)
        // .immediate: remove right away
        // .after(Date): remove after specified date
        await activity.end(finalContent, dismissalPolicy: .default)

        currentActivity = nil
    }

    func cancelActivity() async {
        guard let activity = currentActivity else { return }
        await activity.end(nil, dismissalPolicy: .immediate)
        currentActivity = nil
    }
}

Dynamic Island Presentations

Live Activities appear in three Dynamic Island presentations. All three must be implemented in the widget bundle.

import WidgetKit
import SwiftUI

struct DeliveryLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: DeliveryAttributes.self) { context in
            // LOCK SCREEN / STANDBY presentation
            LockScreenView(context: context)

        } dynamicIsland: { context in
            DynamicIsland {
                // EXPANDED — shown when user long-presses the Dynamic Island
                DynamicIslandExpandedRegion(.leading) {
                    Image(systemName: context.state.status.systemImage)
                        .font(.title2)
                        .foregroundStyle(.blue)
                }

                DynamicIslandExpandedRegion(.trailing) {
                    Text("\(context.state.estimatedMinutesRemaining) min")
                        .font(.headline)
                        .foregroundStyle(.secondary)
                }

                DynamicIslandExpandedRegion(.center) {
                    VStack(spacing: 4) {
                        Text(context.state.status.displayText)
                            .font(.headline)
                        Text(context.attributes.restaurantName)
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                }

                DynamicIslandExpandedRegion(.bottom) {
                    DeliveryProgressBar(
                        currentStep: context.state.currentStep,
                        totalSteps: context.state.totalSteps
                    )
                    .padding(.top, 4)
                }

            } compactLeading: {
                // COMPACT LEADING — left side of the pill
                Image(systemName: context.state.status.systemImage)
                    .foregroundStyle(.blue)

            } compactTrailing: {
                // COMPACT TRAILING — right side of the pill
                Text("\(context.state.estimatedMinutesRemaining)m")
                    .font(.caption2)
                    .foregroundStyle(.secondary)

            } minimal: {
                // MINIMAL — shown when multiple Live Activities are active
                Image(systemName: context.state.status.systemImage)
                    .foregroundStyle(.blue)
            }
            .keylineTint(.blue)
        }
    }
}

Lock Screen and Supporting Views

struct LockScreenView: View {
    let context: ActivityViewContext<DeliveryAttributes>

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack {
                VStack(alignment: .leading) {
                    Text(context.attributes.restaurantName)
                        .font(.headline)
                    Text("Order #\(context.attributes.orderNumber)")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
                Spacer()
                Text(context.state.status.displayText)
                    .font(.subheadline.bold())
                    .padding(.horizontal, 10)
                    .padding(.vertical, 4)
                    .background(.blue.opacity(0.2))
                    .clipShape(Capsule())
            }

            DeliveryProgressBar(
                currentStep: context.state.currentStep,
                totalSteps: context.state.totalSteps
            )

            HStack {
                if !context.state.driverName.isEmpty {
                    Label(context.state.driverName, systemImage: "person.fill")
                        .font(.caption)
                }
                Spacer()
                if context.state.estimatedMinutesRemaining > 0 {
                    Text("~\(context.state.estimatedMinutesRemaining) min remaining")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }

            // Show a message when content is stale
            if context.isStale {
                Label("Updating...", systemImage: "arrow.clockwise")
                    .font(.caption2)
                    .foregroundStyle(.orange)
            }
        }
        .padding()
        .activityBackgroundTint(.black.opacity(0.7))
        .activitySystemActionForegroundColor(.white)
    }
}

struct DeliveryProgressBar: View {
    let currentStep: Int
    let totalSteps: Int

    var body: some View {
        HStack(spacing: 4) {
            ForEach(1...totalSteps, id: \.self) { step in
                Capsule()
                    .fill(step <= currentStep ? Color.blue : Color.gray.opacity(0.3))
                    .frame(height: 4)
            }
        }
    }
}

Push-to-Update with Push Tokens

Register for push tokens to update Live Activities from your server. The token can change, so observe it continuously.

extension DeliveryTracker {
    func observePushToken() {
        guard let activity = currentActivity else { return }

        Task {
            for await pushToken in activity.pushTokenUpdates {
                let tokenString = pushToken.map { String(format: "%02x", $0) }.joined()
                print("Push token: \(tokenString)")

                // Send token to your server
                await sendTokenToServer(token: tokenString, activityID: activity.id)
            }
        }
    }

    private func sendTokenToServer(token: String, activityID: String) async {
        guard let url = URL(string: "https://api.example.com/live-activity/register") else { return }
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let body: [String: String] = [
            "activityId": activityID,
            "pushToken": token
        ]
        request.httpBody = try? JSONEncoder().encode(body)

        _ = try? await URLSession.shared.data(for: request)
    }
}

ActivityKit Push Notification Payload

Send this JSON payload from your server via APNs to update or end a Live Activity.

// APNs headers:
// apns-topic: <BundleID>.push-type.liveactivity
// apns-push-type: liveactivity

// Update payload
{
    "aps": {
        "timestamp": 1699900000,
        "event": "update",
        "content-state": {
            "status": "onTheWay",
            "driverName": "Marcus",
            "currentStep": 3,
            "totalSteps": 4,
            "estimatedMinutesRemaining": 12
        },
        "stale-date": 1699900600,
        "dismissal-date": 1699904200,
        "alert": {
            "title": "Delivery Update",
            "body": "Your order is on the way!"
        },
        "sound": "default",
        "relevance-score": 100
    }
}

// End payload
{
    "aps": {
        "timestamp": 1699901000,
        "event": "end",
        "dismissal-date": 1699904600,
        "content-state": {
            "status": "delivered",
            "driverName": "Marcus",
            "currentStep": 4,
            "totalSteps": 4,
            "estimatedMinutesRemaining": 0
        },
        "alert": {
            "title": "Order Delivered",
            "body": "Your food has arrived. Enjoy!"
        }
    }
}

Timer and Progress Live Activities

Use
Text
 with date-relative formatting for automatic countdown timers that the system updates without push notifications.

struct TimerLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: TimerAttributes.self) { context in
            HStack {
                VStack(alignment: .leading) {
                    Text(context.attributes.timerName)
                        .font(.headline)
                    // Automatic countdown timer — system updates this every second
                    Text(context.state.endTime, style: .timer)
                        .font(.system(.title, design: .monospaced))
                        .foregroundStyle(.blue)
                }
                Spacer()
                // Relative time display: "in 5 min"
                Text(context.state.endTime, style: .relative)
                    .font(.caption)
                    .foregroundStyle(.secondary)
            }
            .padding()
            .activityBackgroundTint(.black)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.center) {
                    Text(context.state.endTime, style: .timer)
                        .font(.system(.title, design: .monospaced))
                }
            } compactLeading: {
                Image(systemName: "timer")
            } compactTrailing: {
                Text(context.state.endTime, style: .timer)
                    .frame(width: 50)
                    .font(.caption2.monospacedDigit())
            } minimal: {
                // Progress ring for minimal view
                ProgressView(
                    timerInterval: context.state.startTime...context.state.endTime,
                    countsDown: true
                ) {
                    EmptyView()
                }
                .progressViewStyle(.circular)
                .tint(.blue)
            }
        }
    }
}

struct TimerAttributes: ActivityAttributes {
    let timerName: String

    struct ContentState: Codable, Hashable {
        let startTime: Date
        let endTime: Date
    }
}

StaleDate and Dismissal Policy

Control how stale data is displayed and when ended activities are removed.

// Observe activity state changes across the app lifecycle
func observeAllActivities() {
    Task {
        // Monitor activities for this attribute type
        for await activity in Activity<DeliveryAttributes>.activityUpdates {
            print("New activity: \(activity.id)")

            Task {
                for await state in activity.activityStateUpdates {
                    switch state {
                    case .active:
                        print("Activity is active")
                    case .stale:
                        // Content has passed its staleDate — refresh it
                        print("Activity is stale — requesting update")
                        await refreshActivityFromServer(activity)
                    case .dismissed:
                        print("Activity was dismissed by user or system")
                    case .ended:
                        print("Activity has ended")
                    @unknown default:
                        break
                    }
                }
            }
        }
    }
}

func refreshActivityFromServer(_ activity: Activity<DeliveryAttributes>) async {
    guard let freshState = await fetchLatestState(for: activity.attributes.orderNumber) else {
        return
    }
    let content = ActivityContent(
        state: freshState,
        staleDate: Calendar.current.date(byAdding: .minute, value: 10, to: .now)
    )
    await activity.update(content)
}

func fetchLatestState(for orderNumber: String) async -> DeliveryAttributes.ContentState? {
    // Fetch from your API
    return nil
}

Complete Delivery Tracking Example

A full Livewire-style manager that coordinates the entire Live Activity lifecycle.

import ActivityKit
import Foundation

@MainActor
@Observable
class LiveDeliveryManager {
    var isTracking = false
    var currentStatus: DeliveryStatus = .preparing
    private var activity: Activity<DeliveryAttributes>?
    private var tokenObservationTask: Task<Void, Never>?

    var areActivitiesEnabled: Bool {
        ActivityAuthorizationInfo().areActivitiesEnabled
    }

    func startDelivery(order: String, restaurant: String, eta: Date) async throws {
        guard areActivitiesEnabled else {
            throw DeliveryError.activitiesDisabled
        }

        // End any existing activity first
        if activity != nil {
            await endDelivery()
        }

        let attributes = DeliveryAttributes(
            orderNumber: order,
            restaurantName: restaurant,
            estimatedDeliveryTime: eta
        )

        let initialState = DeliveryAttributes.ContentState(
            status: .preparing,
            driverName: "",
            currentStep: 1,
            totalSteps: 4,
            estimatedMinutesRemaining: 45
        )

        let content = ActivityContent(
            state: initialState,
            staleDate: Calendar.current.date(byAdding: .minute, value: 15, to: .now),
            relevanceScore: 50
        )

        activity = try Activity.request(
            attributes: attributes,
            content: content,
            pushType: .token
        )

        isTracking = true
        currentStatus = .preparing
        startObservingPushToken()
        startObservingState()
    }

    func update(status: DeliveryStatus, driver: String, step: Int, minutes: Int) async {
        guard let activity else { return }

        let state = DeliveryAttributes.ContentState(
            status: status,
            driverName: driver,
            currentStep: step,
            totalSteps: 4,
            estimatedMinutesRemaining: minutes
        )

        let content = ActivityContent(
            state: state,
            staleDate: Calendar.current.date(byAdding: .minute, value: 10, to: .now),
            relevanceScore: status == .nearbyDropoff ? 100 : 75
        )

        await activity.update(content)
        currentStatus = status
    }

    func endDelivery() async {
        guard let activity else { return }

        let finalState = DeliveryAttributes.ContentState(
            status: .delivered,
            driverName: "Driver",
            currentStep: 4,
            totalSteps: 4,
            estimatedMinutesRemaining: 0
        )

        let content = ActivityContent(state: finalState, staleDate: nil)
        await activity.end(content, dismissalPolicy: .after(
            Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
        ))

        tokenObservationTask?.cancel()
        self.activity = nil
        isTracking = false
        currentStatus = .delivered
    }

    private func startObservingPushToken() {
        guard let activity else { return }
        tokenObservationTask?.cancel()

        tokenObservationTask = Task {
            for await token in activity.pushTokenUpdates {
                let tokenString = token.map { String(format: "%02x", $0) }.joined()
                await registerToken(tokenString, activityID: activity.id)
            }
        }
    }

    private func startObservingState() {
        guard let activity else { return }

        Task {
            for await state in activity.activityStateUpdates {
                switch state {
                case .dismissed, .ended:
                    self.isTracking = false
                    self.activity = nil
                default:
                    break
                }
            }
        }
    }

    private func registerToken(_ token: String, activityID: String) async {
        // Send to your backend
        print("Registering token \(token) for activity \(activityID)")
    }
}

enum DeliveryError: LocalizedError {
    case activitiesDisabled
    case failedToStart(Error)

    var errorDescription: String? {
        switch self {
        case .activitiesDisabled:
            return "Live Activities are disabled in Settings."
        case .failedToStart(let error):
            return "Failed to start activity: \(error.localizedDescription)"
        }
    }
}

Widget Bundle Registration

Register the Live Activity widget alongside your other widgets.

import WidgetKit
import SwiftUI

@main
struct AppWidgets: WidgetBundle {
    var body: some Widget {
        DeliveryLiveActivity()
        TimerLiveActivity()
        // Other widgets...
    }
}

Key Considerations

Size limit
: Live Activity UI is rendered at a fixed size; keep content concise.
Update frequency
: The system may throttle updates. Budget approximately one update per hour for push updates; local updates have a higher budget.
Stale date
: Always set a stale date so your UI can show a refresh indicator when data is old.
Push payload size
: The APNs payload for Live Activities must be under 4 KB.
Background
: Live Activities use
activityBackgroundTint
 and
activitySystemActionForegroundColor
 — standard SwiftUI background modifiers do not work.
Availability
: ActivityKit requires iOS 16.1+. Dynamic Island requires iPhone 14 Pro and later.
Info.plist
: Add
NSSupportsLiveActivities
 set to
YES
 in your app target's Info.plist.

---

# App Clips
https://nagarjuna2997.github.io/ios-agent-skill/guides/frameworks-app-clips.html

← Back to all frameworks & guides
All articles →Core UI and Apps · Reference guideApp ClipsRepository guidance for App Clips. Examples, decisions, and verification limits from the maintained source guide.Read or improve the source
 ·
All guidesOn this pageVisual overview: workflow and architecture

App Clip Target Setup in Xcode
Invocation URLs and Advanced Matching
NFC Tag and QR Code Triggers
App Clip Card Configuration
Size Limitations (15 MB)
App Group Data Handoff to Full App
Location Confirmation with CLAppClipCodeLocation
SKOverlay for Full App Promotion
Complete App Clip Example
Key Considerations

Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result1
Resolve invocation URL
↓2
Load the focused experience
↓3
Complete the short task
↓4
Offer full-app continuity

02 / ArchitectureResponsibility boundariesBoundary 1
Invocation linkBoundary 2
App Clip targetBoundary 3
Shared domain logicConnected responsibilities, not a required class hierarchy or an execution trace.
App Clips are lightweight versions of your app that users can discover and launch instantly from NFC tags, QR codes, Safari Smart Banners, Maps, and Messages. They provide focused functionality without requiring a full app download, with a strict 15 MB size limit.

App Clip Target Setup in Xcode

An App Clip is a separate target in your Xcode project that shares code with the main app through shared frameworks or file membership.

// 1. In Xcode: File > New > Target > App Clip
// 2. The App Clip target gets its own bundle identifier:
//    Main app: com.example.myapp
//    App Clip: com.example.myapp.Clip

// 3. App Clip entry point — same as a regular SwiftUI app
import SwiftUI

@main
struct MyAppClip: App {
    var body: some Scene {
        WindowGroup {
            AppClipRootView()
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
                    // Handle the invocation URL
                    handleInvocation(activity)
                }
        }
    }

    private func handleInvocation(_ activity: NSUserActivity) {
        guard let url = activity.webpageURL else { return }
        // Parse the URL to determine what to show
        // e.g., https://example.com/store/123 → show store 123
        AppClipRouter.shared.route(to: url)
    }
}

// 4. Share code between the main app and App Clip
// Use a shared framework or add files to both targets
// In Build Settings, set:
//   _APP_CLIP = 1  (for the App Clip target)

// Conditional compilation for target-specific code
#if APPCLIP
let isAppClip = true
#else
let isAppClip = false
#endif

Invocation URLs and Advanced Matching

Configure invocation URLs in App Store Connect. Each URL maps to a specific App Clip experience.

import SwiftUI

// URL routing for the App Clip
@Observable
class AppClipRouter {
    static let shared = AppClipRouter()

    var currentExperience: AppClipExperience = .default

    enum AppClipExperience {
        case `default`
        case store(storeID: String)
        case product(productID: String)
        case orderPickup(orderID: String)
    }

    func route(to url: URL) {
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
            currentExperience = .default
            return
        }

        let pathComponents = components.path.split(separator: "/").map(String.init)

        // Match URL patterns:
        // https://example.com/store/123        → Store experience
        // https://example.com/product/abc      → Product experience
        // https://example.com/order/pickup/789 → Order pickup
        switch (pathComponents.first, pathComponents.dropFirst().first) {
        case ("store", let storeID?):
            currentExperience = .store(storeID: storeID)
        case ("product", let productID?):
            currentExperience = .product(productID: productID)
        case ("order", _):
            if let orderID = pathComponents.last, pathComponents.contains("pickup") {
                currentExperience = .orderPickup(orderID: orderID)
            }
        default:
            currentExperience = .default
        }
    }
}

// Root view that renders based on the invocation URL
struct AppClipRootView: View {
    var router = AppClipRouter.shared

    var body: some View {
        Group {
            switch router.currentExperience {
            case .default:
                DefaultExperienceView()
            case .store(let storeID):
                StoreExperienceView(storeID: storeID)
            case .product(let productID):
                ProductExperienceView(productID: productID)
            case .orderPickup(let orderID):
                OrderPickupView(orderID: orderID)
            }
        }
    }
}

NFC Tag and QR Code Triggers

App Clips can be triggered by NFC tags and QR codes that encode your registered invocation URL.

// NFC Tag Configuration:
// 1. Write an NDEF record to the NFC tag containing your invocation URL
// 2. The URL must match a registered App Clip experience in App Store Connect
// 3. Use Apple App Clip Codes (designed NFC + visual codes) for best UX

// QR Code Generation (for server or marketing team):
// The QR code simply encodes the invocation URL
// Example: https://appclip.example.com/store/downtown

// Reading NFC tag data within the App Clip (if needed)
import CoreNFC

class NFCReader: NSObject, NFCNDEFReaderSessionDelegate {
    var session: NFCNDEFReaderSession?

    func startScanning() {
        guard NFCNDEFReaderSession.readingAvailable else {
            print("NFC not available on this device")
            return
        }

        session = NFCNDEFReaderSession(
            delegate: self,
            queue: nil,
            invalidateAfterFirstRead: true
        )
        session?.alertMessage = "Hold your iPhone near the tag."
        session?.begin()
    }

    func readerSession(_ session: NFCNDEFReaderSession, didDetectNDEFs messages: [NFCNDEFMessage]) {
        for message in messages {
            for record in message.records {
                if let url = record.wellKnownTypeURIPayload() {
                    print("NFC URL: \(url)")
                    Task { @MainActor in
                        AppClipRouter.shared.route(to: url)
                    }
                }
            }
        }
    }

    func readerSession(_ session: NFCNDEFReaderSession, didInvalidateWithError error: Error) {
        print("NFC session invalidated: \(error.localizedDescription)")
    }
}

App Clip Card Configuration

The App Clip Card is the system UI that appears before the App Clip launches. Configure it in App Store Connect.

// App Clip Card metadata is set in App Store Connect, not in code:
// - Header image: 3000 x 2000 px recommended
// - Title: Your app name or experience title
// - Subtitle: Brief description (up to 56 characters)
// - Call-to-action button: "Open" (default) or custom text

// In your app, provide metadata for the card via the associated website
// Add this to your webpage's <head>:
//
// <meta name="apple-itunes-app"
//       content="app-id=123456789,
//                app-clip-bundle-id=com.example.myapp.Clip,
//                app-clip-display=card">

// Smart App Banner for Safari (also triggers App Clip Card)
// <meta name="apple-itunes-app" content="app-id=123456789">

// Programmatically check if running as App Clip
import StoreKit

struct AppClipBanner: View {
    @State private var showingFullAppOverlay = false

    var body: some View {
        VStack {
            Text("You're using the App Clip")
                .font(.headline)
            Text("Download the full app for all features.")
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
    }
}

Size Limitations (15 MB)

App Clips must be under 15 MB (uncompressed, thinned for a specific device). Strategies to stay within the limit.

// Strategies to minimize App Clip size:

// 1. Share only necessary code — don't include unused frameworks
// In Build Phases, only include files the App Clip needs

// 2. Use on-demand resources for images and assets
// In the asset catalog, assign assets to "App Clip" tag
// Load them at runtime:
import Foundation

func loadOnDemandImage(tag: String) async throws -> Data {
    let request = NSBundleResourceRequest(tags: [tag])
    try await request.beginAccessingResources()
    // Access the resource
    guard let url = Bundle.main.url(forResource: "hero", withExtension: "jpg") else {
        throw AppClipError.resourceNotFound
    }
    let data = try Data(contentsOf: url)
    request.endAccessingResources()
    return data
}

enum AppClipError: Error {
    case resourceNotFound
}

// 3. Use SF Symbols instead of custom images where possible
// 4. Use system fonts instead of bundled custom fonts
// 5. Remove unused localizations
// 6. Use Asset Catalog slicing for images
// 7. Check size: Product > Archive > Distribute App > App Thinning report

// 8. Verify the thinned size:
// $ xcrun app-clip-size --app-clip-path path/to/MyAppClip.app

App Group Data Handoff to Full App

Share data from the App Clip to the full app using App Groups so users don't lose progress.

import Foundation

// Both the App Clip and main app must have the same App Group entitlement:
// group.com.example.myapp.shared

struct SharedDataManager {
    static let suiteName = "group.com.example.myapp.shared"

    // Save data from App Clip for the full app to read
    static func saveFromAppClip(userPreferences: UserPreferences) {
        guard let defaults = UserDefaults(suiteName: suiteName) else { return }

        if let data = try? JSONEncoder().encode(userPreferences) {
            defaults.set(data, forKey: "userPreferences")
        }
        defaults.set(true, forKey: "hasAppClipData")
        defaults.set(Date(), forKey: "appClipLastUsed")
    }

    // Read App Clip data from the full app
    static func loadAppClipData() -> UserPreferences? {
        guard let defaults = UserDefaults(suiteName: suiteName) else { return nil }
        guard defaults.bool(forKey: "hasAppClipData") else { return nil }

        guard let data = defaults.data(forKey: "userPreferences") else { return nil }
        return try? JSONDecoder().decode(UserPreferences.self, from: data)
    }

    // Share files via the shared container
    static var sharedContainerURL: URL? {
        FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName)
    }

    static func saveOrderData(_ order: Order) throws {
        guard let containerURL = sharedContainerURL else {
            throw AppClipError.resourceNotFound
        }
        let fileURL = containerURL.appendingPathComponent("pending_order.json")
        let data = try JSONEncoder().encode(order)
        try data.write(to: fileURL)
    }

    static func loadPendingOrder() throws -> Order? {
        guard let containerURL = sharedContainerURL else { return nil }
        let fileURL = containerURL.appendingPathComponent("pending_order.json")
        guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
        let data = try Data(contentsOf: fileURL)
        return try JSONDecoder().decode(Order.self, from: data)
    }
}

struct UserPreferences: Codable {
    var favoriteStoreID: String?
    var preferredPaymentMethod: String?
    var hasCompletedOnboarding: Bool
}

struct Order: Codable {
    let id: String
    let items: [OrderItem]
    let total: Decimal
    let storeID: String
}

struct OrderItem: Codable {
    let name: String
    let quantity: Int
    let price: Decimal
}

Location Confirmation with CLAppClipCodeLocation

Verify that the user is physically present at the expected location to prevent relay attacks.

import AppClip
import CoreLocation

class LocationVerifier {
    func verifyLocation(for activity: NSUserActivity) async -> Bool {
        // Check if the invocation included location data
        guard let payload = activity.appClipActivationPayload else {
            print("No App Clip activation payload")
            return false
        }

        // Define the expected region (set in App Store Connect)
        let expectedCenter = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
        let expectedRegion = CLCircularRegion(
            center: expectedCenter,
            radius: 100,  // meters
            identifier: "store-downtown"
        )

        do {
            try await payload.confirmAcquired(in: expectedRegion)
            print("Location confirmed — user is at the expected location")
            return true
        } catch let error as APActivationPayloadError {
            switch error.code {
            case .disallowed:
                print("User denied location access")
            case .doesNotMatch:
                print("User is not at the expected location")
            @unknown default:
                print("Unknown location error: \(error)")
            }
            return false
        } catch {
            print("Location verification failed: \(error)")
            return false
        }
    }
}

// Use in the App Clip's onContinueUserActivity handler
struct LocationAwareAppClip: View {
    @State private var isVerified = false
    @State private var isVerifying = true
    let verifier = LocationVerifier()

    var body: some View {
        Group {
            if isVerifying {
                ProgressView("Verifying location...")
            } else if isVerified {
                StoreExperienceView(storeID: "downtown")
            } else {
                ContentUnavailableView(
                    "Location Required",
                    systemImage: "location.slash",
                    description: Text("Please visit the store to use this App Clip.")
                )
            }
        }
        .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
            Task {
                isVerified = await verifier.verifyLocation(for: activity)
                isVerifying = false
            }
        }
    }
}

SKOverlay for Full App Promotion

Show an App Store overlay that encourages users to download the full app.

import StoreKit
import SwiftUI

// SwiftUI approach using appStoreOverlay
struct AppClipWithOverlay: View {
    @State private var showOverlay = false

    var body: some View {
        VStack(spacing: 20) {
            Text("Thanks for your order!")
                .font(.title.bold())

            Text("Download the full app to earn rewards, track orders, and more.")
                .font(.body)
                .foregroundStyle(.secondary)
                .multilineTextAlignment(.center)
                .padding(.horizontal)

            Button("Get the Full App") {
                showOverlay = true
            }
            .buttonStyle(.borderedProminent)
        }
        .appStoreOverlay(isPresented: $showOverlay) {
            SKOverlay.AppClipConfiguration(position: .bottom)
        }
        .onAppear {
            // Show overlay automatically after a delay
            Task {
                try? await Task.sleep(for: .seconds(3))
                showOverlay = true
            }
        }
    }
}

// UIKit approach for more control
import UIKit

class AppClipOverlayViewController: UIViewController, SKOverlayDelegate {
    private var overlay: SKOverlay?

    func presentFullAppOverlay() {
        let config = SKOverlay.AppClipConfiguration(position: .bottom)
        overlay = SKOverlay(configuration: config)
        overlay?.delegate = self

        guard let windowScene = view.window?.windowScene else { return }
        overlay?.present(in: windowScene)
    }

    func dismissOverlay() {
        guard let windowScene = view.window?.windowScene else { return }
        SKOverlay.dismiss(in: windowScene)
    }

    // SKOverlayDelegate
    func storeOverlayDidFinishDismissal(_ overlay: SKOverlay, transitionContext: SKOverlay.TransitionContext) {
        print("Overlay dismissed")
    }

    func storeOverlayDidFinishPresentation(_ overlay: SKOverlay, transitionContext: SKOverlay.TransitionContext) {
        print("Overlay presented")
    }

    func storeOverlay(_ overlay: SKOverlay, didFailToLoadWithError error: Error) {
        print("Overlay failed to load: \(error)")
    }
}

Complete App Clip Example

A full cof

Discussion

Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.

Posts are public.Sign in to post

No one has posted yet. Be the first.