---
name: app-clips
description: "iOS App Clips: target setup, invocation from NFC, App Clip Codes, QR, Messages, Maps and Safari banners, App Store Connect experiences, size limits and capability constraints, NSUserActivity routing, SKOverlay promotion, App Group or keychain handoff, ephemeral notifications, location confirmation, migration to the full app. Use when building a clip, wiring invocation, configuring experiences or handing off to the full app."
metadata:
  source: multi-agent-pipeline
---

# App Clips

A clip is a tiny, instantly launched piece of an app. Someone scans a code,
taps a tag or follows a link, finishes one job, and leaves without installing
anything. This skill is about building one, sending people to the right
screen, staying under its limits, and passing them on to the full app.
Baseline: iOS 26 and Swift 6.3; lower floors are noted where they apply.

Deeper material:

- `references/routing-and-experiences.md`: URL handling in SwiftUI and UIKit,
  the three kinds of App Store Connect experience, every launch source, Smart
  App Banners, and testing locally.
- `references/data-handoff-notifications-location.md`: shared containers,
  keychain direction, Sign in with Apple, the 8-hour notification window, and
  proving a physical visit.
- `references/size-capabilities-and-promotion.md`: an outline for feasibility
  answers, the size tiers, Background Assets, `SKOverlay`, and what the
  platform switches off.

## Setting up the target

Add the clip through File > New > Target > App Clip, inside the project that
already holds the full app. Xcode creates the target, gives the full app an
"Embed App Clip" build phase, and fills in the entitlements that link the two.

The clip's bundle identifier must extend the parent's: `org.example.Tickets`
pairs with `org.example.Tickets.Clip`.

When archiving, signing or App Store Connect validation fails, look at the raw
keys rather than Xcode's display names:

| Holder | Raw entitlement |
|---|---|
| Clip | `com.apple.developer.on-demand-install-capable` |
| Clip | `com.apple.developer.parent-application-identifiers` |
| Full app | `com.apple.developer.associated-appclip-app-identifiers` |

Share logic through Swift packages or source files that both targets compile,
and fence clip-only code with `#if APPCLIP`. Frameworks that only the full app
uses stay out of the clip's link phase.

## Routing an invocation

Launches arrive as an `NSUserActivity` whose type is
`NSUserActivityTypeBrowsingWeb`; its `webpageURL` identifies what to show.
Keep that parsing in shared code. After installation the full app takes the
clip's place, and the very same links then open the full app.

| UI stack | Where the URL shows up |
|---|---|
| SwiftUI | `.onContinueUserActivity(NSUserActivityTypeBrowsingWeb)` |
| UIKit, first launch | `connectionOptions.userActivities`, read in `scene(_:willConnectTo:options:)` |
| UIKit, already running | the activity passed to `scene(_:continue:)` |

The callback `scene(_:willContinueUserActivityWithType:)` is merely a heads-up that an
activity is on its way; it has no URL, so nothing should route from it.

```swift
import SwiftUI

@main
struct TicketsClip: App {
    @State private var router = VisitRouter()

    var body: some Scene {
        WindowGroup {
            VisitScreen(router: router)
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { launch in
                    if let link = launch.webpageURL {
                        router.open(link)
                    }
                }
        }
    }
}
```

`VisitRouter` and `VisitScreen` are shared types; see the routing reference.

App Store Connect side:

- Every clip needs a default experience.
- Advanced experiences exist for Maps placement, tying the clip to a place,
  App Clip Codes used in production, a card per location, and precise routing
  at a venue.
- Demo App Clip Codes are generated from the short variant of the demo link.
- For your own domain, list `appclips:example.com` under Associated Domains in
  the clip and in the full app, and publish an `apple-app-site-association`
  file there that names the clip.
- A Safari banner's meta tag names the app through `app-id`, the clip through
  `app-clip-bundle-id`, and asks for the card with `app-clip-display=card`.
  `app-argument` is defined for full-app banners, so clip routing cannot
  depend on it.

## Size and capability decisions

Size:

- Read the number from the App Thinning Size Report; never estimate.
- In a feasibility answer, say which limit applies and why, based on the
  oldest supported iOS version, the launch sources, and whether the setting
  offers dependable internet.
- 100 MB is available only on iOS 17 and later, for clips launched digitally
  where a good connection is expected. Physical launch sources or iOS 16
  support shrink the budget, and iOS 15 support shrinks it further.
- Through the demo link, a clip may still use 100 MB and accept NFC and QR
  tests; demo codes must be generated from the short demo link.
- These figures have moved before. Re-read the current App Clip and App Store
  Connect pages before a size ruling holds up a release.

Downloads:

- Background Assets are for material that can land after launch while the
  task proceeds, and the clip may not flag any of it as essential.
- Big bundled media, ML models, product catalogs, and anything required before
  the task can begin are a bad match. State plainly that a mandatory download
  in front of the task is not acceptable, and check both how large downloads
  are and whether one blocks the task. Serve that content from a server or
  stream, fetch it after the task, or leave it to the full app.

Promotion: show `SKOverlay.AppClipConfiguration` (UIKit) or `appStoreOverlay`
(SwiftUI) once the task has succeeded. It must never stand between the person
and the task.

Platform boundaries:

- CloudKit: from iOS 16 the clip may read the public database using the
  `CloudKit-Anonymous` service. Writing to it, document storage in iCloud,
  the iCloud key-value store, and private or shared containers are all
  unavailable.
- Live Activities: supported from iOS 16 (ActivityKit's own APIs start at
  16.1) through a widget extension embedded only in the clip. That extension
  holds Live Activities and nothing else, and it must carry the App Clip
  Extension capability; in a review, name its raw key,
  `com.apple.developer.on-demand-install-capable`.
- In any feasibility answer, list as unavailable: App Intents,
  `SKAdNetwork`, custom URL schemes, `App Tracking Transparency`, Background
  Tasks, in-app purchase, background URL sessions, state that must persist
  locally, and anything that assumes background or long-lived execution.

Depth for product reviews:

- Send these elsewhere without implementation detail: Live Activity code
  (ActivityKit, WidgetKit), purchasing and monetization (StoreKit), background
  work (BackgroundTasks), CloudKit schema or sync past public reads, long-lived
  credentials and long-term state (the full app).
- A PM feasibility answer covers capability fit, which size tier and why,
  invocation limits, and who owns each handoff. Confirming location, promoting
  the install, App Group or keychain handoff and Live Activities are explained
  as concepts.
- Such an answer leaves out package structure, type names like
  `APActivationPayload`, `CLCircularRegion` and
  `SKOverlay.AppClipConfiguration`, App Group or keychain code, and other
  API-level material, unless someone asks for the implementation.

## Data, notifications, location

- **Shared container.** App Groups carry only data that may be read by anyone
  holding the group entitlement, because every such target can read and write
  both the container and the shared defaults suite. It is a convenience, not a
  security boundary: no passwords, refresh tokens, payment data or other
  secrets.
- **Keychain.** Items the clip wrote become readable by the full app on iOS
  15.4 and later, as long as the linking entitlements are in place (on the
  full app's side that means `com.apple.developer.associated-appclip-app-identifiers`). The reverse
  never works: the clip cannot see the full app's items.
- **Sign in with Apple.** Keep the credential's `user` identifier
  (`ASAuthorizationAppleIDCredential.user`) in the shared container; the full
  app asks `ASAuthorizationAppleIDProvider.getCredentialState(forUserID:)` and
  proceeds only on `.authorized`.
- **Ephemeral notifications.** Put `NSAppClipRequestEphemeralUserNotification`
  inside the `NSAppClip` dictionary of the clip's Info.plist. The permission
  covers a window of up to 8 hours following every launch, and the person may
  disable it from the card, so read the settings and require `.ephemeral`
  before scheduling anything.
- **Relaunch from a notification.** The clip comes back with no invocation
  URL. To steer to the correct experience, give remote pushes a
  `target-content-id` and local ones a
  `UNNotificationContent.targetContentIdentifier` whose value is a URL
  matching an advanced experience, never a made-up identifier.
- **Location confirmation.** Read `NSUserActivity.appClipActivationPayload`,
  build a `CLCircularRegion`, and pass it to
  `APActivationPayload.confirmAcquired(in:)`; the region's radius is 500 m or less. The flag
  `NSAppClipRequestLocationConfirmation` belongs in the clip target's
  `NSAppClip` dictionary, never the full app's. The check verifies that an
  eligible physical launch happened at the place; it does not grant ongoing
  location access.

## Common mistakes

1. **Exceeding the budget.** A clip launched from physical codes that still
   runs on iOS 16 has a fraction of the room of an iOS 17+, link-only clip.
   Measure again after every substantial change.
2. **Confusing entitlements.** Xcode's labels hide the raw keys. Two belong on
   the clip (install-capable, parent identifiers) and one on the full app
   (associated clip identifiers); see the table above.
3. **Routing from the advance notice.**
   `scene(_:willContinueUserActivityWithType:)` has no activity to read.
4. **Never testing a real invocation.** Running the scheme straight from
   Xcode may bypass the launch path and mask routing faults. Use the
   `_XCAppClipURL` scheme variable or a Local Experience (Settings > Developer
   > Local Experiences).
5. **Ignoring the replacement.** Once installed, the full app answers the
   clip's links; give it the same router.
6. **Secrets in the group container.** Credentials go to the keychain or are
   verified on the server.
7. **A clip that only advertises.** People should complete a focused task, or
   try a complete demo, without installing. Skip ads, splash screens,
   downloads that hold up launch, repeated install nags, and flows that are
   really a website in a web view.
8. **No associated domains.** Custom URLs and advanced experiences fail
   without an `appclips:` entry in each target and a published association
   file (path `/.well-known/apple-app-site-association`) naming the clip.

## Review checklist

- [ ] Clip bundle ID extends the parent app's bundle ID
- [ ] Both clip entitlements and the full app's association entitlement present (table above)
- [ ] Shared logic lives in packages or behind `APPCLIP`
- [ ] SwiftUI uses `.onContinueUserActivity(NSUserActivityTypeBrowsingWeb)`
- [ ] UIKit covers cold launch via `connectionOptions.userActivities` and warm launch via `scene(_:continue:)`
- [ ] Every clip URL also opens correctly in the full app
- [ ] Size confirmed in the thinning report for the chosen OS floor and launch sources; the answer names all three tiers (10, 15 and 100 MB)
- [ ] `appclips:` domain entry in both targets when custom URLs are used
- [ ] Association file served from `/.well-known/apple-app-site-association`
- [ ] Default experience set up in App Store Connect
- [ ] Advanced experiences behind production codes, Maps and per-place cards; demo codes built from the short demo link
- [ ] Any Live Activity lives in a clip-only widget extension containing only Live Activities, with the install-capable key
- [ ] Group container holds nothing secret
- [ ] Keychain direction spelled out (full app reads clip items from iOS 15.4 with the entitlements; never the other way)
- [ ] Full app checks the Apple ID credential state before trusting a clip sign-in
- [ ] Install overlay shown only after success, never as a gate
- [ ] Location-confirmation flag only in the clip's Info.plist
- [ ] Notification code requires `.ephemeral` before scheduling
- [ ] Notification relaunches without a URL are routed correctly
- [ ] Design review turns down marketing-only, ad-filled, web-view, install-first and launch-blocking flows
- [ ] Unavailable runtime features listed, including local persistence and background or long-running assumptions
- [ ] Nothing relies on background execution, restricted frameworks, custom schemes, IAP or durable local storage
- [ ] Feasibility answers keep sibling work at the boundary (no code for ActivityKit, StoreKit, BackgroundTasks, CloudKit, packages, App Groups, keychain, location checks or overlays)
- [ ] Verified with a Local Experience and with `_XCAppClipURL`

## Apple documentation

All under https://developer.apple.com/documentation/appclip unless noted:

- `creating-an-app-clip-with-xcode` (target setup)
- `configuring-the-launch-experience-of-your-app-clip` (experiences)
- `responding-to-invocations` (URL handling)
- `choosing-the-right-functionality-for-your-app-clip` (limits and frameworks)
- `confirming-a-person-s-physical-location` (activation payload)
- `sharing-data-between-your-app-clip-and-your-full-app` (containers, keychain)
- `enabling-notifications-in-app-clips` (8-hour window)
- `supporting-invocations-from-your-website-and-the-messages-app` (banners)
- `offering-live-activities-with-your-app-clip` (widget extension)
- `recommending-your-app-to-app-clip-users` (overlay)
- `apactivationpayload` (API)
- `creating-app-clip-codes` (codes)
- `distributing-your-app-clip` (release)
- https://developer.apple.com/documentation/storekit/skoverlay/appclipconfiguration
- https://developer.apple.com/documentation/foundation/nsuseractivitytypebrowsingweb
- HIG: https://developer.apple.com/design/human-interface-guidelines/app-clips
