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 cofDiscussion
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.

