第 09 章:iOS —— UIKit 嵌套滚动与统一垂直驱动
1. iOS 的页面骨架
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 的页面复用策略
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)
}这里有三个意图:
- handoff tab 不创建原生 page;
- 相同 tab id 尽量复用旧 page,避免 table/slot host 全量重建;
- 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.frameheaderView.framepager.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:)。当动态字体变化时:
- header 和所有 pages 重算 preferred height;
- native engine 重新 layout;
slotLayoutDidChange通知 Surface;- 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 回调。