Skip to content

第 09 章:iOS —— UIKit 嵌套滚动与统一垂直驱动 ​

1. iOS 的页面骨架 ​

text
HomeContainerView : UIView, UIScrollViewDelegate
└── outerScrollView : UIScrollView
    ├── headerView : HomeContainerHeaderView
    ├── pager : UIScrollView (paging enabled)
    │   └── HomeContainerPageView x N
    │       └── UITableView
    └── tabsView : HomeContainerTabsView

与 Android 不同,iOS 维护的是自定义 pager scroll view、nested table view、gesture recognizer delegate 和 page transition state。代码中显式区分 VerticalScrollOwner.header 与 .body。

2. iOS 17.4+ 与 legacy fallback ​

usesUnifiedVerticalDriver 在 iOS 17.4+ 返回 true,旧系统保持 legacy nested-scroll fallback:

  • 新路径尽量让一个 outer driver 管理垂直惯性;
  • 旧路径用 outer scroll 与 page table 的 content offset 协调;
  • pager 横向手势和垂直手势通过 require(toFail:)、directional lock、自定义 recognizer 协同。

这是一个很典型的兼容性选择:不为了追求最新系统行为而放弃旧系统,而是把更强的惯性一致性限制在能可靠支持的平台版本。

3. 首状态 apply 必须等待 bounds ​

submitInitialState 会同步 decode,但不会立刻无条件构建 UIKit 内容;它先把 pendingInitialState 存起来,等:

  • 已经 attach 到 window;
  • bounds width/height 均大于 0;
  • 没有另一个 initial apply scheduled;

然后在 main queue 调用 applySnapshot。原因是 snapshot apply 会构建和布局 UIKit 内容,太早执行会得到零尺寸 pager、空 table 或错误 slot host。

后续 submitState 则在 parsingQueue decode,再回到 main queue;owner changed 时先 prepareViewportForOwnerChange,清理 transition、refresh、scroll 和 page viewport。

4. Snapshot apply 的页面复用策略 ​

swift
let inlineTabs = next.tabs.filter { $0.destination == .inline }
for tab in inlineTabs {
  let page = oldPages[tab.id] ?? makePage(tabId: tab.id)
  page.apply(tab: tab, theme: next.theme,
             allowsAnimatedDifferences: !ownerChanged)
  nextPages.append(page)
}

这里有三个意图:

  1. handoff tab 不创建原生 page;
  2. 相同 tab id 尽量复用旧 page,避免 table/slot host 全量重建;
  3. owner 变化禁止 animated differences,避免把 owner A 的视觉过渡动画带到 owner B。

5. Pager transition 状态机 ​

HomeContainerTabSelectionQueue 只保留最新 pending request;如果过渡期间来了多个 Store selectedTab 更新,不会积累一串互相冲突的动画,而是以最后一个请求为准。

6. 统一垂直滚动 ​

页面高度由 header、tabs、pager 和 selected page content 共同决定。iOS 会在 layoutSubviews 更新:

  • outerScrollView.frame
  • headerView.frame
  • pager.frame
  • 每个 page 的 frame 与 pager contentSize
  • 当前 page 的统一 vertical content size
  • shared chrome layout

旧系统路径还会在 page content offset 变化时调用 coordinateNestedScroll,在 header 和 body 之间转移滚动 ownership;新路径则以统一 driver 做更直接的同步。

7. iOS 的 slot host 与动态字体 ​

HomeContainerView 在 header、tabs、page 内提供 slotHostView(forKey:)。当动态字体变化时:

  1. header 和所有 pages 重算 preferred height;
  2. native engine 重新 layout;
  3. slotLayoutDidChange 通知 Surface;
  4. Surface 重新把 React slot 调整到 host bounds。

因此 slot 的高度不能只在首次 render 时测一次;动态 type、图片加载、列表 attach/detach 和动画都可能改变 host 几何。

8. iOS 的小结 ​

iOS 方案把难点集中在 nested scroll、pager 和 UIKit reuse;它和 Android 的具体容器不同,但共享同一套协议语义:合法 snapshot、inline/handoff、owner 认证、slot host lookup、refresh ack 和 intent 回调。

基于 app-monorepo 2026-07-31 源码快照整理 · 非官方文档