- 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
115 lines
3.8 KiB
Swift
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
|
|
}
|