---
name: app-clips
description: "Build lightweight App Clip experiences for instant iOS app access without full installation. Covers target setup, invocation URL handling, experience configuration, size limits, invocation methods (NFC, QR, App Clip Codes, Safari banners, Maps, Messages), NSUserActivity handling, data migration to the full app via shared App Group containers, SKOverlay for full-app promotion, location confirmation with APActivationPayload, lifecycle considerations, and capability limitations. Use when creating App Clips or configuring App Clip invocation and data migration."
---

# App Clips

Lightweight, instantly-available versions of your iOS app for in-the-moment experiences or demos. Targets iOS 26+ / Swift 6.2 unless noted.

## Contents

- [App Clip Target Setup](#app-clip-target-setup)
- [Invocation URL Handling](#invocation-url-handling)
- [App Clip Experience Configuration](#app-clip-experience-configuration)
- [Size Limits](#size-limits)
- [Invocation Methods](#invocation-methods)
- [Data Migration to Full App](#data-migration-to-full-app)
- [SKOverlay for Full App Promotion](#skoverlay-for-full-app-promotion)
- [Location Confirmation](#location-confirmation)
- [Lifecycle and Ephemeral Nature](#lifecycle-and-ephemeral-nature)
- [Capabilities and Limitations](#capabilities-and-limitations)
- [Common Mistakes](#common-mistakes)
- [Review Checklist](#review-checklist)
- [References](#references)

## App Clip Target Setup

An App Clip is a **separate target** in the same Xcode project as your full app:

1. **File → New → Target → App Clip** — Xcode creates the target with the `com.apple.developer.on-demand-install-capable` entitlement and a `Parent Application Identifiers` entitlement linking back to the full app.
2. The App Clip bundle ID **must** be a suffix of the full app's: `com.example.MyApp.Clip`.
3. Xcode adds an **Embed App Clip** build phase to the full app target automatically.

### Share code between targets

Use Swift packages or shared source files. Add files to **both** targets, or use the `APPCLIP` active compilation condition:

```swift
// In App Clip target Build Settings → Active Compilation Conditions: APPCLIP

#if !APPCLIP
// Full-app-only code (e.g., background tasks, App Intents)
#else
// App Clip specific code
#endif
```

Prefer local Swift packages for shared modules — add the package as a dependency of both targets.

### Shared asset catalogs

Create a shared asset catalog included in both targets to avoid duplicating images and colors.

## Invocation URL Handling

App Clips receive an `NSUserActivity` of type `NSUserActivityTypeBrowsingWeb` on launch. Handle it with `onContinueUserActivity`:

```swift
@main
struct DonutShopClip: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onContinueUserActivity(
                    NSUserActivityTypeBrowsingWeb
                ) { activity in
                    handleInvocation(activity)
                }
        }
    }

    private func handleInvocation(_ activity: NSUserActivity) {
        guard let url = activity.webpageURL,
              let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
        else { return }

        // Extract path/query to determine context
        let locationID = components.queryItems?
            .first(where: { $0.name == "location" })?.value

        // Update UI for this location
    }
}
```

For UIKit scene-based apps, implement `scene(_:willConnectTo:options:)` for cold launch and `scene(_:continue:)` for warm launch.

**Key rule:** The full app **must** handle all invocation URLs identically — when a user installs the full app, it replaces the App Clip and receives all future invocations.

## App Clip Experience Configuration

Configure experiences in **App Store Connect** after uploading a build containing the App Clip.

### Default App Clip experience (required)

- Provide: header image, subtitle (≤56 chars), call-to-action verb
- App Store Connect generates a **default App Clip link**: `https://appclip.apple.com/id?=<bundle_id>&key=value`
- Supports: QR codes, NFC tags, Messages, Spotlight, other apps

### Demo App Clip link

- Auto-generated by App Store Connect for demo versions of your app
- Supports **all invocations** including physical (App Clip Codes, NFC, QR)
- Allows larger binary size (up to 100 MB uncompressed)
- Cannot contain URL parameters

### Advanced App Clip experiences (optional)

- Required for: Maps integration, location association, App Clip Codes, per-location card imagery
- Each experience has its own invocation URL, header image, and metadata
- URL **prefix matching** lets one registered URL cover many sub-paths
- Use the App Store Connect API to manage large numbers of experiences programmatically

### Associated domains

For custom URLs (not the default Apple-generated link), add entries to the Associated Domains entitlement and host an AASA file:

```text
appclips:example.com
```

## Size Limits

App Clip binaries must stay within strict uncompressed size limits (measured via App Thinning Size Report):

| iOS Version | Maximum Uncompressed Size |
|---|---|
| iOS 15 and earlier | 10 MB |
| iOS 16 | 15 MB |
| iOS 17+ (digital invocations only) | 100 MB |
| iOS 17+ (via demo link, all invocations) | 100 MB |

The 100 MB limit on iOS 17+ for non-demo links requires: digital-only invocations, no physical invocation support (no App Clip Codes / NFC / QR), and the App Clip must not support iOS 16 or earlier.

**Measure size:** Archive the app → Distribute → Export as Ad Hoc/Development with App Thinning → check `App Thinning Size Report.txt`.

Use Background Assets to download additional content post-launch (e.g., game levels) if needed. App Clip downloads cannot use `isEssential`.

## Invocation Methods

| Method | Requirements |
|---|---|
| **App Clip Codes** | Advanced experience or demo link; NFC-integrated or scan-only |
| **NFC tags** | Encode invocation URL in NDEF payload |
| **QR codes** | Encode invocation URL; works with default or advanced experience |
| **Safari Smart Banners** | Associate App Clip with website; add `<meta>` tag |
| **Maps** | Advanced experience with place association |
| **Messages** | Share invocation URL as text; limited preview with demo links |
| **Siri Suggestions** | Location-based; requires advanced experience for location suggestions |
| **Other apps** | iOS 17+; use Link Presentation or `UIApplication.open(_:)` |

### Safari Smart App Banner

Add this meta tag to your website to show the App Clip banner:

```html
<meta name="apple-itunes-app"
      content="app-id=YOUR_APP_ID, app-clip-bundle-id=com.example.MyApp.Clip,
               app-clip-display=card">
```

## Data Migration to Full App

When a user installs the full app, it replaces the App Clip. Use a **shared App Group container** to migrate data:

```swift
// In both targets: add App Groups capability with the same group ID

// App Clip — write data
func saveOrderHistory(_ orders: [Order]) throws {
    guard let containerURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.myapp.shared"
    ) else { return }

    let data = try JSONEncoder().encode(orders)
    let fileURL = containerURL.appendingPathComponent("orders.json")
    try data.write(to: fileURL)
}

// Full app — read migrated data
func loadMigratedOrders() throws -> [Order] {
    guard let containerURL = FileManager.default.containerURL(
        forSecurityApplicationGroupIdentifier: "group.com.example.myapp.shared"
    ) else { return [] }

    let fileURL = containerURL.appendingPathComponent("orders.json")
    guard FileManager.default.fileExists(atPath: fileURL.path) else { return [] }
    let data = try Data(contentsOf: fileURL)
    return try JSONDecoder().decode([Order].self, from: data)
}
```

### Shared UserDefaults

```swift
// Write (App Clip)
let shared = UserDefaults(suiteName: "group.com.example.myapp.shared")
shared?.set(userToken, forKey: "authToken")

// Read (Full app)
let shared = UserDefaults(suiteName: "group.com.example.myapp.shared")
let token = shared?.string(forKey: "authToken")
```

### Keychain sharing

Starting iOS 15.4, App Clip keychain items are accessible to the corresponding full app via the `parent-application-identifiers` and `associated-appclip-app-identifiers` entitlements. Use distinct `kSecAttrLabel` values to distinguish App Clip vs. full app entries.

### Sign in with Apple

Store the `ASAuthorizationAppleIDCredential.user` in the shared container so the full app can silently verify without re-prompting login.

## SKOverlay for Full App Promotion

Display an overlay recommending the full app from within the App Clip:

### SwiftUI

```swift
struct OrderCompleteView: View {
    @State private var showOverlay = false

    var body: some View {
        VStack {
            Text("Order placed!")
            Button("Get the full app") { showOverlay = true }
        }
        .appStoreOverlay(isPresented: $showOverlay) {
            SKOverlay.AppClipConfiguration(position: .bottom)
        }
    }
}
```

### UIKit

```swift
func displayOverlay() {
    guard let scene = view.window?.windowScene else { return }

    let config = SKOverlay.AppClipConfiguration(position: .bottom)
    let overlay = SKOverlay(configuration: config)
    overlay.delegate = self
    overlay.present(in: scene)
}
```

`SKOverlay.AppClipConfiguration` automatically resolves to the parent app. Available iOS 14.0+.

**Never** block the user's task to force installation — show the overlay after task completion.

## Location Confirmation

Use `APActivationPayload` to verify a user's physical location without requesting full location access:

```swift
import AppClip
import CoreLocation

func verifyLocation(from activity: NSUserActivity) {
    guard let payload = activity.appClipActivationPayload,
          let url = activity.webpageURL
    else { return }

    // Build the expected region (up to 500m radius)
    let center = CLLocationCoordinate2D(latitude: 37.334722, longitude: -122.008889)
    let region = CLCircularRegion(center: center, radius: 100, identifier: "store-42")

    payload.confirmAcquired(in: region) { inRegion, error in
        if let error = error as? APActivationPayloadError {
            switch error.code {
            case .doesNotMatch:
                // URL doesn't match registered App Clip URL
                break
            case .disallowed:
                // User denied location, or invocation wasn't NFC/visual code
                break
            @unknown default:
                break
            }
            return
        }

        if inRegion {
            // Confirmed — user is at the expected location
        } else {
            // User is not at expected location (e.g., NFC tag was moved)
        }
    }
}
```

Enable location confirmation in `Info.plist`:

```xml
<key>NSAppClip</key>
<dict>
    <key>NSAppClipRequestLocationConfirmation</key>
    <true/>
</dict>
```

This is **lightweight** — the system verifies location without granting your App Clip continuous access. The App Clip card shows a note that the clip can verify location. Available iOS 14.0+.

## Lifecycle and Ephemeral Nature

- **No Home Screen icon** — App Clips appear in the App Library and recent apps
- **Automatic removal** — the system deletes the App Clip and its data after a period of inactivity (typically ~30 days, system-determined)
- **No persistent state guarantee** — treat App Clip storage as ephemeral; migrate important data to the shared container or a server
- **Relaunching** — returning to a previously launched App Clip from the App Library uses the last invocation URL; returning from the App Switcher launches without an invocation URL (restore saved state)
- **Notifications** — App Clips can request ephemeral notification permission (up to 8 hours) via `Info.plist`; set `NSAppClipRequestEphemeralUserNotification` to `true`
- **Location access** — `requestWhenInUseAuthorization()` only; resets daily at 4:00 AM

## Capabilities and Limitations

### Available to App Clips

SwiftUI, UIKit, Core Location (when-in-use), Sign in with Apple, Apple Pay, CloudKit (public database read-only, iOS 16+), Background Assets, StoreKit (`SKOverlay`), Keychain, App Groups, Push Notifications (ephemeral), Live Activities (iOS 16.1+)

### Not available / no-op at runtime

App Intents, Background Tasks, CallKit, Contacts, CoreMotion, EventKit, HealthKit, HomeKit, MediaPlayer, Messages, NearbyInteraction, PhotoKit, SensorKit, Speech, SKAdNetwork, App Tracking Transparency

### Additional restrictions

- No background URL sessions
- No background Bluetooth
- No multiple scenes on iPad
- No on-demand resources
- No custom URL schemes
- No in-app purchases (reserve for full app)
- `UIDevice.name` and `identifierForVendor` return empty strings

## Common Mistakes

### Exceeding App Clip size limit

```swift
// ❌ DON'T: Include large frameworks or bundled assets
// Importing heavyweight frameworks like RealityKit or large ML models
// pushes the App Clip well over 10–15 MB.

// ✅ DO: Use Asset Catalog thinning, exclude unused architectures,
// strip debug symbols, and split shared code into lean Swift packages.
// Measure with App Thinning Size Report after every change.
```

### Not testing invocation URLs locally

```swift
// ❌ DON'T: Only test App Clip with a direct Xcode launch
// This skips invocation URL handling and misses bugs.

// ✅ DO: Use the _XCAppClipURL environment variable in the scheme,
// or register a Local Experience in Settings → Developer → Local Experiences
// to test with realistic invocation URLs and App Clip cards.
```

### Not handling the full app replacing the App Clip

```swift
// ❌ DON'T: Assume only the App Clip receives invocations
// When the user installs the full app, ALL invocations go to it.

// ✅ DO: Share invocation-handling code between both targets.
// The full app must handle every invocation URL the App Clip supports.
#if !APPCLIP
// Full app can additionally show richer features for the same URL
#endif
```

### Storing critical data only in App Clip storage

```swift
// ❌ DON'T: Store important data in the App Clip's sandboxed container
let fileURL = documentsDirectory.appendingPathComponent("userData.json")
// This data is DELETED when the system removes the App Clip.

// ✅ DO: Write to the shared App Group container or sync to a server
guard let shared = FileManager.default.containerURL(
    forSecurityApplicationGroupIdentifier: "group.com.example.shared"
) else { return }
let fileURL = shared.appendingPathComponent("userData.json")
```

### Missing associated domains configuration

```swift
// ❌ DON'T: Configure App Clip experiences in App Store Connect
// without setting up associated domains and the AASA file.
// Invocations from your website and advanced experiences will fail.

// ✅ DO: Add the Associated Domains entitlement with:
//   appclips:example.com
// AND host /.well-known/apple-app-site-association on your server:
// {
//   "appclips": {
//     "apps": ["TEAMID.com.example.MyApp.Clip"]
//   }
// }
```

## Review Checklist

- [ ] App Clip target bundle ID is a suffix of the full app's bundle ID
- [ ] `Parent Application Identifiers` entitlement is set correctly
- [ ] Shared code uses Swift packages or compilation conditions (`APPCLIP`)
- [ ] `onContinueUserActivity(NSUserActivityTypeBrowsingWeb)` handles invocation URLs
- [ ] Full app handles all invocation URLs the App Clip supports
- [ ] App Thinning Size Report confirms binary is within size limits for target iOS
- [ ] Associated Domains entitlement includes `appclips:yourdomain.com` (if using custom URLs)
- [ ] AASA file hosted at `/.well-known/apple-app-site-association` (if using custom URLs)
- [ ] Default App Clip experience configured in App Store Connect
- [ ] Shared App Group container used for data the full app needs
- [ ] `SKOverlay` / `appStoreOverlay` shown **after** task completion, never blocking
- [ ] `NSAppClipRequestLocationConfirmation` set in Info.plist if using location verification
- [ ] No reliance on background processing, restricted frameworks, or persistent local storage
- [ ] Tested with Local Experiences (Settings → Developer) and `_XCAppClipURL` env var
- [ ] Handles launch without invocation URL (App Switcher / notification re-entry)

## References

- [App Clips framework](https://sosumi.ai/documentation/appclip/)
- [Creating an App Clip with Xcode](https://sosumi.ai/documentation/appclip/creating-an-app-clip-with-xcode/)
- [Configuring App Clip experiences](https://sosumi.ai/documentation/appclip/configuring-the-launch-experience-of-your-app-clip/)
- [Responding to invocations](https://sosumi.ai/documentation/appclip/responding-to-invocations/)
- [Choosing the right functionality](https://sosumi.ai/documentation/appclip/choosing-the-right-functionality-for-your-app-clip/)
- [Confirming a person's physical location](https://sosumi.ai/documentation/appclip/confirming-a-person-s-physical-location/)
- [Sharing data between App Clip and full app](https://sosumi.ai/documentation/appclip/sharing-data-between-your-app-clip-and-your-full-app/)
- [Recommending your app to App Clip users](https://sosumi.ai/documentation/appclip/recommending-your-app-to-app-clip-users/)
- [APActivationPayload](https://sosumi.ai/documentation/appclip/apactivationpayload/)
- [SKOverlay.AppClipConfiguration](https://sosumi.ai/documentation/storekit/skoverlay/appclipconfiguration/)
- [NSUserActivityTypeBrowsingWeb](https://sosumi.ai/documentation/foundation/nsuseractivitytypebrowsingweb/)
- [Creating App Clip Codes](https://sosumi.ai/documentation/appclip/creating-app-clip-codes/)
- [Distributing your App Clip](https://sosumi.ai/documentation/appclip/distributing-your-app-clip/)
- [App Clips HIG](https://sosumi.ai/design/human-interface-guidelines/app-clips/)
