Loading...
Loading...
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.
npx skill4agent add dpearson2699/swift-ios-skills app-clipscom.apple.developer.on-demand-install-capableParent Application Identifierscom.example.MyApp.ClipAPPCLIP// 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
#endifNSUserActivityNSUserActivityTypeBrowsingWebonContinueUserActivity@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
}
}scene(_:willConnectTo:options:)scene(_:continue:)https://appclip.apple.com/id?=<bundle_id>&key=valueappclips:example.com| 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 |
App Thinning Size Report.txtisEssential| 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 |
| 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 |
<meta name="apple-itunes-app"
content="app-id=YOUR_APP_ID, app-clip-bundle-id=com.example.MyApp.Clip,
app-clip-display=card">// 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)
}// 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")parent-application-identifiersassociated-appclip-app-identifierskSecAttrLabelASAuthorizationAppleIDCredential.userstruct 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)
}
}
}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.AppClipConfigurationAPActivationPayloadimport 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)
}
}
}Info.plist<key>NSAppClip</key>
<dict>
<key>NSAppClipRequestLocationConfirmation</key>
<true/>
</dict>Info.plistNSAppClipRequestEphemeralUserNotificationtruerequestWhenInUseAuthorization()SKOverlayUIDevice.nameidentifierForVendor// ❌ 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.// ❌ 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.// ❌ 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// ❌ 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")// ❌ 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"]
// }
// }Parent Application IdentifiersAPPCLIPonContinueUserActivity(NSUserActivityTypeBrowsingWeb)appclips:yourdomain.com/.well-known/apple-app-site-associationSKOverlayappStoreOverlayNSAppClipRequestLocationConfirmation_XCAppClipURL