Files
gaojie 1ea74b46da
Sync to site1 / sync (push) Has been cancelled
chore: update CrowdRoom categories from worldmodel to CrowdRoom
2026-05-21 02:20:22 +08:00

40 KiB
Raw Permalink Blame History

title, date, draft, tags, categories
title date draft tags categories
CrowdRoom · iOS App 设计(v0.2 2026-05-20 false
CrowdRoom
众包
3D 重建
导航
隐私
iOS
CrowdRoom

CrowdRoom · iOS App 设计(v0.2

版本v0.22026-05-19 v0.2 修订:回写 G-4(§8.4 PrivacyManifest 新节,Apple 强制项)、G-5(§8.1 NSLocationWhenInUseUsageDescription 文案明确「精确 GPS 不会上传」)、G-8(§1.2 IA 跳转图追加 PrivacyDetail 节点)。源决策见 09_privacy.md §4 P-2 / P-3 与 §6.2 C-P2-6。

本章承接 00_overview.md §4 架构图、01_data_schema.md 的 9 张表与 layer_manifest.json schema、以及 02_api_contract.md §2 的 19 个端点(v0.2,原 16 个 + 3 个新增)与 §8.1 的 5 条 iOS 硬契约(iOS-X1 ~ iOS-X5),落地为一份可直接交付给 iOS 工程团队的 App 设计。

复用 plans/iphone/iphone_simplified_plan.md 的扫描引导思想与 plans/iphone/roomplan_accuracy_and_cad_export.md 的 RoomPlan 精度边界,但裁剪到「只上传、不出图」的轻量端。

iOS 端只做三件事:采集(RoomPlan+ 端侧脱敏(Vision + CIGaussianBlur+ 上传到 Supabase。Remix 编辑、Web 渲染、转码、审核——一概不做。


1. iOS App 信息架构(IA

1.1 整体导航:5 Tab + 模态扫描

CrowdRoom iOS 采用 5 个底部 Tab + 一个独立模态扫描页的结构。扫描入口故意放在 Tab Bar 正中间作为大按钮(参考 Instagram Reels 的 + 设计),强化「采集」是 App 的第一动作。

Tab 图标 功能
1. 发现 🔭 公共 feed、搜索、标签筛选
2. 通知 🔔 我的房间被 Remix / 评论 / 点赞、转码完成通知
3. 扫描 ★ +(中间凸起大按钮) 唤起 RoomCaptureView 模态,全屏覆盖;扫描结束回 Tab 1 或我的
4. 我的房间 📁 我上传的 / 草稿 / 失败需要重试
5. 我的 👤 Profile、配额、设置、登录态

Remix 编辑入口只在 Web 端(见 02_api_contract.md §4),iOS 端只提供「在浏览器中打开并 Remix」的深链接跳转,不放独立 Tab。

1.2 页面跳转图

graph TD
    Launch[启动页]
    Auth[登录 注册页]
    AppleLogin[Sign in with Apple]
    EmailLogin[邮箱登录]

    TabDiscover[Tab1 发现 Feed]
    TabNotif[Tab2 通知列表]
    TabScan[Tab3 扫描入口大按钮]
    TabMyRooms[Tab4 我的房间]
    TabProfile[Tab5 我的]

    SearchPage[搜索 Tag 筛选页]
    RoomDetail[房间详情 360 预览]
    CommentList[评论列表]
    ReportSheet[举报弹层]
    RemixLaunchSheet[Remix 跳转确认弹层]
    WebRemix[Safari 跳转 Web Remix 编辑器]

    ScanIntro[扫描引导页 三步]
    ScanCapture[RoomPlan 扫描中]
    ScanReview[扫描完成预览页]
    Redacting[端侧脱敏进度页]
    UploadForm[上传表单 标题 标签 可见性]
    UploadProgress[上传进度页]
    TranscodeWait[等待转码页 Realtime]
    UploadDone[发布完成页]

    DraftList[草稿列表]
    QuotaPage[配额详情页]
    SettingsPage[设置页]
    PrivacyPage[隐私脱敏策略页]
    PrivacyDetail[单房间 隐私详情 脱敏检测框列表]
    AccountDelete[注销账号并删除全部数据]

    Launch --> Auth
    Auth --> AppleLogin
    Auth --> EmailLogin
    AppleLogin --> TabDiscover
    EmailLogin --> TabDiscover

    TabDiscover --> SearchPage
    TabDiscover --> RoomDetail
    SearchPage --> RoomDetail
    RoomDetail --> CommentList
    RoomDetail --> ReportSheet
    RoomDetail --> RemixLaunchSheet
    RemixLaunchSheet --> WebRemix

    TabScan --> ScanIntro
    ScanIntro --> ScanCapture
    ScanCapture --> ScanReview
    ScanReview --> Redacting
    Redacting --> UploadForm
    UploadForm --> UploadProgress
    UploadProgress --> TranscodeWait
    TranscodeWait --> UploadDone
    UploadDone --> RoomDetail

    TabNotif --> RoomDetail
    TabMyRooms --> RoomDetail
    TabMyRooms --> DraftList
    DraftList --> UploadForm

    TabProfile --> QuotaPage
    TabProfile --> SettingsPage
    SettingsPage --> PrivacyPage
    SettingsPage --> AccountDelete
    TabMyRooms --> RoomDetail
    RoomDetail --> PrivacyDetail

24 个页面节点v0.2,原 22 个 + v0.2 / G-8 追加 PrivacyDetail + v0.2 / P-4 配套追加 AccountDelete)。

🔄 v0.2 — 回写自 G-8:上图追加 PrivacyDetail 节点(路径 我的房间 → 单房间详情 → 隐私详情),对应 09_privacy.md §4 P-2 「owner 可在『我的房间 → 隐私详情』查看每个 bbox 缩略图并对漏检/误检发起『申请重做』工单」。AccountDelete 节点为 09_privacy.md §4 P-4 三阶段注销流程的 iOS 入口(路径 Tab5 我的 → 设置 → 注销账号并删除全部数据),调用 02_api_contract.md §2 E-18 account-delete


2. 核心用户流程(Critical Flows

2.1 Flow A:扫描 → 端侧脱敏 → 三段式上传 → 等待转码 → 发布

契约 iOS-X1(端侧人脸检测 + 高斯模糊)、iOS-X2(三段式上传)、iOS-X3JSON 原样)、iOS-X4Realtime 订阅)、iOS-X5(预查配额)全部出现在此流程

sequenceDiagram
    autonumber
    participant U as 用户
    participant App as iOS App
    participant V as Vision  CoreImage
    participant E as Edge Function
    participant S as Supabase Storage
    participant DB as Postgres
    participant W as Transcode Worker
    participant RT as Realtime Channel

    U->>App: 点击 Tab 中间 + 按钮
    Note over App: iOS-X5 进扫描页前预查配额
    App->>E: GET functions v1 quota
    E-->>App: tier free storage used limit
    alt 配额超限
        App-->>U: 本地拦截 弹窗提示升级
    end

    U->>App: 走完三步引导 开始 RoomCaptureView
    App->>App: RoomPlan 采集 5 10 分钟
    U->>App: 点击完成
    App->>App: 导出 source usdz 与 roomplan json
    Note over App: iOS-X3 RoomPlan JSON 原样保留 不做坐标变换

    Note over App,V: iOS-X1 端侧脱敏开始
    App->>V: 解压 usdz 遍历每张贴图 PNG JPEG
    V->>V: VNDetectFaceRectanglesRequest
    V-->>App: 命中 face bbox list
    App->>V: CIGaussianBlur radius 18 仅作用于命中区域
    V-->>App: 模糊后贴图
    App->>App: 重新打包 usdz 同时把 face bbox 累积到 redactions

    U->>App: 填写标题 标签 可见性 公开 私有
    App->>E: POST upload-init title tags bytes_source
    E->>DB: insert rooms 与 room_versions status uploading
    E->>S: sign presigned URL usdz 与 json
    E-->>App: version_id presigned_usdz presigned_json
    Note over App,S: iOS-X2 三段式 第二段 直传 Storage
    App->>S: PUT source usdz 大于 30 MB 走分片 multipart
    App->>S: PUT source roomplan json
    App->>E: POST upload-complete version_id redactions
    E->>DB: update status queued insert redactions rows
    E->>W: POST enqueue version_id

    Note over App,RT: iOS-X4 订阅 Realtime 不轮询
    App->>RT: subscribe channel room room_id
    W->>W: usdz to glb 分层标注 manifest 缩略图
    W->>E: POST transcode-done status ready paths
    E->>DB: update status ready current_version_id
    E->>RT: broadcast event ready version_id
    RT-->>App: WebSocket push 收到 ready
    App-->>U: 弹卡片 你的扫描已上线 跳房间详情

2.2 Flow B:浏览 → 房间详情 → 触发 Remix → 跳 Web

iOS 端不做 Remix 编辑(编辑在 Web 端 R3F 渲染器里),仅做发起入口。Universal Link 透传 room_id + version_id,落地 Web 域名 crowdroom.app/r/{room_id}?remix=1

sequenceDiagram
    autonumber
    participant U as 用户
    participant App as iOS App
    participant PG as PostgREST
    participant CDN as CDN
    participant SF as SFSafariViewController

    U->>App: Tab 发现 下拉刷新
    App->>PG: GET rest v1 rooms visibility eq public order created_at desc limit 20
    PG-->>App: Room 列表 含 thumbnail_path
    App->>CDN: GET thumbnail webp 列表卡片
    CDN-->>App: 缩略图

    U->>App: 点击某张卡片
    App->>PG: GET rest v1 rooms id eq room_id with current_version
    PG-->>App: Room 详情

    Note over App: iOS 端仅展示静态 360 缩略图轮播
    Note over App: 不在端内渲染 glb 太重 留给 Web

    U->>App: 点击 Remix 按钮
    App-->>U: 弹层 提示 Remix 编辑器在浏览器中体验更好
    U->>App: 点击 确认
    App->>SF: 打开 https crowdroom app r room_id remix 1
    SF-->>U: 进入 Web Remix 编辑器

2.3 Flow C:通知到达 → 跳详情

sequenceDiagram
    autonumber
    participant RT as Realtime Channel
    participant App as iOS App 后台 或 前台
    participant UN as UNUserNotificationCenter
    participant U as 用户
    participant PG as PostgREST

    Note over App,RT: App 启动时 已订阅 user user_id 个人频道
    RT-->>App: push event remix_created 或 comment_new
    alt App 在前台
        App-->>U: Toast 提示 你的房间被 Remix
    else App 在后台
        App->>UN: scheduleLocalNotification 标题 房间被 Remix
        UN-->>U: 系统通知中心展示
        U->>UN: 点击通知
        UN->>App: 唤起 app 携带 deeplink room_id
    end
    App->>PG: GET rest v1 rooms id eq room_id
    PG-->>App: Room 详情
    App-->>U: 跳房间详情页 评论列表锚到该条

3. 关键技术栈与库选型表

模块 推荐方案 备选 一行理由
语言 / 最低 iOS Swift 5.9 + iOS 17.0 iOS 16.0 RoomPlan API 在 17+ 增加了 second-pass mesh 优化与更稳的 MultiRoom 支持;Realtime SDK 也对 17 友好
UI 框架 SwiftUI 主,UIKit 局部桥接 纯 UIKit SwiftUI 写 Tab/列表/表单效率高;扫描页的 RoomCaptureViewUIViewControllerRepresentable 桥接
RoomPlan 集成 RoomCaptureView 标准引导 RoomCaptureSession 自定义 MVP 不重写引导动画;标准 View 自带语音/手势提示,省 2 周 UX 工时;后续如要换皮再切 Session
端侧人脸检测 Vision VNDetectFaceRectanglesRequest CoreML + 自训模型 系统级、无依赖、iPhone 13+ 单张 1024×1024 < 80 msRevision 3 召回率够
贴图脱敏 Core Image CIGaussianBlur + 蒙版合成 Metal Shader 自写 CIFilter 链路成熟、GPU 加速、CIContext 直接渲到 CGImage 写回 PNG
.usdz 解包/重打包 ModelIO + USDKitiOS 17+ Compression framework 外挂 usdzconvert CLI iOS 沙箱不能跑 CLIModelIO 支持读 USD,贴图替换走解包→改文件→重压 zip(usdz 本质是 zip
网络层 URLSession + async/await + URLSessionUploadTaskbackground config Alamofire 无三方依赖;backgroundSessionConfiguration 是 App 进后台续传的唯一官方路径
Supabase SDK supabase-swift ≥ 2.0 手撸 REST + WebSocket 官方维护、Realtime channel + Auth + Storage 三件套统一 SDK
Realtime 监听 supabase-swift Realtime channel room:{id} + user:{id} 自建 SSE iOS-X4 强制要求订阅模型;SDK 处理重连与心跳
本地缓存 SwiftDataiOS 17 Core Data / Realm 草稿、未完成上传任务、Feed 分页缓存;SwiftData 与 SwiftUI 双向绑定省胶水代码
性能/崩溃监控 MetricKit(系统)+ Sentry iOS SDK Firebase Crashlytics MetricKit 拿 RoomPlan 期 GPU/热量数据;Sentry 与 Supabase 后端 Sentry 共享 issue 视图
深链接 Universal Linksapple-app-site-association URL Scheme Web Remix 编辑器跳回 App、通知点击跳详情都走 UL;URL Scheme 不安全且会被 Safari 拦截
图像渲染 Image(uiImage:) + AsyncImage + 自建磁盘缓存 Kingfisher / SDWebImage 列表缩略图为主,自建 LRU 200 MB 够用;不引三方避免主线程阻塞

4. 端侧脱敏管线详细设计(iOS-X1 落地)

契约 iOS-X1:上传前必须在端侧跑人脸检测 + 贴图高斯模糊,并把命中区域写入 redactions[],不依赖服务端二次脱敏。本节是 X1 的完整工程落地

4.1 管线流程

graph TD
    A[RoomPlan 导出 source usdz] --> B[Compression 解包 usdz 到 tmp redact 目录]
    B --> C[ModelIO 枚举所有 MDLTexture 引用]
    C --> D[拿到贴图文件路径列表 PNG JPEG]
    D --> E[对每张贴图执行 Vision 人脸检测]
    E --> F{检测到人脸}
    F -- 无 --> G[原图不动]
    F -- 有 --> H[CIGaussianBlur radius 18 渲染整图]
    H --> I[用人脸 bbox 做蒙版 仅模糊区域合成回原图]
    I --> J[写回贴图文件 同名覆盖]
    G --> K[累计到 redactions list]
    J --> K
    K --> L[Compression 重新打包 usdz]
    L --> M[校验 sha256 与体积 失败回滚]
    M --> N[redactions 数组准备 POST 到 upload-complete]

4.2 Swift 伪代码骨架

import RoomPlan
import Vision
import CoreImage
import ModelIO
import Compression

// 脱敏管线主入口
// 输入 RoomPlan 导出的 sourceUSDZ URL
// 输出 脱敏后的新 usdz URL 与 redactions 数组
func redactUSDZ(at sourceURL: URL) async throws -> (URL, [Redaction]) {
    // 1 解包 usdz 实质是无压缩 zip 容器
    let workDir = FileManager.default.temporaryDirectory
        .appendingPathComponent("redact_\(UUID().uuidString)")
    try unzipUSDZ(source: sourceURL, into: workDir)

    // 2 用 ModelIO 枚举所有贴图引用
    let asset = MDLAsset(url: workDir.appendingPathComponent("scene.usdc"))
    let textureURLs = collectTextureURLs(in: asset, workDir: workDir)

    // 3 对每张贴图跑 Vision 检测 + 模糊
    var redactions: [Redaction] = []
    let ctx = CIContext(options: [.useSoftwareRenderer: false])

    for texURL in textureURLs {
        guard let cgImage = loadCGImage(texURL) else { continue }

        // 3 1 Vision 人脸检测
        let request = VNDetectFaceRectanglesRequest()
        request.revision = VNDetectFaceRectanglesRequestRevision3
        let handler = VNImageRequestHandler(cgImage: cgImage, options: [:])
        try handler.perform([request])
        guard let faces = request.results, !faces.isEmpty else { continue }

        // 3 2 命中 用 CIGaussianBlur 整图模糊
        let ciOriginal = CIImage(cgImage: cgImage)
        let blurred = ciOriginal
            .applyingFilter("CIGaussianBlur", parameters: [kCIInputRadiusKey: 18.0])
            .cropped(to: ciOriginal.extent)

        // 3 3 把人脸 bbox 转成蒙版图 把模糊层贴回原图
        let maskCI = buildFaceMask(faces: faces, extent: ciOriginal.extent)
        let composited = blurred.applyingFilter(
            "CIBlendWithMask",
            parameters: [
                kCIInputBackgroundImageKey: ciOriginal,
                kCIInputMaskImageKey: maskCI
            ]
        )

        // 3 4 写回贴图文件 同名覆盖 保持 ModelIO 引用不变
        guard let outCG = ctx.createCGImage(composited, from: composited.extent) else { continue }
        try writePNG(outCG, to: texURL)

        // 3 5 记录到 redactions 数组 上传时 POST 给 Edge Function
        for face in faces {
            redactions.append(Redaction(
                textureRef: texURL.lastPathComponent,
                kind: "face",
                region: face.boundingBox,    // 归一化坐标 0 1
                method: "gaussian_blur_r18"
            ))
        }
    }

    // 4 重新打包 usdz 校验
    let redactedURL = workDir.appendingPathComponent("redacted.usdz")
    try zipUSDZ(folder: workDir, output: redactedURL)
    try validatePackaged(redactedURL)

    return (redactedURL, redactions)
}

关键点:usdz 是无压缩 zip,重新打包必须用 compression_encode_bufferCOMPRESSION_LZFSE_NONE 或直接走 store-only zip——否则 Apple 工具链识别不出来。

4.3 性能预算

阶段 iPhone 15 Pro 目标 iPhone 13 Pro 目标 备注
解包 60 MB usdz < 0.5 s < 1.0 s I/O bound
Vision 检测 20 张 1024² 贴图 < 2.5 s < 4.5 s Neural Engine
CIGaussianBlur + 合成 < 1.5 s < 2.5 s GPU bound
重打包 < 0.5 s < 1.0 s I/O
总计 ≤ 8 s ≤ 12 s iPhone 12 Pro 预期 16 s 见 §10 风险

UI:脱敏期间显示带百分比的进度页,文案「正在检查照片中的人脸…」。

4.4 失败兜底

  • 重试机制:单张贴图脱敏失败(如 ModelIO 解不开纹理)→ 重试 1 次。整体管线失败 → 重试最多 3 次。
  • 3 次失败后:弹窗提示「我们没能自动模糊照片里的人脸。建议你关闭含人的扫描区域重扫,或手动框选要模糊的区域」,给出两个按钮:「重新扫描」与「手动标注后上传」。
  • 手动标注降级:进入一个 UIScrollView 缩略图墙,让用户长按贴图后框选矩形,前端写到 redactions[kind=manual],仍走相同上传流。
  • 「不脱敏直接上传」按钮一律不提供——这是 iOS-X1 的硬约束。

5. 三段式上传实现(iOS-X2 落地)

契约 iOS-X2:上传走 upload-init → 直传 Storage → upload-complete 三步,禁止把 .usdz 字节流塞进 Edge Function body。

5.1 顺序图

sequenceDiagram
    autonumber
    participant App as iOS App
    participant E as Edge Function
    participant S as Supabase Storage

    App->>E: POST upload-init title tags bytes_source
    E-->>App: version_id presigned_usdz presigned_json expires_at
    App->>App: 创建 URLSession backgroundConfiguration identifier crowdroom upload version_id
    par 并行 双 PUT
        App->>S: PUT source usdz 大于 30 MB 走 multipart 5 MB chunk
        App->>S: PUT source roomplan json
    end
    S-->>App: 200 OK 两次
    App->>E: POST upload-complete version_id redactions
    E-->>App: status queued

5.2 关键实现要点

场景 策略
大文件分片 .usdz > 30 MB 启用 multipart PUT5 MB 一片,并行 3 路;Supabase Storage 支持 S3 兼容的 multipart
断点续传 URLSessionUploadTask + 自管理 Range 头;SwiftData 表 upload_chunks 记录每片状态(pending/sent/acked),App 重启后扫表续传
App 进后台 URLSession(configuration: .background(withIdentifier:)) 让系统在 App 被挂起后继续传;完成时通过 application(_:handleEventsForBackgroundURLSession:completionHandler:) 唤醒
网络切换 注册 NWPathMonitor,从 Wi-Fi 切到蜂窝时暂停上传并弹窗:「当前已切换到蜂窝网络,继续上传可能产生流量费用」(默认开关:仅 Wi-Fi)
电量低于 20% 启动上传前查 UIDevice.current.batteryLevel,< 0.20 时弹窗「电量较低,是否仍继续上传?」并禁用 multipart 并行(降为 1 路)
超时与退避 单片超时 60 s,整体 20 分钟;指数退避 5 s → 15 s → 45 s,3 次失败后转 failed 入草稿
presigned 过期 expires_at 提前 30 s 触发 upload-init 重签;server-side 已设 15 min TTL,足以覆盖大文件

5.3 错误码 → 用户文案映射

业务错误码(来自 02_api_contract.md §7 iOS 文案 后续动作
QUOTA_EXCEEDED "本月上传额度已用完,下月 1 号重置" 跳「我的 → 配额详情」
FILE_TOO_LARGE "扫描文件超过 50 MB,请尝试缩小扫描范围" 跳「重新扫描」
INVALID_TAGS "标签数量超过 8 个或含非法字符" 高亮表单标签输入框
STORAGE_FORBIDDEN "上传授权已过期,正在重新申请…" 自动调 upload-init 重签 1 次
NETWORK_TIMEOUT(本地判定) "网络连接超时,已为你保存草稿" 写入草稿表,Tab 4 可见
REDACTION_INVALID "脱敏信息格式错误,请重新扫描" 跳「重新扫描」;同时 Sentry 上报(端 bug)
ROOM_TRANSCODE_FAILED "云端处理失败" 展示「重试」按钮 → transcode-retry3 次失败后建议人工反馈
WORKER_UNAVAILABLE "服务繁忙,已自动加入队列,1 分钟后重试" 60 s 后自动重发 upload-complete
UNAUTHENTICATED "登录已过期,请重新登录" 弹登录页(保留草稿)
RATE_LIMITED "操作过于频繁,请稍后再试" 30 s 冷却倒计时

6. Realtime 转码进度 UIiOS-X4 落地)

契约 iOS-X4:监听 Realtime channel room:{room_id} 等待 ready 事件,禁止轮询 room_versions.status

6.1 等待页 UX

进入「等待转码」页后展示一个环形进度(CircularProgressView),中间显示阶段文案,下方有「在后台等待」与「取消并保存草稿」两个按钮。

阶段(来自 room_versions.status enum 文案 环形进度估算(无真实百分比,按阶段递增)
uploading "上传中…" 0 → 25%
queued "已加入处理队列" 25% → 35%
transcoding "解析中 / 分层中 / 生成预览…" 35% → 90%(每收一次 progress payload +5%
ready "发布完成 🎉" 100%
failed "处理失败" 红色 × ,展示重试按钮

阶段进度没有真实百分比(Worker 不上报中间百分比),iOS 端用「阶段映射 + 时间外推」假装平滑;Realtime payload 一旦到 ready,直接跳 100%。

6.2 Realtime 订阅代码骨架

let channel = supabase.realtime.channel("room:\(roomId)")
channel.on("broadcast", filter: .init(event: "transcode_progress")) { msg in
    // 阶段切换 触发 UI 动画
    Task { await viewModel.updatePhase(msg.payload["status"] as? String) }
}
channel.on("broadcast", filter: .init(event: "ready")) { msg in
    Task { await viewModel.markReady(versionId: msg.payload["version_id"] as? String) }
}
channel.on("broadcast", filter: .init(event: "failed")) { msg in
    Task { await viewModel.markFailed(error: msg.payload["error"] as? String) }
}
await channel.subscribe()

重连Supabase SDK 内置 30 s 心跳 + 指数退避重连。断线期间错过的事件由 App 重连后主动调一次 GET /rest/v1/room_versions?id=eq.{id} 补查最终状态(这是唯一允许的「兜底查询」,不是轮询)。

6.3 后台模式与本地推送

  • App 进后台时 Realtime channel 会被 iOS 暂停(WebSocket 不能在后台保活)
  • 解决方案:App 进后台前若仍在等待转码 → 注册 BGAppRefreshTask,约 15 分钟后唤醒一次后台拉取
  • 后台任务执行时调 GET /rest/v1/room_versions?id=eq.{id} 单次查询,若已 ready → 通过 UNUserNotificationCenter 发本地通知「你的扫描已发布」
  • 不依赖服务端 APNs push(MVP 不接苹果推送证书),全部走本地通知

6.4 失败重试入口

  • failed 事件到达时,等待页转为「失败页」,展示错误码对应文案 + 「重试」按钮
  • 「重试」按钮调 POST /functions/v1/transcode-retry02_api_contract 未列,作为 iOS 期望接口提给后端)
  • 失败 3 次后,「重试」按钮隐藏,改显「联系客服」入口(跳邮件 mailto:

7. 扫描引导 UX 与质量门控

复用 plans/iphone/iphone_simplified_plan.md §3.1 的「准备 / 慢速移动 / 覆盖检查」三阶段思想,但裁剪为消费级三步引导。精度边界引用 plans/iphone/roomplan_accuracy_and_cad_export.md §1.2 的 ±3 cm 墙面 / ±5 cm 家具。

7.1 三步引导

步骤 标题 文案与示意 用户操作
Step 1 · 准备 "把房间整理一下" 三条建议:① 打开所有灯 ② 移走移动物体(宠物/人) ③ 清洁 LiDAR 镜头 点「我准备好了」
Step 2 · 慢走 "用 0.3 m/s 的速度环绕房间" 短视频示意 + RoomCaptureView 自带的语音引导一起播 进入 RoomCaptureView,扫描 510 分钟
Step 3 · 完成 "看一下你的房间" 展示 RoomPlan 生成的 wireframe 预览 + 质量分 「重扫」或「下一步:脱敏并上传」

7.2 实时质量提示(扫描中浮层)

RoomCaptureView 上叠加一个 SwiftUI 浮层,每 1 s 从 RoomCaptureSession.Delegate 读取一次状态:

信号 检测方式 浮层提示
覆盖率不足 RoomPlan instructions == .lowTexture 或墙面置信度 .low 的占比 > 30% "🔍 这面墙再扫一遍"
漏扫墙面 房间未闭合(拓扑检测:墙数 < 3 或存在自由端) "↩️ 似乎少了一面墙"
强反光面 检测到 WindowMirror(用 RoomPlan category)正对镜头 "⚠️ 镜面会影响精度,请侧 30° 扫描"
速度过快 ARKit transform 一阶差分 > 0.6 m/s 持续 2 s "🐢 慢一点,0.3 m/s 最佳"

7.3 完成后质量分(A/B/C

扫描结束后用一个简单规则给房间打分(不是机器学习模型,是规则引擎,避免 MVP 复杂度):

维度 A 档 B 档 C 档
墙面 confidence high 占比 ≥ 90% 7090% < 70%
家具识别数 ≥ 5 件 24 件 < 2 件
房间闭合 拓扑不闭合
扫描时长 510 min 35 min < 3 min

任一维度命中 C → 整体 C 档

  • A/B 档:直接进上传流,UI 给绿勾或黄勾标记
  • C 档:弹窗「这次扫描质量较低,建议重扫;如仍上传,我们会给它打『C 档』标签,发现页排序权重会降低」——仍允许上传(社区数据不挑食),但 rooms.quality_grade 字段标 C02_api_contract.md E-06 列表查询时排序权重 × 0.5

8. 隐私与权限请求文案

8.1 Info.plist 条目

Key 中文 usage description
NSCameraUsageDescription "CrowdRoom 需要相机权限来扫描你的房间。我们只会使用相机进行 3D 建模,不会单独保存照片到相册。"
NSPhotoLibraryAddUsageDescription "CrowdRoom 需要相册写入权限,以便把你扫描完成的 3D 房间预览图保存到相册(可选)。"
NSLocationWhenInUseUsageDescription v0.2 文案"CrowdRoom 可选使用你的位置,仅为给你扫描的房间打上「城市」级标签(约 5 km 精度),该城市标签会显示在你的公开房间页你的精确 GPS 坐标不会上传、不会存储到服务器。你可以随时在「设置 → 隐私 → 位置打标」里关闭。"
NSMicrophoneUsageDescription 不申请RoomPlan 不需要录音)
NSUserTrackingUsageDescription 不申请(MVP 不做跨 App 跟踪,见 8.2

🔄 v0.2 — 回写自 G-5:本节根据 09_privacy.md §4 P-3 决策追加。NSLocationWhenInUseUsageDescription 文案在 v0.2 起必须显式包含「城市标签会显示在公开房间页」与「精确 GPS 不会上传」两句——前者满足 PR-3 用户可控的「授权时即告知公开后果」,后者满足 PR-2 最小数据采集的「告知不持有原始坐标」。该文案变更同时对齐 01_data_schema.md §3.2 rooms.location_city= location_label 别名)字段语义与 04_web_app_plan.md §8.2 详情页元信息行。

8.2 ATTApp Tracking Transparency

结论:MVP 不申请 ATT

  • CrowdRoom MVP 不接广告 SDK、不与第三方数据公司共享 IDFA
  • Sentry / MetricKit 不使用 IDFA,走 Apple 私有指标
  • 后续如接入 Apple Search Ads 归因,再单独申请 ATT 并补充权限弹窗文案
  • 这一决策与本任务规划的「隐私治理」预留接口由子任务 5 收口

8.3 首次启动权限弹窗顺序

  1. 进入扫描页 → 申请 Camera(必要)
  2. 扫描完成后想保存预览图到相册 → 申请 PhotoLibraryAdd(按需)
  3. 上传表单页填写「城市」标签时 → 申请 LocationWhenInUse(按需,可跳过)

契约 iOS-X5 衍生:进入扫描页时同时调 GET /functions/v1/quota,与相机权限请求并行发起,弹窗与配额预检不互相阻塞。

8.4 PrivacyManifestPrivacyInfo.xcprivacy

🔄 v0.2 — 回写自 G-4:本节根据 09_privacy.md §6.2 C-P2-6Apple 2024 春起强制,本已 cross-ref 为「MVP 待补」)新增。Apple App Store Connect 在 Xcode 15+ 上传时会自动跑 Privacy Manifest Aggregate Report;缺失或第三方 SDK 未声明会直接拒审

8.4.1 我们自己声明(CrowdRoom App bundle 内)

PrivacyInfo.xcprivacy 必须列出 4 类 Required Reason API 使用类别(CrowdRoom 业务场景下能触发的常见类别):

NSPrivacyAccessedAPICategory 触发场景 选择的 reason code
NSPrivacyAccessedAPICategoryFileTimestamp .usdz / .roomplan.json 文件 creationDate 用于上传顺序排序 C617.1(在 App 内用于显示给用户)
NSPrivacyAccessedAPICategorySystemBootTime MetricKit / Sentry 性能事件相对时间戳 35F9.1(计算相对设备启动时间)
NSPrivacyAccessedAPICategoryDiskSpace 扫描前预估「设备剩余空间是否够暂存原始 .usdz」 85F4.1(在 App 内显示空间不足提示)
NSPrivacyAccessedAPICategoryUserDefaults 持久化「位置打标开关 / 端侧脱敏统计」等用户偏好 CA92.1(同一 App 内读写)

同时声明 数据收集分类NSPrivacyCollectedDataTypes),与 App Store Connect Privacy Nutrition Label 同源:

数据类型 是否收集 用途 关联用户
NSPrivacyCollectedDataTypeUserID App Functionality(鉴权) 关联
NSPrivacyCollectedDataTypeEmailAddress App Functionality(账号恢复) 关联
NSPrivacyCollectedDataTypeOtherUserContent(房间几何 + 标题 + 评论) App Functionality 关联
NSPrivacyCollectedDataTypeCoarseLocation(城市级 5 km (可选) App Functionality(公开标签) 关联
NSPrivacyCollectedDataTypePreciseLocation
NSPrivacyCollectedDataTypeCrashData App FunctionalitySentry 不关联
NSPrivacyCollectedDataTypePerformanceData AnalyticsPostHog,需 opt-in 不关联
NSPrivacyCollectedDataTypeAdvertisingData
NSPrivacyCollectedDataTypeTrackingIDIDFA

8.4.2 第三方 SDK PrivacyManifest 自查表

每个引入的 SDK 都必须自带 PrivacyInfo.xcprivacy(Apple 维护一份强制 SDK 名单:OpenSSL / FMDB / SQLite 等通用基础库以及主流分析/广告 SDK 都在其中)。MVP 引入的 SDK 自查状态:

SDK 是否在 Apple 强制列表 SDK 已自带 PrivacyManifest 版本下限 备注
Supabase SwiftGoTrue + PostgREST + Storage + Realtime ⚠ 部分依赖(如 SQLite)在列 自 v2.0+ 已自带;底层 URLSession 无需 v2.5.0+ Apple 通用网络栈无需自带
Sentry-Cocoa 在列 自 v8.20+ 自带 v8.25+ 关键:必须升级到 ≥ v8.20 才能过审
PostHog iOS 在列(涉及 UserDefaults + SystemBootTime 自 v3.0+ 自带 v3.5+ MVP 仅在用户 opt-in 后初始化
Lucide Icons(仅 SVG 资源,无 runtime 不在列 n/a n/a 资源包,不申报
Apple Vision / RoomPlan / ARKit n/a n/a(系统框架) iOS 17+ 系统框架不计入第三方

CI 检查项:在 GitHub Actions 上跑 xcodebuild 时附加 -checkPrivacyManifest YESXcode 15.3+ 支持的隐式 lint),任何缺失会直接 build fail。

8.4.3 上传到 App Store Connect 时的签名验证清单

按以下清单逐项勾选,否则放弃发版:

  1. ☐ Xcode 项目根目录下存在 PrivacyInfo.xcprivacy(不是 Resources/ 子目录)
  2. ☐ Archive 后 .ipa 解包,PrivacyInfo.xcprivacy 出现在主 bundle 根
  3. ☐ 所有 Frameworks/ 下的第三方 .framework / .xcframework 内部存在 PrivacyInfo.xcprivacy
  4. ☐ App Store Connect 上传后 24h 内查看 Privacy Manifest Aggregate Report,确认无 Missing reason codeMissing data type 警告
  5. ☐ 如有警告 → 找到对应 SDK 升级版本 → 重新 archive 上传
  6. ☐ 与 App Store Connect 「应用隐私详情」(Privacy Nutrition Label)字段逐项核对(同源;不一致会被 Apple 人工标红)

拒审风险等级:高。Apple 2024 春起对漏报的 PrivacyManifest 直接拒审(不再警告);本节的 6 项 checklist 必须在每次 minor 版发版前由 release manager 复核一次。


9. MVP 范围与不做项

9.1 MVP(与 00_overview.md §7.1 同步,8 周窗口)能交付

模块 交付物
Auth Sign in with Apple + 邮箱登录两条路径
Tab 1 发现 公共 feed 分页 + 标签筛选 + 搜索
Tab 2 通知 评论 / 点赞 / Remix 三类通知 + 转码完成通知
Tab 3 扫描 RoomPlan 标准 RoomCaptureView + 三步引导 + 质量分 + 端侧脱敏 + 三段式上传
Tab 4 我的房间 已发布 / 草稿 / 失败 三个分段控制器
Tab 5 我的 Profile、配额详情、设置、隐私脱敏策略说明
详情页 静态 360° 缩略图轮播 + 评论 + 点赞 + 跳 Web Remix
Realtime room:{id}user:{id} 两个 channel 订阅
配额 进扫描前预检 + 错误码本地拦截

9.2 明确不做

  • iOS 端 Remix 编辑器(统一在 Web 端做,原因:R3F + drei 在 iOS WKWebView 性能足够;做原生 SceneKit Remix 工作量约等于重写一遍 Web 渲染器)
  • AR 内即时换家具预览(QuickLook 也不接,避免「我以为我换了,实际服务端没收到」的双状态问题)
  • 协同编辑Remix 走 fork 模型)
  • Android / iPad 专属布局iPad 用 iPhone scaledMVP 不做 split view 适配)
  • Apple Watch / visionOS 端
  • 本地 .glb 渲染(详情页只展示缩略图,3D 渲染交给 Web)
  • 离线模式(草稿可保留,但浏览必须联网)
  • 自训 ML 模型做家具识别(一律走 RoomPlan 内置 16 类语义)

10. 风险与开放问题

# 风险 / 开放问题 当前判断 待解答
R-iOS-1 RoomPlan 在低光场景失败率高roomplan_accuracy_and_cad_export.md §1.3 提到光照影响 ±1–3 cm,且极弱光会直接拒绝开始扫描) UX 兜底:扫描前自动检测 AVCaptureDevice.iso,过高时弹窗建议开灯;仍允许强行扫描 是否需要在 App 内置「补光手电」开关?(受发热限制,可能不实用)
R-iOS-2 端侧脱敏在 iPhone 12 Pro 上耗时超目标(§4.3 估算 16 s,超 8 s 预算 2 倍) 12 Pro 是 RoomPlan 最低支持设备(无 12 Pro 就无 LiDAR),不能放弃;预计提示「正在处理,预计 15 秒」即可 是否对 12/12 Pro 默认降级到「仅检测 + 不模糊,把任务转交服务端」?——但这违反 iOS-X1,需要子任务 5 隐私治理章节回答能否破例
R-iOS-3 .usdz 重新打包工具链稳定性iOS 沙箱无法跑 usdzconvert CLI,只能用 ModelIO + 手动 zipModelIO 对 USD 写回支持有限 备选方案:不重打包,把脱敏后的贴图作为「平行文件」一起 PUT,让 Worker 端做替换合并。代价是 Worker 改造一次 是否可接受 Worker 帮忙合并?此决策由子任务 5 与后端方共同确认
R-iOS-4 Universal Link 在国内 Safari 跳转受限:部分国产浏览器(QQ/微信)不会触发 UL iOS 端从微信打开链接时,引导用户「点右上角 → 在 Safari 中打开」;这是行业通病不再投入解决 是否做微信小程序版的 Remix 编辑入口?P2 再说
R-iOS-5 Realtime channel 在长时间后台时丢事件WebSocket 在 iOS 后台 30 s 内被杀;BGAppRefreshTask 调度由系统决定,最长可能 1 小时才唤醒一次 兜底:每次 App 前台化时主动调一次 GET /rest/v1/room_versions?id=in.(...) 补查所有「进行中」版本 此「补查」是否会被风控误判为「轮询」?需要与 iOS-X4 契约文字微调(明确:「补查」≠「轮询」)

11. 给子任务 5(隐私治理)的契约要点

iOS 端在端侧脱敏管线(§4)、权限文案(§8)、风险 R-iOS-2/R-iOS-3 处与隐私治理章节有强耦合。子任务 5 撰写隐私治理总章时必须保证以下要点:

# 治理章节必须保证 X 与本章对应
P-1 明确「端侧脱敏失败 3 次后是否可降级到服务端二次脱敏」的策略——若不允许,则 iOS-X1 维持硬约束;若允许,则需定义服务端二次脱敏 SLA §4.4 + R-iOS-2
P-2 给出 redactions[] 表的保留期与可见性策略(用户能否查看自己上传的人脸 bbox 数据) §4.1 末尾的 redactions POST
P-3 明确「位置标签是否在公开页面展示」——若展示,则需补强 NSLocationWhenInUseUsageDescription 文案中的「会被其他用户看到」声明 §8.1 location 文案
P-4 给出用户「申请删除我所有数据」的端到端流程,iOS 端需要提供入口(设置 → 账号 → 注销账号并删除全部数据) §1.2 SettingsPage 待加子页
P-5 明确「ATT 何时启用」的触发条件(接入哪种 SDK 必须申请 ATT),iOS 端据此决定是否在某个版本灰度推 ATT 弹窗 §8.2
P-6 明确「未成年用户保护」策略:是否需要在 iOS 端首次启动时弹年龄确认;CrowdRoom 内容是否在 App Store 标 17+ 本章未覆盖,留给子任务 5

12. 本章小结

关键产出 一句话
5 Tab + 22 页面 IA 扫描放中间凸起按钮强化采集动作;Remix 编辑全在 Web,iOS 只做发起入口
3 个核心 Flow A 扫描脱敏上传等待发布 / B 浏览跳 Web Remix / C 通知跳详情
13 项技术栈选型 SwiftUI + iOS 17 + RoomCaptureView + Vision + CoreImage + supabase-swift + SwiftData
端侧脱敏管线 RoomPlan 导出 → ModelIO 解包 → Vision 检测 → CIGaussianBlur 蒙版合成 → 重打包,iPhone 15 Pro ≤ 8 s
三段式上传 + 10 条错误码文案 upload-init → 直传 Storage → upload-complete,背景 URLSession + 断点续传 + 分片 + 电量检查
Realtime 5 阶段进度 UI uploading → queued → transcoding → ready/failed,禁止轮询,后台用 BGAppRefreshTask 兜底
三步扫描引导 + A/B/C 质量分 复用 iPhone 简易方案的扫描节奏,规则引擎打分,C 档允许上传但排序降权
6 条治理契约移交 端侧脱敏破例、redactions 保留期、位置可见性、删除流程、ATT、未成年保护

读完本章你应能:

  • 给 iOS 工程师一份 8 周内可交付的功能清单
  • 评审端侧脱敏方案是否真的能落到 iPhone 12 Pro 上
  • 接手子任务 5 时知道隐私治理章节要回答哪 6 个问题

章节版本v0.1 · 草案 关键收获iOS 端是 CrowdRoom 的唯一采集入口最重的隐私防线——5 条硬契约(iOS-X1~X5)全部落到具体章节;3D 渲染、Remix 编辑、转码、审核一概甩给 Web 与服务端,保持 iOS 端「轻、快、合规」三件套。