# TencentQianWebView — iOS Swift SDK Framework

腾讯电子签 H5 容器 SDK iOS Swift 实现的 Framework 源码目录，对应 Xcode 工程中的 `TencentQianWebView` framework target。

## 模块标识

| 项 | 值 |
|---|---|
| 模块名 | `TencentQianWebView` |
| 语言 | Swift 5.9 |
| Deployment Target | iOS 14.3 |
| 内核 | WKWebView |
| 分发方式 | XcodeGen 工程（`ios-swift/project.yml`） + Swift Package Manager（仓库根 `Package.swift`） |

## 目录结构

```
TencentQianWebView/
├── Sources/
│   ├── TencentQianWebView.swift              # 容器模式公共入口（命名空间 enum）
│   ├── TencentQianWebViewController.swift    # 容器内置 ViewController
│   ├── TencentQianWebViewConfigurator.swift  # 配置模式公共入口
│   ├── TencentQianWebViewComponent.swift     # 组件模式（嵌入自定义布局，含嵌套 Controller）
│   ├── TencentQianWebViewPrewarmer.swift    # 预热公共入口（configure/prewarm/releaseAll）
│   ├── KeyboardAvoidance.swift               # 底部安全区 + 软键盘避让（容器内建 / 宿主一行接入）
│   ├── TencentQianWebViewShared.swift        # 共享 WKProcessPool 等
│   ├── Configuration/                        # WKWebViewConfiguration 工厂
│   ├── Internal/                             # navigation / UI delegate / Prewarm（池/manifest/去重）
│   ├── Download/                             # 下载策略 + URLSession + 分享 sheet
│   ├── Scheme/                               # qianapp / 外部 scheme 拦截
│   ├── Model/                                # Config / SignResult / SignCallbacks / SdkEvent / Prewarm*
│   └── Resources/                            # 国际化字符串（Base / zh-Hans）
└── Info.plist
```

## 公共 API 速览

### 容器模式 `TencentQianWebView.open`

SDK 内置承载 VC（含顶栏、进度条、关闭确认、安全区适配、软键盘避让），一次调用即可拉起签署 H5：

```swift
import TencentQianWebView

TencentQianWebView.open(
    from: self,
    url: signUrl,
    config: TencentQianWebViewConfig(topBarTitle: "电子签署"),
    callbacks: SignCallbacks(onSignResult: { result in
        // result.action / result.result / result.flowId / result.extras
    })
)
```

### 组件模式 `TencentQianWebViewComponent`

将签署 WebView 嵌入宿主自定义布局，顶栏 / 进度条 / 错误页由宿主自绘。参数面与 Android 同名（除 `reusePrewarmPool`，iOS 无此参数）；命令对象为嵌套类型 `TencentQianWebViewComponent.Controller`（`TencentQianWebViewController` 已被容器模式占用）：

```swift
let controller = TencentQianWebViewComponent.Controller()

let component = TencentQianWebViewComponent(
    url: signUrl,
    config: TencentQianWebViewConfig(topBarTitle: "电子签署"),
    onSignResult: { result in /* 处理签署结果 */ },
    onTitleChanged: { title, canGoBack in /* 更新自绘顶栏标题 */ },
    onProgressChanged: { progress, visible in /* 显示/更新进度条 */ },
    onRenderUnrecoverable: { /* 显示错误页 + controller.reload() 重试 */ },
    controller: controller
)

myView.addSubview(component)  // + Auto Layout 约束

// 页面销毁时（viewDidDisappear 确认退出 / deinit）：component.release()
```

#### 宿主避让工具 `KeyboardAvoidance.apply`（组件 / 配置模式）

组件 / 配置模式由宿主持有布局，SDK 不主动避让；一行调用即可接入与容器模式同款的
**底部安全区 + 软键盘避让**（对齐 Android `applyEdgeToEdgeInsets` 的定位）：

```swift
// 组件模式：webView 传组件实例；配置模式：传自有 WKWebView
KeyboardAvoidance.apply(webView: component, in: view)
```

除底部约束外，`apply` 同时自动接入与容器模式同款的**键盘弹出窗口 contentOffset 冻结
+ 动画落定后 `nearest` 补滚**（抑制 WebKit 聚焦 reveal 过期几何的「掉-弹」抖动，用户
手势滚动不受影响；传入非 WKWebView 的普通视图时退化为仅装底部约束）。

调用后不要再为该视图添加其他底部约束；返回句柄生命周期与宿主视图绑定，需提前解除时调 `remove()`。
语义详见 [../../docs/troubleshooting.md Q15](../../docs/troubleshooting.md#q15-h5-底部-fixed-弹框中的输入框被软键盘遮挡三端键盘避让机制)。

> iOS `WKWebView` 无 `clearHistory` API，池实例永不复用到页面，故组件**无 `reusePrewarmPool` 参数**。崩溃恢复复用容器模式内核（原地 `load` 恢复，非 Android 销毁重建）。完整参数面与关键约束详见 [../../docs/usage-guide.md §1.5](../../docs/usage-guide.md#15-组件模式嵌入自定义布局)。

### 配置模式 `TencentQianWebViewConfigurator.apply`

把 SDK 能力叠加到客户已有 `WKWebView` 上，保留客户自有 UI 与导航：

```swift
final class MyWebViewController: UIViewController {
    private let webView = WKWebView()
    private var handle: TencentQianWebViewConfigurator.Handle?  // 必须强引用持有

    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(webView)

        handle = TencentQianWebViewConfigurator.apply(
            webView: webView,
            hostController: self,
            config: TencentQianWebViewConfig(),
            callbacks: SignCallbacks(onSignResult: { result in /* ... */ })
        )
        webView.load(URLRequest(url: signUrl))
    }
}
```

退出页面 `handle.detachAndDestroy()` 解绑并销毁；复用 WebView 则用 `handle.detach()` 仅解绑 SDK 注入。

### 预热（可选，提升首屏速度）

预热与容器/配置模式**正交**，不预热时行为与现状完全一致，预热是纯增量收益。`configure()` 可省（首次 `prewarm` 会以默认 `PROD` 懒初始化）；`MINIMAL` 仅 DNS、收益小，真正提速靠 `LIGHT` / `HEAVY`，按意图触发即可：

```swift
// 进入签署业务入口前
TencentQianWebViewPrewarmer.configure(PrewarmConfig(environment: .prod))
TencentQianWebViewPrewarmer.prewarm(.light, bizType: "single-sign")
TencentQianWebViewPrewarmer.prewarm(.heavy, bizType: "single-sign")

// 签署流程结束、不再需要时主动释放预热池实例（页面 WebView 关闭即销毁，无需手动回收）
TencentQianWebViewPrewarmer.releaseAll()
```

> iOS `WKWebView` 无 `clearHistory` API，池实例只加载 `warmup.html` 做网络/进程预热，**永不复用到页面**；预热收益靠共享 `WKProcessPool`（进程复用）+ `.default()` DataStore（磁盘缓存跨实例命中）承接。

完整档位语义、触发时机、池生命周期与端差异详见 [../../docs/usage-guide.md §3.5](../../docs/usage-guide.md#35-预热可选提升首屏速度)。

## 单元测试

```bash
bash scripts/test-ios-swift.sh
```

测试 target 见同级 `../TencentQianWebViewTests/`，使用 XCTest。

## SPM 集成

仓库根 [`Package.swift`](../../Package.swift) 暴露 `TencentQianWebView` library product，客户可直接通过 SPM 引入：

```swift
.package(url: "<your-git-url>", from: "1.0.0"),
.product(name: "TencentQianWebView", package: "h5-in-app-integration"),
```

## 关键设计点

- **共享 `WKProcessPool` 单例**：`TencentQianWebViewShared.processPool`，所有 SDK 创建的 `WKWebView` 共用，保证多 WebView 间 Cookie 互通
- **UA 同步注入**：`WKConfigurationFactory.makeConfiguration` 通过 `applicationNameForUserAgent` 同步注入，无时序竞争。注意该属性为**替换语义**（整体替换 UA 末尾系统默认后缀而非追加）。mobile 模式下 SDK 注入串与 Mobile Safari 后缀逐字节一致（`Version/<v> Mobile/15E148 Safari/604.1`），最终 UA 与 Safari 完全一致、仅末尾追加 `qianwv/<version>` 单一标识——WKWebView 与 Safari 同 WebKit 引擎，逐字节一致可让所有 UA sniffing 分支（特性检测、风控、第三方 SDK 浏览器白名单）行为与 Safari 对齐；iPad 内容模式由 `iPadContentMode` 控制（默认 mobile，`desktop` 恢复 Mac 伪装）
- **下载策略**：`WKNavigationDelegate.decidePolicyFor navigationResponse` 识别下载 → `URLSession` 下载到沙盒 → `UIActivityViewController` 让用户分享 / 保存

## 集成与排错

- 接入指南：[../../docs/usage-guide.md](../../docs/usage-guide.md)
- 接入自检清单：[../../docs/integration-checklist.md](../../docs/integration-checklist.md)
- 排错决策树 + FAQ：[../../docs/troubleshooting.md](../../docs/troubleshooting.md)
- `qianapp://` 协议规范：[../../docs/qianapp-protocol.md](../../docs/qianapp-protocol.md)
