Files
ios/Xuxucheck/docs/webview-safe-area.md
T
admin d6aeede2bc 导入 XuXuCheck 工程(iOS 干化学尿液试纸分析 App)
从桌面工程复制而来,作为本仓库的 Xuxucheck/ 子目录。

为控制仓库体积做了两处删减:
- 移除 Frameworks/opencv2.xcframework 下的两个 visionOS 切片
  (xros-arm64、xros-arm64-simulator,合计约 106 MB),并同步删掉该
  xcframework 内 Info.plist 里对应的两条 AvailableLibraries 记录,
  只保留 ios-arm64 与 ios-arm64_x86_64-simulator。
- 移除全部 .DS_Store 与 xcuserdata/。

已用 xcodebuild 从本目录完整编译验证:BUILD SUCCEEDED,
ProcessXCFramework 以 --platform ios 成功选中框架,产物含 3699 个 cv:: 符号
(OpenCV 确实链入),嵌入 framework 3 个,与移除无用 pod 之后的状态一致。

仓库约 206 MB,其中 OpenCV 203 MB、其余源码与依赖约 3 MB。
2026-09-23 14:17:31 +08:00

154 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# WebView 上下边距 / 通屏显示
涉及文件(这几处是**一个整体**,只改其中一处会出问题):
- `XuXuCheck/Components/XuXuWebView.swift` — `topSafeInset` 常量、`.padding(.top,)`、两层 `.ignoresSafeArea(...)`
- `XuXuCheck/Components/WebViewRepresentable.swift` — `contentInsetAdjustmentBehavior = .never`
所有网页(记录 / 成员 / 我的 / 产品介绍 / 检测结果,以及首页四个入口)都走同一个 `XuXuWebView`,
改这里一处,全局生效。
## 现象
三个阶段,顶部和底部各修了一半:
1. 改之前 —— 每个网页顶部露出一大条蓝色空白,内容被压得很低;
同时屏幕最下方 34pt 是一条纯白带。
2. 只通屏顶部之后 —— 标题被灵动岛压住,网页左上角的返回键落进系统区域,点不动。
3. 现在 —— 顶部留 20pt,底部铺到物理底边。
## 判断逻辑(可复用)
同一个「留白」现象,可能是两种**完全相反**的成因,修法也相反。所以第一步不是改代码,
而是**确定这块区域显示的是谁的颜色** —— 用 `docs/tools/px.py` 量出来,别靠眼睛:
| 量到的颜色 | 含义 | 该怎么做 |
| --- | --- | --- |
| 本页蓝色底 `#395CFF` | 容器铺到了,webview 没铺到,露出 App 自己的底色 | 让 webview 铺满这一边 |
| 父视图的白 / 浅灰 `#FFFFFF` | 连容器都没铺到,露出的是 `XuXuWebView` 外面 | 给容器的 `ignoresSafeArea` edges 补上这一边 |
| 网页自己的底色 `#E8F3FE` | 网页在该边自己留了空 | 是网页的事,客户端别动 |
| 系统材质(模糊 / 纯黑) | 系统安全区自己画的 | 这块本来就该留给系统,不要盖 |
本文件里的顶部和底部就是这两种情况的活标本:
- **顶部** —— 所有层都铺到了,但网页自带 ~49pt 让位,系统安全区又整份叠上去 = 双份,所以要**收**。
- **底部** —— 两层都停在安全区上,没人去画那 34pt,父视图白底漏出来,所以要**铺满**。
另一个容易搞错的是 `ignoresSafeArea` 和 `padding` 的顺序:想「铺满但内容内缩」,
必须 `padding` 在前、`ignoresSafeArea` 在后;反过来说,看到别人写
`.ignoresSafeArea(...).padding(...)` 时,那是给满屏视图加外衬距,底部会溢出容器。
## 顶部
原来蓝色底铺到了物理顶端,而 `VStack` / `WebViewRepresentable` 还遵守顶部安全区,
网页从安全区下方开始渲染 —— 顶部就空出约 62pt 的蓝色带。
这 62pt 本身该留,问题是**网页自己也留了一份**。从改前的截屏反推(iPhone 16 Pro,单位 pt):
| 量 | 值 | 来源 |
| --- | --- | --- |
| webview 起点 | ~62 | 设备顶部安全区 |
| 网页标题中心 | ~111 | 截屏量得 |
| → 网页自身头部下移量 | ~49 | 111 − 62 |
| 灵动岛黑胶囊纵向范围 | 20 ~ 47 | 截屏量得 |
也就是说网页头部是按「浏览器里已经让开状态栏」设计的,自带约 49pt 下移,真正多余的只有那 62pt。
但直接把 webview 铺到 y=0 也不行:标题中心会落到 ~49pt,正好嵌进灵动岛(20~47pt)下沿;
返回键同在这一行,也就贴进了屏幕左上角那块**系统手势保留区**(iOS 留给下拉通知中心/控制中心的地盘),
那里的点击会被系统吃掉 —— 这就是「返回点不动」的原因,不是按钮坏了。
现在留一小条白、整体下移 `topSafeInset`(当前 20pt):`20 + 49 ≈ 69pt`,
实测标题中心在 ~70pt,离开灵动岛下沿 23pt,也离开了手势区。
## 底部
底部那条白带的原因和顶部相反 —— 是**两层都没铺到底**:
```swift
Color(hex: 0x395cff).ignoresSafeArea(.keyboard, edges: .bottom)
.ignoresSafeArea(.container, edges: [.top, .horizontal]) // ← 原先少了 .bottom
VStack { ... }.ignoresSafeArea(.container, edges: .top) // ← 原先只有 .top
```
两层都停在底部安全区边界上,于是最下面 34pt 谁也不画,露出的是 `XuXuWebView` 背后**父视图的白底**。
实测(同一张截屏,屏高 874pt):
| 位置 | 颜色 |
| --- | --- |
| 网页正文底色 | `#E8F3FE` |
| y = 840 ~ 874pt | `#FFFFFF` 纯白,正好 34pt = 底部安全区 |
修法:两层的 edges 都补上 `.bottom`。
## 现在的做法
```swift
// XuXuWebView.swift
private let topSafeInset: CGFloat = 20
...
Color(hex: 0x395cff)
.ignoresSafeArea(.keyboard, edges: .bottom)
.ignoresSafeArea(.container, edges: [.top, .horizontal, .bottom])
VStack { WebViewRepresentable(currentUrl: currentUrl) ... }
.padding(.top, topSafeInset)
.ignoresSafeArea(.container, edges: [.top, .bottom])
```
**顺序不能反**:先 `.padding(.top,)` 把内容推下去,再 `.ignoresSafeArea(...)` 让容器重新铺到物理顶端,
被推出来的那 20pt 就露出 ZStack 里的蓝色底。反过来写(`.ignoresSafeArea` 在前、`.padding` 在后)
是给一个已经满屏的视图再加外衬距,底部会被挤出容器。
**`.never` 不能删**:默认的 `.automatic` 在 webView 铺到状态栏下面之后会再叠一份安全区 inset,
和 `.padding(.top,)` 双重叠加,就又回到「一大条留白」的原始状态。这三处是一套,注意成对维护。
## 调参
只改 `XuXuWebView.swift` 里的 `topSafeInset`:
| topSafeInset | 效果 |
| --- | --- |
| `8` ~ `12` | 顶部最窄,逼近灵动岛;小屏/无刘海机型可能不够 |
| **`20`(当前)** | 标题与返回键落在灵动岛下方,顶部留一条窄蓝边 |
| `30` ~ `38` | 更稳妥地躲开左上角手势区;顶部蓝边更明显 |
| `62`(原值附近) | 回到改动前的观感 |
判断依据:以**头部最挤的那个页面**为准。底部没有可调参数,它就是物理底边。
## 已知取舍
底部铺到物理底边之后,网页内容在滚到最末时可以压到 home indicator 底下(home indicator 是半透明浮层,
文字仍看得见,但会有一条横杠压在最后一行上)。若这点不能接受,可以给
`WebViewRepresentable` 的 `scrollView.contentInset.bottom` 设成安全区高度 —— 背景仍通到物理底,
只是内容提前 34pt 结束。**注意**:`contentInset` 区域显示的是 scrollView 自己的背景色
(通常会被 WebKit 同步成页面底色,但不保证),设之前先在真机上确认它在你家页面上不是白的,
否则会重新出现一条白带。
## 验证
```bash
xcodebuild -workspace XuXuCheck.xcworkspace -scheme XuXuCheck -configuration Debug \
-destination 'id=<device-id>' -allowProvisioningUpdates build
```
真机逐页看:标题是否完整露出、左上角返回能否点、顶部蓝边宽窄、**页面滚到底时底部是否还有白带**、
最后一行是否被 home indicator 压住。
## 附:怎么量截屏像素
判断「某某区域到底是什么颜色、多高」不要靠眼睛估计,直接读像素。
本机 `swift` 解释器构建模块会报错(`could not build module '_DarwinFoundation1'`),用下面这条纯标准库的路子:
```bash
sips -s format bmp /tmp/shot.jpg --out /tmp/shot.bmp # sips 输出 24bpp、自下而上排列的 BMP
python3 px.py /tmp/shot.bmp # 见 docs/tools/px.py
```
`px.py` 会沿最左边一列扫描并打印所有颜色跳变点(最左列只经过背景,不经过文字),
再用 `pt = 像素 / 3`(iPhone 16 Pro 是 3x)换算成点。`webview-safe-area.md` 里的数字都是这么来的。
## 相关
- 只影响网页;`CameraView`、`ScanView` 等原生界面不受影响。
- 顶部 inset 与相机启动白屏无关,见项目根目录 `CLAUDE.md` 的「Notes and quirks」。