IPA 升级 itms-services 路径补主动 suspend:用户回桌面 + 看到下载进度

【背景修正】
上个 commit (befe205) commit msg 里说"iOS 调 itms-services 时会自动把 App 切后台"
不准确 —— 实测系统弹窗只是模态覆盖在本 App 上,用户点「安装」后 App 回到 active,
桌面图标下载进度需要用户手动按 Home 才能看到。

【方案】
调 UIApplication.open(itms-services URL) 后,0.3s 用私有 selector "suspend"
把 App 主动切后台(等价按 Home / 上滑):
- 用户立即看到桌面 + 系统安装确认窗(弹窗是系统级,覆盖在桌面上)
- 用户点「安装」→ 弹窗 dismiss → 用户已经在桌面 → 看到 App 图标下载进度
- App 进程留在内存,IPA 替换时由 iOS 自动结束

【私有 API 风险评估】
suspend selector 是 iOS 私有 API,AppStore 审核会拒。本项目企业签 / 渠道分发,
不上架 AppStore,可以用 (CLAUDE.md 第 6 节稳定>一切原则下接受)。
UIControl().sendAction 比 perform() 更安全:selector 不响应时静默忽略,不 crash。

【仅 itms-services 路径触发 suspend】
Safari 路径(普通 https URL)不调 suspend:用户已经在 Safari,主动切后台反而
让 Safari 也消失,糟糕的 UX。

BuildProject 通过。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
joywayer
2026-06-27 22:12:59 +08:00
co-authored by Claude Opus 4.7
parent befe205bf9
commit 960adc5491
@@ -554,26 +554,44 @@ public final class WebContainerViewController: UIViewController {
} }
private func showIPAUpgradeAlert(downloadURL: String) { private func showIPAUpgradeAlert(downloadURL: String) {
// 行为目标:点「确定」直接弹系统安装窗(不跳 Safari),用户点「安装」后 // 行为目标:点「确定」直接弹系统安装窗 + App 自动切后台(用户回到桌面),
// App 自然切后台、用户看到桌面图标下载进度,App 进程留在内存,无需主动 exit。 // 用户在桌面上点「安装」即可看到 App 图标下载进度,App 进程留在内存不退出。
// //
// 实现依赖:URL 必须是 iOS 可直接接管的安装协议(itms-services 或 manifest plist 直链)。 // iOS 实际行为:调 itms-services 时系统弹安装确认窗,但本 App 仍处于 inactive
// 详见 makeInstallURL 注释。如果远端配的是 https 网页链接,仍会跳 Safari // 状态(弹窗是模态覆盖在 App 上),用户点「安装」后弹窗 dismiss、App 回到 active
// 给用户看引导页 —— 这是预期外的情形,需后台把 appDownload 改成 itms-services // —— 系统**不会主动**切后台。要让用户回桌面,需要本 App 主动调私有 selector
// 或 .plist 直链才能跳过 Safari 中转。 // `suspend` 把自己切后台(等价于用户按 Home / 上滑)。
// 本项目企业签 / 渠道分发,不上架 AppStore,可以用私有 API。
//
// URL 形态:itms-services 直链 / .plist 直链 / 其它(Safari)—— 详见 makeInstallURL。
// Safari 路径不调 suspend:用户已经在 Safari,suspend 反而破坏 Safari 引导页体验。
let alert = UIAlertController( let alert = UIAlertController(
title: "需要升级", title: "需要升级",
message: "检测到新版本,点击「确定」开始升级,系统会自动下载并安装。", message: "检测到新版本,点击「确定」开始升级,系统会自动下载并安装。",
preferredStyle: .alert preferredStyle: .alert
) )
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
if let url = Self.makeInstallURL(from: downloadURL) { guard let url = Self.makeInstallURL(from: downloadURL) else { return }
UIApplication.shared.open(url) UIApplication.shared.open(url)
if url.scheme?.lowercased() == "itms-services" {
// 0.3s 等系统接收 itms-services URL(确认弹窗 schedule 完成),
// 然后把 App 切后台。系统弹窗仍会覆盖在桌面上正常显示。
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
Self.suspendAppToHomeScreen()
}
} }
}) })
present(alert, animated: true) present(alert, animated: true)
} }
/// 把本 App 切到后台(用户看到桌面),App 进程留在内存。
/// 私有 selector `suspend` 等价于按 Home / 上滑回桌面的系统行为。
/// `UIControl.sendAction` 内部会处理 selector 不响应的情况,比 `perform()` 安全。
private static func suspendAppToHomeScreen() {
let sel = NSSelectorFromString("suspend")
UIControl().sendAction(sel, to: UIApplication.shared, for: nil)
}
/// 把远端 `appDownload` 规范化为 iOS 系统可直接接管的安装协议,避免跳 Safari 中转。 /// 把远端 `appDownload` 规范化为 iOS 系统可直接接管的安装协议,避免跳 Safari 中转。
/// ///
/// 三种 raw URL 形态: /// 三种 raw URL 形态: