// RDEPUBWebView.swift // EPUB WebView 主类 // 封装 WKWebView 的 EPUB 渲染能力,是 Resource View 层的核心组件。 // 统一管理固定版式和重排模式的加载流程,持有当前渲染状态(spineIndex、pageIndex、 // 主题色、高亮等),并通过委托模式通知上层页面渲染和交互事件。 // 具体逻辑分散在 +Configuration、+FixedLayout、+Reflowable、+JavaScriptBridge、+Search 扩展中。 import UIKit import WebKit /// RDEPUBWebView 委托协议,定义 WebView 向上层通知的事件 public protocol RDEPUBWebViewDelegate: AnyObject { /// 阅读位置更新事件 func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) /// 文本选择变化事件 func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) /// 选择菜单动作请求事件 func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) /// 内部链接点击事件 func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) /// 外部链接点击事件 func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL) /// JavaScript 错误事件 func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String) /// 页面渲染完成事件 func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView) } /// 委托协议的默认实现(选择菜单动作为可选) public extension RDEPUBWebViewDelegate { func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {} } /// 自定义 WKWebView 子类,支持拷贝/高亮/批注选择菜单 final class RDEPUBAnnotationWebView: WKWebView { /// 选择菜单动作回调 var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)? override var canBecomeFirstResponder: Bool { true } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { switch action { case #selector(rd_copy(_:)), #selector(rd_highlight(_:)), #selector(rd_annotate(_:)): return true default: return false } } @objc func rd_copy(_ sender: Any?) { onSelectionAction?(.copy) } @objc func rd_highlight(_ sender: Any?) { onSelectionAction?(.highlight) } @objc func rd_annotate(_ sender: Any?) { onSelectionAction?(.annotate) } } /// EPUB WebView 主类,封装 WKWebView 的 EPUB 渲染能力 /// Resource View 层核心组件,统一管理固定版式和重排模式的渲染 public final class RDEPUBWebView: UIView { /// 委托对象,接收渲染和交互事件 public weak var delegate: RDEPUBWebViewDelegate? /// 渲染完成回调(与 delegate 并行使用) public var onRendered: (() -> Void)? /// 当前关联的出版物 var publication: RDEPUBPublication? /// 当前渲染请求 var currentRenderRequest: RDEPUBRenderRequest? /// 内部 WKWebView 实例 var webView: WKWebView? /// URL Scheme 处理器 var schemeHandler: RDEPUBResourceURLSchemeHandler? /// 已配置的出版物标识(用于判断是否需要重建 WebView) var configuredPublicationKey: String? /// 当前加载签名(用于去重相同请求) var currentLoadSignature: String? /// 当前 spine 索引 var currentSpineIndex = 0 /// 当前资源 href var currentHref = "" /// 当前页码(从 0 开始) var currentPageIndex = 0 /// 当前章节总页数 var currentTotalPagesInChapter = 1 /// 当前视口尺寸 var viewportSize: CGSize = .zero /// 当前内容内边距 var currentPadding: UIEdgeInsets = .zero /// 当前字号 var currentFontSize: CGFloat = 16 /// 当前行高倍数 var currentLineHeightMultiple: CGFloat = 1.5 /// 当前主题背景色 var currentThemeBackgroundColor: String? /// 当前主题文字色 var currentThemeTextColor: String? /// 目标跳转位置 var targetLocation: RDEPUBLocation? /// 待应用的高亮列表 var pendingHighlights: [RDEPUBHighlight] = [] /// 当前固定版式 spread(固定版式时非空) var fixedSpread: EPUBFixedSpread? /// 是否为固定版式模式 var isFixedLayout = false /// 是否有待处理的进度汇报请求 var pendingProgressionRequest = false /// 固定版式就绪回退定时器 var fixedLayoutReadyWorkItem: DispatchWorkItem? /// 当前请求是否已完成渲染 var didRenderCurrentRequest = false /// 调试日志作用域 let debugScope = "ReaderWebView" public override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { teardownWebView() } public override func layoutSubviews() { super.layoutSubviews() webView?.frame = bounds } /// 重置所有状态并销毁 WebView public func reset() { delegate = nil onRendered = nil publication = nil currentRenderRequest = nil currentSpineIndex = 0 currentHref = "" currentPageIndex = 0 currentTotalPagesInChapter = 1 viewportSize = .zero currentPadding = .zero currentFontSize = 16 currentLineHeightMultiple = 1.5 currentThemeBackgroundColor = nil currentThemeTextColor = nil targetLocation = nil pendingHighlights = [] fixedSpread = nil isFixedLayout = false pendingProgressionRequest = false didRenderCurrentRequest = false teardownWebView() } /// 加载渲染请求:配置 WebView、生成签名、分发到固定版式或重排处理 public func load(publication: RDEPUBPublication, request: RDEPUBRenderRequest) { configureWebViewIfNeeded(publication: publication) guard let webView else { return } self.publication = publication self.currentRenderRequest = request self.currentSpineIndex = request.primarySpineIndex self.currentHref = request.primaryHref self.didRenderCurrentRequest = false cancelFixedLayoutReadyFallback() let publicationKey = publication.parser.opfURL?.path ?? publication.parser.extractionRootURL?.path ?? "" let loadSignature = pageLoadSignature(publicationKey: publicationKey, request: request) RDEPUBWebViewDebug.log( debugScope, message: "load request=\(request.isFixedLayout ? "fixed" : "reflowable") signature=\(loadSignature) webView=\(RDEPUBWebViewDebug.webViewID(webView))" ) switch request { case .reflowable(let reflowableRequest): handleReflowableLoad(publication: publication, request: reflowableRequest, loadSignature: loadSignature, in: webView) case .fixed(let fixedRequest): handleFixedLayoutLoad(publication: publication, request: fixedRequest, loadSignature: loadSignature, in: webView) } } /// 生成页面加载签名 func pageLoadSignature( publicationKey: String, request: RDEPUBRenderRequest ) -> String { switch request { case .reflowable(let request): return reflowableLoadSignature(publicationKey: publicationKey, request: request) case .fixed(let request): return fixedLayoutLoadSignature(publicationKey: publicationKey, request: request) } } /// 标记渲染完成:通知委托和回调,延迟 100ms 确保布局稳定 func rendered() { guard !didRenderCurrentRequest else { return } didRenderCurrentRequest = true cancelFixedLayoutReadyFallback() RDEPUBWebViewDebug.log(debugScope, message: "rendered spine=\(currentSpineIndex) href=\(currentHref) fixed=\(isFixedLayout)") DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in guard let self else { return } self.delegate?.epubWebViewDidFinishRendering(self) self.onRendered?() } } }