import UIKit // MARK: - Web 内容视图代理 /// Web 内容视图的代理协议 /// 将 WebView 中的事件(位置更新、文本选择、链接点击等)转发给控制器 protocol RDEPUBWebContentViewDelegate: AnyObject { /// WebView 中阅读位置更新时调用 func epubWebContentView(_ contentView: RDEPUBWebContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) /// WebView 中文本选择变化时调用 func epubWebContentView(_ contentView: RDEPUBWebContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) /// 用户从选择菜单中触发操作 func epubWebContentView(_ contentView: RDEPUBWebContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) /// 用户点击内部链接(章节跳转) func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) /// 用户点击外部链接 func epubWebContentView(_ contentView: RDEPUBWebContentView, didActivateExternalLink url: URL) /// WebView 中 JavaScript 执行出错 func epubWebContentView(_ contentView: RDEPUBWebContentView, didLogJavaScriptError message: String) } // MARK: - Web 内容视图 /// EPUB 固定布局和 Web 渲染路径的内容视图 /// 包装 RDEPUBWebView,提供页码标签和加载/释放资源的控制 /// 适用于固定布局 EPUB 和不支持 Native Text 的场景 final class RDEPUBWebContentView: UIView { weak var delegate: RDEPUBWebContentViewDelegate? private let epubWebView = RDEPUBWebView() private let decorationOverlayView = RDEPUBWebDecorationOverlayView() private let pageNumberLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 13) return label }() override init(frame: CGRect) { super.init(frame: frame) clipsToBounds = true layer.masksToBounds = true addSubview(epubWebView) addSubview(decorationOverlayView) addSubview(pageNumberLabel) epubWebView.delegate = self epubWebView.onDecorationsResolved = { [weak self] decorations in self?.decorationOverlayView.applyDecorations(decorations) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() epubWebView.frame = bounds decorationOverlayView.frame = bounds let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20)) pageNumberLabel.frame = CGRect( x: bounds.width - labelSize.width - 24, y: bounds.height - labelSize.height - 20, width: labelSize.width, height: labelSize.height ) } func configure( publication: RDEPUBPublication, request: RDEPUBRenderRequest, pageNumber: Int, totalPages: Int, theme: RDEPUBReaderTheme ) { backgroundColor = theme.contentBackgroundColor pageNumberLabel.textColor = theme.contentTextColor pageNumberLabel.text = "\(pageNumber) / \(totalPages)" decorationOverlayView.applyDecorations([]) epubWebView.load(publication: publication, request: request) setNeedsLayout() } func releaseResources() { decorationOverlayView.applyDecorations([]) epubWebView.reset() epubWebView.delegate = self epubWebView.onDecorationsResolved = { [weak self] decorations in self?.decorationOverlayView.applyDecorations(decorations) } } } extension RDEPUBWebContentView: RDEPUBWebViewDelegate { func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) { delegate?.epubWebContentView(self, didUpdateLocation: location, spineIndex: spineIndex) } func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) { delegate?.epubWebContentView(self, didChangeSelection: selection, spineIndex: spineIndex) } func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) { delegate?.epubWebContentView(self, didRequestSelectionAction: action) } func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) { delegate?.epubWebContentView(self, didActivateInternalLink: location, fromSpineIndex: fromSpineIndex) } func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL) { delegate?.epubWebContentView(self, didActivateExternalLink: url) } func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String) { delegate?.epubWebContentView(self, didLogJavaScriptError: message) } func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView) {} }