refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs

- 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
This commit is contained in:
shenlei
2026-07-10 19:44:53 +09:00
parent d5a7755702
commit d7fcda345d
460 changed files with 38358 additions and 2300 deletions
@@ -0,0 +1,243 @@
import UIKit
import WebKit
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)
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String)
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView)
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?)
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?)
}
public extension RDEPUBWebViewDelegate {
func epubWebView(_ webView: RDEPUBWebView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction) {}
func epubWebView(_ webView: RDEPUBWebView, didTapImageWithSource src: String, sourceRect: CGRect?) {}
func epubWebView(_ webView: RDEPUBWebView, didTapFootnoteWithAltText altText: String, sourceRect: CGRect?) {}
}
final class RDEPUBAnnotationWebView: WKWebView {
var onSelectionAction: ((RDEPUBAnnotationMenuAction) -> Void)?
// M-15: Backing store for UIEditMenuInteraction (iOS 16+).
// Stored as Any? to avoid @available on stored property restriction.
var _editMenuInteraction: Any?
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)
}
}
// MARK: - UIEditMenuInteractionDelegate (iOS 16+)
@available(iOS 16.0, *)
extension RDEPUBAnnotationWebView: UIEditMenuInteractionDelegate {
func editMenuInteraction(
_ interaction: UIEditMenuInteraction,
menuFor configuration: UIEditMenuConfiguration
) -> UIMenu {
UIMenu(children: [
UICommand(title: "拷贝", action: #selector(rd_copy(_:))),
UICommand(title: "高亮", action: #selector(rd_highlight(_:))),
UICommand(title: "批注", action: #selector(rd_annotate(_:)))
])
}
func editMenuInteraction(
_ interaction: UIEditMenuInteraction,
targetRectFor configuration: UIEditMenuConfiguration
) -> CGRect {
bounds
}
}
public final class RDEPUBWebView: UIView {
public weak var delegate: RDEPUBWebViewDelegate?
public var onRendered: (() -> Void)?
var onDecorationsResolved: (([RDEPUBTextOverlayDecoration]) -> Void)?
var publication: RDEPUBPublication?
var currentRenderRequest: RDEPUBRenderRequest?
var webView: WKWebView?
var schemeHandler: RDEPUBResourceURLSchemeHandler?
var configuredPublicationKey: String?
var currentLoadSignature: String?
var currentSpineIndex = 0
var currentHref = ""
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] = []
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")
}
// M-08: script message handler RDEPUBWeakScriptMessageHandler
// configureWebViewIfNeededwebView userContentController self
// deinit
// reset() WKWebView dealloc
deinit {
teardownWebView()
}
public override func layoutSubviews() {
super.layoutSubviews()
webView?.frame = bounds
}
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()
}
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)
}
}
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?()
}
}
}