import UIKit import WebKit extension RDEPUBWebView { func configureWebViewIfNeeded(publication: RDEPUBPublication) { let parser = publication.parser let publicationKey = parser.opfURL?.path ?? parser.extractionRootURL?.path ?? UUID().uuidString if publicationKey == configuredPublicationKey, webView != nil { return } teardownWebView() let userContentController = WKUserContentController() let messageHandlerProxy = RDEPUBWeakScriptMessageHandler(target: self) RDEPUBJavaScriptBridge.messageNames.forEach { userContentController.add(messageHandlerProxy, name: $0) } userContentController.addUserScript( WKUserScript( source: RDEPUBJavaScriptBridge.userScript, injectionTime: .atDocumentEnd, forMainFrameOnly: true ) ) let configuration = WKWebViewConfiguration() configuration.websiteDataStore = .nonPersistent() configuration.userContentController = userContentController let schemeHandler = RDEPUBResourceURLSchemeHandler(parser: parser) configuration.setURLSchemeHandler(schemeHandler, forURLScheme: RDEPUBResourceURLSchemeHandler.scheme) let webView = RDEPUBAnnotationWebView(frame: bounds, configuration: configuration) webView.onSelectionAction = { [weak self] action in guard let self else { return } self.handleSelectionMenuAction(action) } // M-15: Use UIEditMenuInteraction on iOS 16+ to replace deprecated UIMenuController if #available(iOS 16.0, *) { let interaction = UIEditMenuInteraction(delegate: webView) webView.addInteraction(interaction) webView._editMenuInteraction = interaction } else { UIMenuController.shared.menuItems = [ UIMenuItem(title: "拷贝", action: #selector(RDEPUBAnnotationWebView.rd_copy(_:))), UIMenuItem(title: "高亮", action: #selector(RDEPUBAnnotationWebView.rd_highlight(_:))), UIMenuItem(title: "批注", action: #selector(RDEPUBAnnotationWebView.rd_annotate(_:))) ] } webView.navigationDelegate = self // 安全区自适应会把 Web 布局视口缩小(横屏左右各 59pt、底部 21pt), // 固定布局页面随之按缩小后的视口适配,产生大边距/中缝。 // 阅读区域的安全区避让统一由 fixedContentInset / 排版 CSS 负责。 webView.scrollView.contentInsetAdjustmentBehavior = .never webView.scrollView.isScrollEnabled = false webView.scrollView.showsHorizontalScrollIndicator = false webView.scrollView.showsVerticalScrollIndicator = false webView.scrollView.bounces = false webView.scrollView.alwaysBounceHorizontal = false webView.scrollView.alwaysBounceVertical = false webView.scrollView.clipsToBounds = true webView.scrollView.panGestureRecognizer.isEnabled = false webView.scrollView.pinchGestureRecognizer?.isEnabled = false webView.isOpaque = false webView.backgroundColor = .clear webView.clipsToBounds = true if #available(iOS 16.4, *) { webView.isInspectable = RDEPUBWebViewDebug.isInspectableEnabled } addSubview(webView) webView.frame = bounds webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.schemeHandler = schemeHandler self.webView = webView self.configuredPublicationKey = publicationKey RDEPUBWebViewDebug.log(debugScope, message: "configured webView=\(RDEPUBWebViewDebug.webViewID(webView)) publicationKey=\(publicationKey)") } func handleSelectionMenuAction(_ action: RDEPUBAnnotationMenuAction) { delegate?.epubWebView(self, didRequestSelectionAction: action) clearWebSelection() } func clearWebSelection() { webView?.evaluateJavaScript("window.getSelection && window.getSelection().removeAllRanges();") } func teardownWebView() { cancelFixedLayoutReadyFallback() if let webView { RDEPUBWebViewDebug.log(debugScope, message: "teardown webView=\(RDEPUBWebViewDebug.webViewID(webView))") } // M-15: Clean up UIEditMenuInteraction on iOS 16+ if #available(iOS 16.0, *), let interaction = (webView as? RDEPUBAnnotationWebView)?._editMenuInteraction as? UIEditMenuInteraction { webView?.removeInteraction(interaction) } webView?.navigationDelegate = nil if let userContentController = webView?.configuration.userContentController { RDEPUBJavaScriptBridge.messageNames.forEach { userContentController.removeScriptMessageHandler(forName: $0) } } (webView as? RDEPUBAnnotationWebView)?.onSelectionAction = nil webView?.removeFromSuperview() webView = nil schemeHandler = nil configuredPublicationKey = nil currentLoadSignature = nil } } /// M-08: WKUserContentController 强持有注册的 script message handler。 /// 直接注册 RDEPUBWebView 自身会形成保留环 /// (webView → userContentController → RDEPUBWebView → webView), /// 被丢弃的页面 WebView 将永远无法释放。本代理弱持有真实 handler, /// 保留环从一开始就不成立,RDEPUBWebView 可随最后一个持有者正常 deinit。 private final class RDEPUBWeakScriptMessageHandler: NSObject, WKScriptMessageHandler { private weak var target: WKScriptMessageHandler? init(target: WKScriptMessageHandler) { self.target = target } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { target?.userContentController(userContentController, didReceive: message) } }