- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
133 lines
5.8 KiB
Swift
133 lines
5.8 KiB
Swift
|
||
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)
|
||
}
|
||
}
|