ReadViewSDK/Sources/RDEpubReaderView/EPUBUI/TextPage/RDEPUBChapterDisplayContentCache.swift
shenlei d7fcda345d 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
2026-07-10 19:44:53 +09:00

115 lines
3.8 KiB
Swift

import UIKit
#if canImport(DTCoreText)
import DTCoreText
#endif
/// Chapter-level shared display content and layouter
/// (LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md P1-1 / P1-3).
///
/// One entry holds the chapter-length display string (theme text color and
/// dark-image adjustment injected once over the full chapter at build time)
/// plus the DTCoreTextLayouter wrapping its framesetter. Page views reference
/// the shared string and request per-page layout frames; they must never
/// mutate the shared string the framesetter and any live layout frame hold
/// it. Page-level decoration (highlights, search, selection) is drawn by the
/// overlay views instead.
///
/// Main-thread only.
final class RDEPUBChapterDisplayContentCache {
#if canImport(DTCoreText)
struct Entry {
let content: NSAttributedString
let layouter: DTCoreTextLayouter?
fileprivate let signature: Signature
}
fileprivate struct Signature: Equatable {
let chapterContentID: ObjectIdentifier
let contentLength: Int
let textColor: UIColor
let backgroundColor: UIColor
let darkImageAdjustmentEnabled: Bool
let darkImageBlendRatio: CGFloat
init(page: RDEPUBTextPage, configuration: RDEPUBReaderConfiguration) {
chapterContentID = ObjectIdentifier(page.chapterContent)
contentLength = page.chapterContent.length
textColor = configuration.theme.contentTextColor
backgroundColor = configuration.theme.contentBackgroundColor
darkImageAdjustmentEnabled = configuration.darkImageAdjustmentEnabled
darkImageBlendRatio = configuration.darkImageBlendRatio
}
}
/// Current chapter plus one adjacent chapter during page-curl/scroll
/// transitions across a chapter boundary.
private static let capacity = 2
private var entries: [String: Entry] = [:]
private var accessOrder: [String] = []
func entry(for page: RDEPUBTextPage, configuration: RDEPUBReaderConfiguration) -> Entry {
dispatchPrecondition(condition: .onQueue(.main))
let signature = Signature(page: page, configuration: configuration)
if let cached = entries[page.href], cached.signature == signature {
touch(page.href)
return cached
}
let entry = Self.buildEntry(page: page, configuration: configuration, signature: signature)
entries[page.href] = entry
touch(page.href)
evictIfNeeded()
RDEPUBMemoryProbe.log("displayContentBuilt href=\(page.href) length=\(entry.content.length)")
return entry
}
func removeAll() {
entries.removeAll()
accessOrder.removeAll()
}
private func touch(_ href: String) {
accessOrder.removeAll { $0 == href }
accessOrder.append(href)
}
private func evictIfNeeded() {
while accessOrder.count > Self.capacity {
let evicted = accessOrder.removeFirst()
entries.removeValue(forKey: evicted)
}
}
private static func buildEntry(
page: RDEPUBTextPage,
configuration: RDEPUBReaderConfiguration,
signature: Signature
) -> Entry {
let content = NSMutableAttributedString(attributedString: page.chapterContent)
let fullRange = NSRange(location: 0, length: content.length)
_ = RDEPUBDarkImageAdjuster.adjustIfNeeded(
content,
in: fullRange,
configuration: configuration
)
content.addAttribute(
.foregroundColor,
value: configuration.theme.contentTextColor,
range: fullRange
)
let layouter = DTCoreTextLayouter(attributedString: content)
layouter?.shouldCacheLayoutFrames = false
return Entry(content: content, layouter: layouter, signature: signature)
}
#else
func removeAll() {}
#endif
}