223 lines
8.5 KiB
Swift
223 lines
8.5 KiB
Swift
// 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 onDecorationsResolved: (([RDEPUBTextOverlayDecoration]) -> 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
|
||
onDecorationsResolved = 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?()
|
||
}
|
||
}
|
||
}
|