App Clip 轻 App 怎么做?在 Windows 上开发 App Clip 并真机测试
最后更新
App Clip(轻 App)是完整 App 的一小部分功能,用户扫码、碰 NFC 标签或点链接就能马上用,不用先去 App Store 下载。这篇给 SwiftUI 工程 MyApp 加一个「扫码点单」App Clip:用 kxbuild project add-target --type app-clip 创建目标,处理调用 URL,配置关联域名和网站文件,再用 iPhone 的「本地体验」在真机上测试,全程在 Windows 上完成。
文中的工程已经能用 KXBuild 编译并装到真机上。还没走到这一步的,先看第一个 SwiftUI App。Apple 的规则以官方文档为准,截至 2026 年 10 月。
App Clip 轻 App 适合做什么?
App Clip 和完整 App 放在同一个工程、同一个 ipa 里提交,审核和上架也一起走。用户通过下面这些方式触发它,系统先弹出一张 App Clip 卡片,点按钮后几秒内打开。
- 扫描 App Clip 码、二维码,或者碰一下 NFC 标签
- 在 Safari 网页上点智能横幅或 App Clip 卡片
- 点「信息」里别人发来的链接
- 「地图」里的地点链接、Siri 建议、Spotlight 搜索
几条和普通 App 不同的规则,开发前先确认需求合适。
| 规则 | 内容 |
|---|---|
| 必须有完整 App | App Clip 不能单独上架,完整 App 至少要有 App Clip 的全部功能 |
| 只能有一个 | 一个 App 只能有一个 App Clip target,多个场景用不同的调用 URL 区分 |
| 不在主屏幕 | 用户不安装 App Clip,它不出现在主屏幕,一段时间不用会被系统删除 |
| 装了完整 App 之后 | 完整 App 会替换 App Clip,之后同样的链接、二维码都打开完整 App |
| 不能后台运行 | 不能用后台网络、后台模式,不能在不使用时保持蓝牙连接 |
| 扩展 | 不能带 App 扩展,只能带一个用于实时活动的小组件扩展 |
| 隐私 | 不能请求追踪授权,不能持续定位,只能和自己的完整 App 共享数据 |
另外有一批框架在 App Clip 里编译能过,运行时却没有功能,比如 HealthKit、通讯录、日历、相册、CoreMotion、CallKit、后台任务。用到这些的功能留给完整 App。
大小限制
App Clip 要秒开,所以有大小上限。限制的是解压后的 App Clip 二进制,按支持的最低系统不同。
| 系统版本 | 解压后的大小上限 |
|---|---|
| iOS 15 及更早 | 10 MB |
| iOS 16 | 15 MB |
| iOS 17 及以上 | 100 MB,有附加条件 |
100 MB 的条件是同时满足下面几条:只支持数字方式调用(网站、Spotlight 等),不支持 App Clip 码、二维码、NFC 这类实体方式;用户大多在网络可靠的环境里使用;不支持 iOS 16 及更早。用 App Store Connect 生成的演示链接(demo link)时,也可以用 100 MB 上限并支持实体方式调用,适合做游戏试玩版。
KXBuild 添加的 App Clip 默认最低支持 iOS 16,按 15 MB 控制。出包后把 ipa 改名为 zip 解压,Payload\MyApp.app\AppClips\MyAppClip.app 文件夹的大小是未经瘦身的大小,只要它低于上限就稳妥。图片、音频这类大资源尽量放到服务器上按需下载。
怎么给工程添加 App Clip 目标?
以下两种做法任选其一。
- VS Code 插件。在 KXBuild 视图中右键 MyApp 这个 target,选
KXBuild: 添加 Target…,类型选 App Clip,名称用默认的 MyAppClip。要和 App 共享数据时填 App Group,暂时不共享就留空。 - 命令行。在工程所在目录执行下面的命令。
1kxbuild project add-target -P .\MyApp.xcodeproj --type app-clip --name MyAppClip
App Clip 的 Bundle ID 是 App 的 Bundle ID 加 .Clip,这里是 com.example.myapp.Clip。这和 Apple 的规则一致:App Clip 的标识符必须以完整 App 的标识符开头,后面再接一段。全部类型和参数见 kxbuild 命令参考和添加 Target。
命令输出会提示两件事:为 com.example.myapp.Clip 注册 App ID,以及在 com.example.myapp.Clip、com.example.myapp 两个 App ID 上开启 App Clips 能力。下文「准备 App ID 和描述文件」一节逐步操作。
App Clip 目标生成了哪些文件?
工程目录下多出一个 MyAppClip 文件夹,已经可以直接编译。
| 文件 | 作用 |
|---|---|
| MyAppClipApp.swift | App Clip 的入口,已经写好接收调用 URL 的代码 |
| ContentView.swift | App Clip 的界面 |
| Info.plist | NSAppClip 字典,两个开关默认关闭 |
| MyAppClip.entitlements | Parent Application Identifiers,指向完整 App |
| Assets.xcassets | App Clip 自己的图标和强调色 |
App 这边多了 MyApp/MyApp.entitlements,工程里原来有 entitlements 文件的会直接加进去。两个 entitlements 的内容如下,一个说「我的父 App 是谁」,一个说「我的 App Clip 是谁」,两边对上,系统才认这对关系。
1<!-- MyAppClip/MyAppClip.entitlements -->
2<key>com.apple.developer.parent-application-identifiers</key>
3<array>
4 <string>$(AppIdentifierPrefix)com.example.myapp</string>
5</array>
6
7<!-- MyApp/MyApp.entitlements -->
8<key>com.apple.developer.associated-appclip-app-identifiers</key>
9<array>
10 <string>$(AppIdentifierPrefix)com.example.myapp.Clip</string>
11</array>
Parent Application Identifiers 只能有一项,就是完整 App 的标识符,只加在 App Clip 上。$(AppIdentifierPrefix) 在签名时换成你的 Team ID 加点,不用手动改。以后改了 App 的 Bundle ID,这两处要一起改。
App Clip 已经设置为随 App 一起构建,并嵌入 App 的 AppClips 文件夹,不用再改工程设置。
Info.plist 里 NSAppClip 的两个开关按需打开。
| 键 | 打开后 |
|---|---|
| NSAppClipRequestEphemeralUserNotification | 每次打开后 8 小时内可以发通知,不用单独请求权限,卡片上会注明,用户可以关掉 |
| NSAppClipRequestLocationConfirmation | 可以用系统提供的方式确认用户是否在指定地点附近,适合到店场景 |
App Clip 怎么处理调用 URL?
App Clip 每次被调用,系统都会把调用 URL 放在一个 NSUserActivity 里交给它,类型是 NSUserActivityTypeBrowsingWeb。App Clip 要根据 URL 直接进入对应的内容,比如扫哪张桌子的二维码,就打开哪家门店的菜单,不要让用户再选一次。
把 MyAppClip/MyAppClipApp.swift 换成下面的内容。它支持两种 URL:自己网站的 https://clip.example.com/store/42,和 Apple 生成的默认链接 https://appclip.apple.com/id?=com.example.myapp.Clip&store=42。
1import SwiftUI
2
3@main
4struct MyAppClipApp: App {
5 // 从通知或多任务界面回来时没有调用 URL,用上次保存的门店
6 @AppStorage("lastStoreID") private var storeID = ""
7
8 var body: some Scene {
9 WindowGroup {
10 ContentView(storeID: storeID)
11 .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
12 if let id = ClipLink.storeID(from: activity.webpageURL) {
13 storeID = id
14 }
15 }
16 }
17 }
18}
19
20enum ClipLink {
21 /// 支持两种调用 URL:
22 /// https://clip.example.com/store/42
23 /// https://appclip.apple.com/id?=com.example.myapp.Clip&store=42
24 static func storeID(from url: URL?) -> String? {
25 guard let url,
26 let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil }
27 if let value = components.queryItems?.first(where: { $0.name == "store" })?.value, !value.isEmpty {
28 return value
29 }
30 let parts = url.pathComponents
31 if let index = parts.firstIndex(of: "store"), index + 1 < parts.count {
32 return parts[index + 1]
33 }
34 return nil
35 }
36}
再把 MyAppClip/ContentView.swift 换成下面的内容。下单后在底部弹出推荐安装完整 App 的浮层。
1import SwiftUI
2import StoreKit
3
4struct ContentView: View {
5 let storeID: String
6 @State private var ordered = false
7 @State private var showFullApp = false
8
9 var body: some View {
10 VStack(spacing: 16) {
11 Image(systemName: "cup.and.saucer.fill")
12 .font(.system(size: 56))
13 .foregroundStyle(.tint)
14 if storeID.isEmpty {
15 Text("扫描桌上的二维码开始点单")
16 .font(.title3)
17 } else {
18 Text("门店 \(storeID)")
19 .font(.title.bold())
20 Button(ordered ? "已下单" : "来一杯拿铁") {
21 ordered = true
22 showFullApp = true
23 }
24 .buttonStyle(.borderedProminent)
25 .disabled(ordered)
26 }
27 }
28 .padding()
29 // 完成任务后在底部推荐完整 App
30 .appStoreOverlay(isPresented: $showFullApp) {
31 SKOverlay.AppClipConfiguration(position: .bottom)
32 }
33 }
34}
几种调用情况都要考虑到。
| 用户怎么打开 | 收到的 URL |
|---|---|
| 扫码、NFC、点链接、网站横幅 | 二维码或链接里的 URL,或网站页面的 URL |
| 地图、Siri 建议 | 你在 App Store Connect 里为体验登记的 URL |
| 从 App 资源库或 Spotlight 回到用过的 App Clip | 上一次的调用 URL |
| 从通知或多任务界面回来 | 没有 URL,要自己恢复上次的状态 |
所以上面的代码用 @AppStorage 记住了上次的门店。推荐完整 App 的浮层不要挡住主流程,Apple 要求不能强制用户安装完整 App 才能完成任务。
完整 App 也要处理同样的 URL
用户装了完整 App 之后,同样的二维码会打开完整 App。把 MyApp/MyAppApp.swift 换成下面的内容,文件末尾放一份和 App Clip 相同的 ClipLink。
1import SwiftUI
2
3@main
4struct MyAppApp: App {
5 @State private var storeID: String?
6
7 var body: some Scene {
8 WindowGroup {
9 ContentView()
10 // 装了完整 App 之后,同样的链接会打开完整 App,这里要处理同样的 URL
11 .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in
12 storeID = ClipLink.storeID(from: activity.webpageURL)
13 print("打开门店 \(storeID ?? "-")")
14 }
15 }
16 }
17}
18
19enum ClipLink {
20 static func storeID(from url: URL?) -> String? {
21 guard let url,
22 let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil }
23 if let value = components.queryItems?.first(where: { $0.name == "store" })?.value, !value.isEmpty {
24 return value
25 }
26 let parts = url.pathComponents
27 if let index = parts.firstIndex(of: "store"), index + 1 < parts.count {
28 return parts[index + 1]
29 }
30 return nil
31 }
32}
App Clip 里的数据要带到完整 App,给两个 target 配同一个 App Group,用 UserDefaults(suiteName:) 或共享容器读写,做法和 171 篇小组件共享数据一样。登录状态可以放进钥匙串,或者用「通过 Apple 登录」,让用户换到完整 App 后不用重新登录。
App ID 和描述文件怎么准备?
App Clip 是单独签名的程序,有自己的 App ID 和描述文件。登录 developer.apple.com,进入 Certificates, Identifiers & Profiles,需要账号持有人或管理员权限。
- 注册 App Clip 的 App ID。在 Identifiers 页点加号,选 App IDs,类型选 App Clip。在下拉框里选父 App 的 App ID
com.example.myapp,Product Name 填Clip,它会接在父 App ID 后面,得到com.example.myapp.Clip。 - 开启能力。按添加 target 时的提示,在 App Clip 和完整 App 两个 App ID 上开启 App Clips 相关能力。下一节要用关联域名的,同时勾选 Associated Domains。
- 生成描述文件。在 Profiles 页给
com.example.myapp.Clip新建 Development 描述文件,勾选证书和测试机。完整 App 的 App ID 改过能力,把它原来的描述文件 Edit 后直接 Save,重新下载。 - 放进 keychain 目录。两份描述文件都放进 KXBuild 的
keychain目录,删掉旧文件。构建时按 Bundle ID 给 App 和 App Clip 各配一份。
扩展、App Clip 这类子目标的签名规则见 iOS App 扩展的签名配置,描述文件和证书目录的完整步骤见证书与签名。
关联域名与网站文件
用自己的网址调用 App Clip,要让 App Clip 和网站互相确认,原理和 Universal Links 一样。不是所有情况都必须做,先按下表判断。
| 场景 | 要不要关联网站 |
|---|---|
| 开发阶段用本地体验测试二维码、NFC | 不用 |
只用 Apple 生成的默认链接 appclip.apple.com |
不用 |
| 网站上显示智能横幅和 App Clip 卡片 | 要 |
| 高级体验(App Clip 码、地图地点、多个门店不同卡片) | 要 |
| 要支持 iOS 16.3 及更早系统上的全部调用方式 | 要 |
| 在 TestFlight 里给测试员配 App Clip 体验 | 要 |
第一步:两个 target 都加 appclips 域名
在 MyAppClip/MyAppClip.entitlements 和 MyApp/MyApp.entitlements 里都加上下面这个键,保留原有的内容。双击 .entitlements 文件用属性列表编辑器编辑也可以。
1<key>com.apple.developer.associated-domains</key>
2<array>
3 <string>appclips:clip.example.com</string>
4</array>
格式是 appclips: 加完整域名,只写子域名和顶级域名,不带 https://、路径、查询参数、结尾斜杠和通配符 *。完整 App 已经有 Universal Links 的,在同一个数组里和 applinks: 并列即可。两个 target 的 entitlements 文件已经在构建设置里指定好,不用再设置 CODE_SIGN_ENTITLEMENTS。
两个 App ID 都要勾选 Associated Domains 并重新生成描述文件,否则签名时会报描述文件不含这项能力。
第二步:网站上放 apple-app-site-association
在 https://clip.example.com/.well-known/apple-app-site-association 放一个 JSON 文件,没有扩展名,加上 appclips 段。apps 里是「Team ID.App Clip 的 Bundle ID」。
1{
2 "appclips": {
3 "apps": ["ABCDE12345.com.example.myapp.Clip"]
4 },
5 "applinks": {
6 "details": [
7 {
8 "appIDs": ["ABCDE12345.com.example.myapp"],
9 "components": [{ "/": "/store/*" }]
10 }
11 ]
12 }
13}
applinks 段是给完整 App 的 Universal Links 用的,装了完整 App 之后同样的链接才能打开它。文件要用 HTTPS 直接返回,不能重定向,服务器要允许 AASA-Bot 和 CFNetwork 这两个 User-Agent 访问。每个要调用 App Clip 的域名都要放一份自己的文件。
文件格式、托管要求和 Apple CDN 缓存的细节,见 Universal Links 与 URL Scheme 配置。上传构建并创建体验后,App Store Connect 的构建详情页 App Clip 一栏可以点 View Status 查看每个域名的验证状态。
在 iPhone 上用本地体验测试
本地体验(Local Experiences)是 iPhone 开发者设置里的功能,能在不上架、不配网站的情况下,用真实的二维码或 NFC 调起 App Clip,看到完整的卡片和启动流程。
第一步:把 App Clip 装到手机上
- 先删掉手机上的完整 MyApp。装着完整 App 时,调用会打开完整 App 而不是 App Clip。
- 用数据线连上 iPhone,确认已经打开开发者模式,见设备准备。
- 在状态栏选好 iPhone,按 F5。工程里有 MyApp 和 MyAppClip 两个应用 target,会弹出选择,选 MyAppClip(
com.example.myapp.Clip)。
想固定每次 F5 都运行 App Clip,在 .vscode/launch.json 里用 bundle 字段指定它,字段说明见运行与调试。
1{
2 "version": "0.2.0",
3 "configurations": [
4 { "type": "kxbuild", "request": "launch", "name": "Run App Clip", "build": true, "bundle": "com.example.myapp.Clip" }
5 ]
6}
App Clip 启动后,F5 的断点、变量和 KXBuild Console 输出都和普通 App 一样可用。直接启动时没有调用 URL,界面显示「扫描桌上的二维码开始点单」,这是正常的。
第二步:注册本地体验
在 iPhone 上打开「设置 → 开发者」,在 App Clips Testing 分组点 Local Experiences,再点 Register Local Experience,按下表填写。
| 字段 | 填什么 |
|---|---|
| URL Prefix | 调用 URL 或它的前缀,如 https://clip.example.com/store |
| Bundle ID | App Clip 的 Bundle ID com.example.myapp.Clip |
| Title | 卡片标题,30 个字符以内,如「街角咖啡」 |
| Subtitle | 卡片副标题,56 个字符以内,如「扫码点单,不用排队」 |
| Action | 卡片按钮的文字,如 Open |
| 图片 | 卡片顶部的头图 |
URL 前缀会匹配所有以它开头的地址,https://clip.example.com/store 能覆盖 /store/42、/store/43。本地体验的优先级高于 App Store Connect 里配置的体验。
「设置」里看不到「开发者」时,先确认开发者模式已经打开,再用数据线连一次电脑并按 F5 装一次 App。
第三步:扫码调起 App Clip
- 用任意二维码工具把
https://clip.example.com/store/42生成二维码,显示在电脑屏幕上,或者写进一个 NFC 标签。 - 用 iPhone 的相机或控制中心的扫码器扫它。在「设置 → 控制中心」里可以把扫码器和 NFC 标签读取器加进控制中心。
- 屏幕下方弹出 App Clip 卡片,显示刚才填的标题、副标题和头图。点按钮打开 App Clip,界面显示「门店 42」。
换成 https://clip.example.com/store/43 再扫一次,确认能直接进入另一家门店。从多任务界面切回 App Clip,确认还停在上次的门店。
测试时注意几点。
- 本地体验只能调起 Development、Ad Hoc 或 TestFlight 签名的 App Clip,不能调起 App Store 上的版本。
- 用本地体验测不了「装了完整 App 时打开完整 App」这种情况。要验证它,装上完整 App,用 Universal Links 打开同样的 URL,方法见 192 篇的「在 iPhone 上验证跳转」。
- 测完 App Store Connect 里的正式体验前,先删掉本地体验,否则它会优先生效。
- 在 App Store Connect 里新建了高级体验却一直调不起来,到「设置 → 开发者」里点 Clear Experience Cache 清掉缓存。
上线前要在 App Store Connect 里配置什么?
App Clip 和完整 App 一起打包上传。在命令面板或工程的右键菜单运行 KXBuild: 生成 App Store ipa,或者执行下面的命令。
1kxbuild build .\MyApp.xcodeproj -c Release --export app-store
上架前给 com.example.myapp.Clip 也生成一份 App Store 类型的描述文件放进 keychain。App Clip 的版本号和构建号和 App 保持一致,在工程设置编辑器里两个 target 一起改。生成的 build\MyApp.ipa 里已经带着 App Clip,上传流程见在 Windows 上上架 App Store。
上传处理完成后,在 App Store Connect 里配置体验。
| 体验 | 在哪里配置 | 内容 |
|---|---|---|
| 默认体验(必需) | App 版本页的 App Clip 部分 | 卡片头图、副标题、按钮文字。系统生成默认链接 https://appclip.apple.com/id?=<bundle id>&key=value,不用改服务器 |
| 演示链接 | 配置默认体验时自动生成 | 用于试玩版,能用 100 MB 上限,不能带 URL 参数 |
| 高级体验(可选) | App 版本页的 Edit Advanced Experiences | 每个门店或地点一张卡片,可以关联地图地点、生成 App Clip 码,要关联网站 |
| TestFlight 测试体验 | 构建详情页的 App Clip 部分 | 最多三个,测试员从 TestFlight 打开,要关联网站 |
默认链接和演示链接要等 App 和 App Clip 审核通过、在 App Store 上架后才能使用,开发阶段用本地体验测试。上架以后,到「设置 → 开发者」的 App Clips Testing 分组里点 Diagnostics,输入网址,系统会检查高级体验、网站关联和智能横幅是否都配置正确。
TestFlight 的上传和测试员管理见 TestFlight 内测分发教程。
App Clip 打不开或报错怎么排查?
| 现象 | 原因 | 处理 |
|---|---|---|
构建时报找不到 com.example.myapp.Clip 的描述文件 |
没注册 App Clip 的 App ID,或描述文件没放进 keychain |
按上文注册 App ID、生成描述文件 |
| 后台提示 App ID 不包含父 App ID | App Clip 的 App ID 没有选对父 App,或 entitlements 里的父标识符和 App 的 Bundle ID 不一致 | 重新注册 App Clip 类型的 App ID,核对 parent-application-identifiers |
| 签名报描述文件不含 Associated Domains | App ID 没勾选 Associated Domains,或描述文件是改能力之前下载的 | 两个 App ID 都勾选,重新生成并替换描述文件 |
| 扫码后打开的是完整 App | 手机上装着完整 App | 测 App Clip 前删掉完整 App |
| 扫码没有弹出卡片 | 没注册本地体验,或 URL 前缀、Bundle ID 填错 | 在「设置 → 开发者 → Local Experiences」里核对 |
| 卡片弹出但点开是空白或默认界面 | 代码没处理这个 URL | 在 onContinueUserActivity 里打印 URL,对照解析逻辑 |
| App Store Connect 里域名验证失败 | AASA 文件路径、格式不对,或服务器拦截了 Apple 的请求 | 检查 .well-known 路径、appclips 段和 User-Agent 放行 |
| 上传时提示 App Clip 太大 | 超过对应系统版本的大小上限 | 去掉大资源改为按需下载,或按条件使用 100 MB 上限 |
App Clip 签名和安装的其他报错见 iOS 签名报错与 ipa 安装失败,16 条常见错误及解决办法。
常见问题
App Clip 的大小限制是多少?
按解压后的二进制计算。iOS 15 及更早是 10 MB,iOS 16 是 15 MB,iOS 17 及以上可以到 100 MB,但要满足只用数字方式调用、不支持 iOS 16 等条件,或者使用 App Store Connect 生成的演示链接。
App Clip 的 Bundle ID 怎么起?
必须以完整 App 的 Bundle ID 开头,后面再接一段,通常是 .Clip,例如 com.example.myapp.Clip。在开发者后台注册时选 App Clip 类型,选好父 App ID,再填后缀。
App Clip 一定要配置关联域名吗?
不一定。开发时用本地体验测试二维码和 NFC 不需要,只用 Apple 生成的默认链接也不需要。要在网站上显示 App Clip 卡片、配置高级体验、支持 iOS 16.3 及更早系统或在 TestFlight 里测试体验,才要加 appclips: 域名并放网站文件。
iPhone 上的本地体验在哪里设置?
打开「设置 → 开发者」,在 App Clips Testing 分组点 Local Experiences,再点 Register Local Experience,填 URL 前缀、App Clip 的 Bundle ID、标题、副标题和按钮文字。看不到「开发者」时,先打开开发者模式并连电脑装一次 App。
装了完整 App 之后还能打开 App Clip 吗?
不能。完整 App 会替换 App Clip,之后同样的二维码和链接都打开完整 App,所以完整 App 也要处理全部调用 URL。测试 App Clip 时先删掉手机上的完整 App。