源码注释: - 为 ~60 个 Swift 文件补充缺失的 doc comment(file header、类型、属性、方法) - 修正 4 处错误注释:翻页模式数量、搜索行为描述、手势识别器描述、悬空文档块 文档维护: - 删除重复文档:WXRead/读书EPUB阅读器实现架构.md(与微信读书版完全一致) - 合并重叠文档:阅读器规划.md → 阅读器功能开发计划.md(单一真值) - 修正过时内容:所有文档中"四种翻页模式"→"三种",移除 horizontalCoverScroll - 更新架构图:补齐 EPUBUI/ReaderController、Paging/、Typesetter/ 等子目录 - 更新 index.md 索引:新增开发计划和架构对比文档引用
137 lines
5.3 KiB
Swift
137 lines
5.3 KiB
Swift
import UIKit
|
||
|
||
/// 视口监测器,监听视图布局和屏幕旋转等视口变化事件,
|
||
/// 检测变化是否显著,必要时触发重新分页或内容重建。
|
||
final class RDEPUBReaderViewportMonitor {
|
||
private unowned let context: RDEPUBReaderContext
|
||
|
||
private var lastAppliedViewportSignature: RDEPUBViewportSignature?
|
||
private var pendingViewportChangeReason: RDEPUBViewportChangeReason?
|
||
private var pendingPresentationRestoreLocation: RDEPUBLocation?
|
||
private var isWaitingForViewportTransitionCompletion = false
|
||
|
||
init(context: RDEPUBReaderContext) {
|
||
self.context = context
|
||
}
|
||
|
||
private var controller: RDEPUBReaderController? {
|
||
context.controller
|
||
}
|
||
|
||
/// 视图布局完成后检查视口是否发生变化,首次布局时触发初始加载
|
||
func viewDidLayoutSubviews() {
|
||
guard let controller else { return }
|
||
guard let viewportSignature = currentViewportSignature() else { return }
|
||
|
||
if !controller.didStartInitialLoad {
|
||
lastAppliedViewportSignature = viewportSignature
|
||
controller.startInitialLoadIfNeeded()
|
||
return
|
||
}
|
||
|
||
guard controller.publication != nil || controller.isExternalTextBook else {
|
||
lastAppliedViewportSignature = viewportSignature
|
||
return
|
||
}
|
||
|
||
guard !isWaitingForViewportTransitionCompletion else {
|
||
return
|
||
}
|
||
|
||
handleViewportChangeIfNeeded(reason: .viewLayout, viewportSignature: viewportSignature)
|
||
}
|
||
|
||
/// 屏幕旋转前捕获当前阅读位置,旋转完成后检测视口变化并处理
|
||
/// - Parameter coordinator: 转场协调器
|
||
func viewWillTransition(with coordinator: UIViewControllerTransitionCoordinator) {
|
||
guard let controller else { return }
|
||
guard controller.didStartInitialLoad else { return }
|
||
pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation()
|
||
isWaitingForViewportTransitionCompletion = true
|
||
|
||
coordinator.animate(alongsideTransition: nil) { [weak self] _ in
|
||
guard let self, let controller = self.controller else { return }
|
||
self.isWaitingForViewportTransitionCompletion = false
|
||
controller.view.layoutIfNeeded()
|
||
self.handleViewportChangeIfNeeded(reason: .orientationTransition)
|
||
}
|
||
}
|
||
|
||
/// 重置所有视口状态,用于重新加载书籍
|
||
func resetForReload() {
|
||
lastAppliedViewportSignature = currentViewportSignature()
|
||
pendingViewportChangeReason = nil
|
||
pendingPresentationRestoreLocation = nil
|
||
isWaitingForViewportTransitionCompletion = false
|
||
}
|
||
|
||
/// 消费并返回待恢复的阅读位置(一次性读取后清空)
|
||
func consumePendingPresentationRestoreLocation() -> RDEPUBLocation? {
|
||
defer { pendingPresentationRestoreLocation = nil }
|
||
return pendingPresentationRestoreLocation
|
||
}
|
||
|
||
/// 主动捕获当前阅读位置到待恢复队列,供后续视口变化后恢复使用
|
||
func capturePendingPresentationRestoreLocation() {
|
||
guard let controller else { return }
|
||
pendingPresentationRestoreLocation = controller.currentVisibleLocation() ?? controller.persistenceLocation()
|
||
}
|
||
|
||
/// 分页完成后处理挂起的视口变化(如有),避免分页过程中重复触发
|
||
func processPendingChangeAfterPagination() {
|
||
guard let pendingReason = pendingViewportChangeReason else { return }
|
||
pendingViewportChangeReason = nil
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.handleViewportChangeIfNeeded(reason: pendingReason)
|
||
}
|
||
}
|
||
|
||
/// 获取当前视口签名,包含容器尺寸和安全区域信息
|
||
func currentViewportSignature() -> RDEPUBViewportSignature? {
|
||
guard let controller else { return nil }
|
||
let containerSize = controller.readerView.bounds.size == .zero ? controller.view.bounds.size : controller.readerView.bounds.size
|
||
guard containerSize.width > 0, containerSize.height > 0 else { return nil }
|
||
let insets = controller.view.safeAreaInsets
|
||
return RDEPUBViewportSignature(
|
||
width: containerSize.width,
|
||
height: containerSize.height,
|
||
safeTop: insets.top,
|
||
safeLeft: insets.left,
|
||
safeBottom: insets.bottom,
|
||
safeRight: insets.right
|
||
)
|
||
}
|
||
|
||
/// 检测视口签名是否发生显著变化,若变化则触发重新分页或重建外部 TextBook
|
||
func handleViewportChangeIfNeeded(
|
||
reason: RDEPUBViewportChangeReason,
|
||
viewportSignature: RDEPUBViewportSignature? = nil
|
||
) {
|
||
guard let controller else { return }
|
||
guard controller.didStartInitialLoad,
|
||
let signature = viewportSignature ?? currentViewportSignature() else {
|
||
return
|
||
}
|
||
|
||
if controller.isRepaginating {
|
||
pendingViewportChangeReason = reason
|
||
return
|
||
}
|
||
|
||
if let lastAppliedViewportSignature,
|
||
!signature.differsSignificantly(from: lastAppliedViewportSignature) {
|
||
return
|
||
}
|
||
|
||
lastAppliedViewportSignature = signature
|
||
|
||
if controller.isExternalTextBook {
|
||
controller.rebuildExternalTextBook()
|
||
return
|
||
}
|
||
|
||
guard controller.publication != nil else { return }
|
||
controller.repaginatePreservingCurrentLocation()
|
||
}
|
||
}
|