// RDEPUBWebView+Reflowable.swift // RDEPUBWebView 的重排模式渲染扩展 // 处理重排(Reflowable)模式的加载流程:构建渲染请求、 // 生成加载签名(去重)、通过 JS 桥接应用分页样式和高亮、 // 处理重复请求跳过和已有文档复用等优化逻辑。 import UIKit import WebKit extension RDEPUBWebView { /// 便捷方法:加载重排模式的指定页面 public func loadPage( parser: RDEPUBParser, spineIndex: Int, pageIndex: Int, totalPagesInChapter: Int, viewportSize: CGSize, padding: UIEdgeInsets, fontSize: CGFloat, lineHeightMultiple: CGFloat, themeBackgroundColor: String?, themeTextColor: String?, targetLocation: RDEPUBLocation?, highlights: [RDEPUBHighlight] ) { let href = parser.spine.indices.contains(spineIndex) ? parser.spine[spineIndex].href : "" let request = RDEPUBRenderRequest.reflowable( RDEPUBReflowableRenderRequest( spineIndex: spineIndex, href: href, pageIndex: pageIndex, totalPagesInChapter: totalPagesInChapter, presentation: RDEPUBPresentationStyle( viewportSize: viewportSize, contentInsets: padding, fontSize: fontSize, lineHeightMultiple: lineHeightMultiple, themeBackgroundColor: themeBackgroundColor, themeTextColor: themeTextColor ), targetLocation: targetLocation, highlights: highlights, searchPresentation: nil ) ) load(publication: parser.makePublication(), request: request) } /// 处理重排模式加载:设置状态、去重检查、加载资源或直接应用样式 func handleReflowableLoad( publication: RDEPUBPublication, request: RDEPUBReflowableRenderRequest, loadSignature: String, in webView: WKWebView ) { guard publication.spine.indices.contains(request.spineIndex), let requestURL = publication.resourceResolver.resourceURL(forRelativePath: request.href) else { return } currentPageIndex = request.pageIndex currentTotalPagesInChapter = max(1, request.totalPagesInChapter) viewportSize = request.presentation.viewportSize currentPadding = request.presentation.contentInsets currentFontSize = request.presentation.fontSize currentLineHeightMultiple = request.presentation.lineHeightMultiple currentThemeBackgroundColor = request.presentation.themeBackgroundColor currentThemeTextColor = request.presentation.themeTextColor targetLocation = request.targetLocation pendingHighlights = request.highlights fixedSpread = nil isFixedLayout = false pendingProgressionRequest = true if currentLoadSignature == loadSignature { if didRenderCurrentRequest || webView.isLoading { let reason = didRenderCurrentRequest ? "already-rendered" : "still-loading" RDEPUBWebViewDebug.log(debugScope, message: "skip duplicate request href=\(request.href) page=\(request.pageIndex) reason=\(reason)") return } if webView.url == requestURL { RDEPUBWebViewDebug.log(debugScope, message: "reuse existing document href=\(request.href) page=\(request.pageIndex)") applyPresentation() return } } currentLoadSignature = loadSignature RDEPUBWebViewDebug.logNavigationEvent(debugScope, webView: webView, event: "load-request", url: requestURL) webView.load(URLRequest(url: requestURL)) } /// 生成重排模式加载签名(包含所有渲染参数的哈希,用于去重) func reflowableLoadSignature(publicationKey: String, request: RDEPUBReflowableRenderRequest) -> String { let targetSignature = [ request.targetLocation?.href ?? "", String(request.targetLocation?.progression ?? 0), String(request.targetLocation?.lastProgression ?? 0), request.targetLocation?.fragment ?? "", request.targetHighlightRangeInfo ?? "" ].joined(separator: "|") let highlightSignature = request.highlights .map { [$0.id, $0.rangeInfo ?? "", $0.color, $0.style.rawValue].joined(separator: "|") } .joined(separator: ",") let searchSignature = [ request.searchPresentation?.keyword ?? "", request.searchPresentation?.resources.map { [ $0.href, String($0.matchCount), String($0.activeLocalMatchIndex ?? -1) ].joined(separator: "|") }.joined(separator: ",") ?? "" ].joined(separator: "#") let presentation = request.presentation return [ publicationKey, "reflowable", String(request.spineIndex), request.href, String(request.pageIndex), String(request.totalPagesInChapter), String(format: "%.2f", presentation.viewportSize.width), String(format: "%.2f", presentation.viewportSize.height), String(format: "%.2f", presentation.contentInsets.top), String(format: "%.2f", presentation.contentInsets.left), String(format: "%.2f", presentation.contentInsets.bottom), String(format: "%.2f", presentation.contentInsets.right), String(format: "%.2f", presentation.fontSize), String(format: "%.2f", presentation.lineHeightMultiple), String(presentation.numberOfColumns), String(format: "%.2f", presentation.columnGap), presentation.themeBackgroundColor ?? "", presentation.themeTextColor ?? "", targetSignature, highlightSignature, searchSignature ].joined(separator: "#") } /// 应用分页展示样式:通过 JS 桥接注入 CSS、设置分页参数和高亮,然后触发渲染完成 func applyPresentation() { guard let webView, let currentRenderRequest else { return } guard case .reflowable(let request) = currentRenderRequest else { return } let script = RDEPUBJavaScriptBridge.applyPresentationScript(for: request) RDEPUBWebViewDebug.logJavaScript( debugScope, webView: webView, action: "applyPresentation", details: "spine=\(request.spineIndex) page=\(request.pageIndex) total=\(request.totalPagesInChapter)" ) webView.evaluateJavaScript(script) { [weak self] _, error in guard let self else { return } if let error { RDEPUBWebViewDebug.logJavaScript(self.debugScope, webView: webView, action: "applyPresentationFailed", details: error.localizedDescription) self.delegate?.epubWebView(self, didLogJavaScriptError: error.localizedDescription) return } self.applySearchDecorationsIfNeeded { self.rendered() } } } }