From apple-kit-skills
Build iOS App Clips: target setup, invocation via URLs/NFC/QR/Safari/Maps/Messages, NSUserActivity handling, App Group data handoff, SKOverlay promotion, size limits.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsThis skill uses the workspace's default tool permissions.
Lightweight, instantly-available versions of your iOS app for in-the-moment experiences or demos. Targets iOS 26+ / Swift 6.3 unless noted.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Lightweight, instantly-available versions of your iOS app for in-the-moment experiences or demos. Targets iOS 26+ / Swift 6.3 unless noted.
An App Clip is a separate target in the same Xcode project as your full app:
com.apple.developer.on-demand-install-capable entitlement and a Parent Application Identifiers entitlement linking back to the full app.com.example.MyApp.Clip.Use Swift packages or shared source files. Add files to both targets, or use the APPCLIP active compilation condition:
// 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.
Create a shared asset catalog included in both targets to avoid duplicating images and colors.
App Clips receive an NSUserActivity of type NSUserActivityTypeBrowsingWeb on launch. Handle it with onContinueUserActivity:
@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.
Configure experiences in App Store Connect after uploading a build containing the App Clip.
https://appclip.apple.com/id?=<bundle_id>&key=valueFor custom URLs (not the default Apple-generated link), add entries to the Associated Domains entitlement and host an AASA file:
appclips:example.com
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.
| 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(_:) |
Add this meta tag to your website to show the App Clip banner:
<meta name="apple-itunes-app"
content="app-id=YOUR_APP_ID, app-clip-bundle-id=com.example.MyApp.Clip,
app-clip-display=card">
When a user installs the full app, it replaces the App Clip. Use a shared App Group container to migrate data:
// 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")
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.
Store the ASAuthorizationAppleIDCredential.user in the shared container so the full app can silently verify without re-prompting login.
Display an overlay recommending the full app from within the App Clip:
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)
}
}
}
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.
Use APActivationPayload to verify a user's physical location without requesting full location access:
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:
<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+.
Info.plist; set NSAppClipRequestEphemeralUserNotification to truerequestWhenInUseAuthorization() only; resets daily at 4:00 AMSwiftUI, 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+)
App Intents, Background Tasks, CallKit, Contacts, CoreMotion, EventKit, HealthKit, HomeKit, MediaPlayer, Messages, NearbyInteraction, PhotoKit, SensorKit, Speech, SKAdNetwork, App Tracking Transparency
UIDevice.name and identifierForVendor return empty strings// ❌ 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 Identifiers entitlement is set correctlyAPPCLIP)onContinueUserActivity(NSUserActivityTypeBrowsingWeb) handles invocation URLsappclips:yourdomain.com (if using custom URLs)/.well-known/apple-app-site-association (if using custom URLs)SKOverlay / appStoreOverlay shown after task completion, never blockingNSAppClipRequestLocationConfirmation set in Info.plist if using location verification_XCAppClipURL env var