长章节内存优化 P1:章节级共享显示内容与 layouter,高亮转 overlay
- 新增 RDEPUBChapterDisplayContentCache(LRU 2,主线程限定,控制器
持有):整章显示串构建时一次性注入全章主题色与暗黑图替换,配套
共享 DTCoreTextLayouter;签名 = 章节内容对象标识 + 主题双色 +
暗黑图配置,设置/主题变化自动失效重建
- RDEPUBTextContentView.configure 改为引用共享 entry,删除每次翻页
的整章可变拷贝与按页属性写入
- 高亮/下划线改走 overlay decoration(高亮背景层、下划线前景层),
移除 applyHighlightsToContent 与 render view 的属性绘制路径,
高亮增删不再触发整章重建
- 内存警告时清空 display cache;shouldAvoidReaderPageCaching 保持
保守(P1-4 待真机数据)
验证:编译通过;高亮/批注/选区/翻页/设置 13 项 UI 回归全过。
SearchTests 10 项失败经二分确认为先存回归(5a41066 与 e4e629a 均
复现,最后已知通过为 2026-06-08),与本次改动无关,待单独排查。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e4e629a06c
commit
c4be426299
@ -61,28 +61,26 @@
|
||||
|
||||
**任务**:
|
||||
|
||||
- [ ] 在章节运行时侧(建议挂在 `RDEPUBRuntimeChapter` 或新建 `RDEPUBChapterDisplayCache`)新增:
|
||||
- `chapterDisplayContent`(`typesetAttributedString` 的一次性拷贝)
|
||||
- `displaySignature`(组成:主题、暗黑图配置、字体设置、本章高亮集合版本)
|
||||
- [ ] 构建共享内容时一次性注入:全章范围主题色;全章范围高亮/下划线属性(若 P1-2 决定转 overlay 则此项取消)
|
||||
- [ ] 确定暗黑图策略(二选一):
|
||||
- a. 构建时全章一次性替换(依赖 `RDEPUBDarkImageAdjuster` 的 NSCache 兜底解码成本)
|
||||
- b. 移到渲染时按可见附件替换,不触碰共享字符串
|
||||
- [ ] `RDEPUBTextContentView.configure` 改为接收"共享内容引用 + `page.contentRange`",删除 443 行的整章拷贝
|
||||
- [ ] 高亮增删、搜索激活、主题/设置变化 → 比对 `displaySignature`,不一致则重建共享内容(禁止原地修改)
|
||||
- [ ] 章节淘汰(`RDEPUBChapterRuntimeStore.evict` / `handleMemoryWarning` / `invalidateAllForSettingsChange`)同步释放 display cache
|
||||
- [x] 已新建 `RDEPUBChapterDisplayContentCache`(LRU 容量 2,主线程限定,`RDEPUBReaderController` 持有):`content` + `layouter` + `signature`
|
||||
- [x] 构建共享内容时一次性注入:全章范围主题色;高亮/下划线因 P1-2 转 overlay,不进共享内容也不进签名
|
||||
- [x] 暗黑图策略选 a:构建时全章一次性替换,复用 `RDEPUBDarkImageAdjuster` 的 NSCache
|
||||
- [x] `RDEPUBTextContentView.configure` 已改为接收 `displayCache` 参数并引用共享 entry,整章拷贝已删除
|
||||
- [x] 签名 = 章节内容对象标识 + 长度 + 主题双色 + 暗黑图配置;设置/主题变化经签名自动失效重建(高亮变化不触发重建,仅重建 overlay 装饰)
|
||||
- [x] 控制器在 `didReceiveMemoryWarning` 清空 display cache;设置变更重建 chapterContent 实例后由签名兜底换代
|
||||
|
||||
**验收**:连续翻页不再出现整章拷贝(打点确认);翻页延迟下降;高亮显示、高亮点击菜单、underline、搜索 currentMatch、附件点击、选区拖拽全部回归通过。
|
||||
|
||||
> 2026-07-08 回归记录:高亮/批注/选区/翻页/设置 13 项 UI 用例全过;SearchTests 10 项失败,但已在会话前提交 5a41066 与 P0 提交 e4e629a 上复现同样失败(搜索 0 命中),确认为先于本优化存在的独立回归(最后一次已知通过是 2026-06-08 全量跑),需单独排查,与 P1 改动无关。
|
||||
|
||||
### P1-2 高亮/下划线剥离为 overlay(与 P1-1 绑定)
|
||||
|
||||
**现状**:搜索高亮已走 overlay(`RDEPUBTextContentView.swift:759` 处 `buildDecorations` 传 `highlights: []`,注释明确为避免双画);普通高亮/下划线仍靠写 `kRDEPUBHighlightAttributeName` / `kRDEPUBUnderlineAttributeName` 属性由 render view 绘制。
|
||||
|
||||
**任务**:
|
||||
|
||||
- [ ] `buildDecorations` 改为传入真实 highlights,高亮/下划线由 decoration 层绘制
|
||||
- [ ] 移除 `applyHighlightsToContent`(`RDEPUBTextContentView.swift:617`)及 render view 对两个自定义属性的绘制路径,确认无双画
|
||||
- [ ] 高亮命中(`highlight(at:)`)与菜单锚点继续走 `interactionController.selectionRects`,坐标保持 chapter-absolute
|
||||
- [x] `buildDecorations` 已传入真实 highlights,高亮进背景层(文字下方)、下划线进前景层
|
||||
- [x] `applyHighlightsToContent` 与 render view 的 `drawHighlights`/`computeHighlightRects`/`attributedDisplayContent` 已整体移除,无双画
|
||||
- [x] 高亮命中与菜单锚点继续走 `interactionController.selectionRects`,坐标保持 chapter-absolute(未改动)
|
||||
|
||||
**验收**:高亮/下划线视觉与改前一致(含跨页高亮的页内裁剪);高亮点击弹菜单正常;与搜索高亮叠加时无双画。
|
||||
|
||||
@ -92,10 +90,10 @@
|
||||
|
||||
**任务**:
|
||||
|
||||
- [ ] `DTCoreTextLayouter` 随 `chapterDisplayContent` 一起放入章节级 display cache,与 signature 同生命周期
|
||||
- [ ] 页面按 `bounds` + `page.contentRange` 取 `layoutFrame`;`RDEPUBTextPageVerticalJustifier.justify` 会修改 layoutFrame,因此 **保持 `shouldCacheLayoutFrames = false`**,layoutFrame 永远按页新建、不共享
|
||||
- [ ] bounds 变化(横竖屏/双页)纳入失效条件;确认旧 layoutFrame 随页面视图释放
|
||||
- [ ] `RDEPUBTextPageBoundaryValidator` 校验路径不变
|
||||
- [x] `DTCoreTextLayouter` 已随共享内容放入 display cache,与 signature 同生命周期
|
||||
- [x] 页面按 `bounds` + `page.contentRange` 取 `layoutFrame`;`shouldCacheLayoutFrames = false` 保持,layoutFrame 按页新建(VerticalJustifier 的修改不会污染共享对象)
|
||||
- [x] framesetter 只依赖字符串、与 bounds 无关,bounds 变化无需失效共享对象;layoutFrame 随页面视图释放
|
||||
- [x] `RDEPUBTextPageBoundaryValidator` 校验路径不变
|
||||
|
||||
**验收**:翻页时不再重建 framesetter(打点确认);横竖屏切换后布局正确;`--demo-pagination-validate` 不回升。
|
||||
|
||||
|
||||
2416
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
2416
ReadViewDemo/Pods/Pods.xcodeproj/project.pbxproj
generated
File diff suppressed because it is too large
Load Diff
@ -64,7 +64,8 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderPageProvider, RDRe
|
||||
chapterCFIMap: resolvedPage.chapter.chapterOffsetMap.cfiMap,
|
||||
chapterFragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets,
|
||||
highlights: textHighlights(for: resolvedPage.page),
|
||||
searchState: searchState(for: resolvedPage.page)
|
||||
searchState: searchState(for: resolvedPage.page),
|
||||
displayCache: textDisplayCache
|
||||
)
|
||||
return contentView
|
||||
}
|
||||
@ -108,7 +109,8 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderPageProvider, RDRe
|
||||
chapterCFIMap: textBook.chapterData(for: page.href)?.chapter.cfiMap,
|
||||
chapterFragmentOffsets: textBook.chapterData(for: page.href)?.chapter.fragmentOffsets ?? [:],
|
||||
highlights: textHighlights(for: page),
|
||||
searchState: searchState(for: page)
|
||||
searchState: searchState(for: page),
|
||||
displayCache: textDisplayCache
|
||||
)
|
||||
return contentView
|
||||
}
|
||||
|
||||
@ -96,6 +96,10 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
|
||||
let paginationHostView = UIView()
|
||||
|
||||
/// Chapter-level shared display content/layouter for text pages
|
||||
/// (LONG_CHAPTER_MEMORY_OPTIMIZATION_PLAN.md P1-1/P1-3).
|
||||
let textDisplayCache = RDEPUBChapterDisplayContentCache()
|
||||
|
||||
lazy var readerContext = RDEPUBReaderContext(controller: self)
|
||||
|
||||
var parser: RDEPUBParser? {
|
||||
@ -292,6 +296,7 @@ public final class RDEPUBReaderController: UIViewController {
|
||||
public override func didReceiveMemoryWarning() {
|
||||
super.didReceiveMemoryWarning()
|
||||
runtime.handleMemoryWarning()
|
||||
textDisplayCache.removeAll()
|
||||
}
|
||||
|
||||
public override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
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
|
||||
}
|
||||
@ -87,12 +87,16 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
return view
|
||||
}()
|
||||
|
||||
/// Chapter-level shared display string owned by the display cache.
|
||||
/// Read-only from the page's perspective: theme color and dark-image
|
||||
/// adjustment are baked in at build time; page decoration goes through
|
||||
/// the overlay views.
|
||||
private var coreTextDisplayContent: NSAttributedString?
|
||||
|
||||
private var coreTextDisplayRange: NSRange?
|
||||
|
||||
/// Framesetter over the full chapter copy, rebuilt when the display
|
||||
/// content changes; layout frames are recomputed per bounds change.
|
||||
/// Shared chapter-level layouter (owned by the display cache); layout
|
||||
/// frames are recomputed per page range and bounds change.
|
||||
private var coreTextLayouter: DTCoreTextLayouter?
|
||||
#endif
|
||||
|
||||
@ -397,7 +401,8 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
chapterCFIMap: RDEPUBCFIMap? = nil,
|
||||
chapterFragmentOffsets: [String: Int] = [:],
|
||||
highlights: [RDEPUBHighlight] = [],
|
||||
searchState: RDEPUBSearchState? = nil
|
||||
searchState: RDEPUBSearchState? = nil,
|
||||
displayCache: RDEPUBChapterDisplayContentCache? = nil
|
||||
) {
|
||||
loadingSpinner.stopAnimating()
|
||||
currentPage = page
|
||||
@ -437,37 +442,26 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
coverImageView.image = nil
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
// In-context display: keep the full chapter string and lay out only the
|
||||
// page's range. CTTypesetter line breaks are context-sensitive, so
|
||||
// In-context display: lay out the full chapter string and render only
|
||||
// the page's range. CTTypesetter line breaks are context-sensitive, so
|
||||
// re-wrapping a page substring can break ±1 character away from the
|
||||
// paginator's line ends (lone characters spilling onto the last line);
|
||||
// laying out the same chapter string the paginator used cannot.
|
||||
// String indices in the layout frame are therefore chapter-absolute.
|
||||
let displayContent = NSMutableAttributedString(attributedString: page.chapterContent)
|
||||
// The chapter string and layouter are shared per chapter via the
|
||||
// display cache; this view must not mutate the shared string.
|
||||
let display = (displayCache ?? RDEPUBChapterDisplayContentCache())
|
||||
.entry(for: page, configuration: configuration)
|
||||
let pageRange = NSIntersectionRange(
|
||||
page.contentRange,
|
||||
NSRange(location: 0, length: displayContent.length)
|
||||
NSRange(location: 0, length: display.content.length)
|
||||
)
|
||||
_ = RDEPUBDarkImageAdjuster.adjustIfNeeded(
|
||||
displayContent,
|
||||
in: pageRange,
|
||||
configuration: configuration
|
||||
)
|
||||
displayContent.addAttribute(
|
||||
.foregroundColor,
|
||||
value: configuration.theme.contentTextColor,
|
||||
range: pageRange
|
||||
)
|
||||
|
||||
applyHighlightsToContent(displayContent, highlights: highlights, page: page)
|
||||
coreTextContentView.isHidden = false
|
||||
coreTextContentView.backgroundColor = .clear
|
||||
coreTextContentView.accessibilityIdentifier = "epub.reader.selection.text"
|
||||
coreTextDisplayContent = displayContent
|
||||
coreTextDisplayContent = display.content
|
||||
coreTextDisplayRange = pageRange
|
||||
coreTextLayouter = DTCoreTextLayouter(attributedString: displayContent)
|
||||
coreTextLayouter?.shouldCacheLayoutFrames = false
|
||||
coreTextContentView.attributedDisplayContent = displayContent
|
||||
coreTextLayouter = display.layouter
|
||||
updateCoreTextLayoutFrameIfNeeded()
|
||||
#else
|
||||
let selectionContent = normalizedPageContent(from: page)
|
||||
@ -521,7 +515,6 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
#if canImport(DTCoreText)
|
||||
coreTextContentView.isHidden = false
|
||||
coreTextContentView.backgroundColor = .clear
|
||||
coreTextContentView.attributedDisplayContent = nil
|
||||
coreTextContentView.layoutFrame = nil
|
||||
coreTextDisplayContent = nil
|
||||
coreTextDisplayRange = nil
|
||||
@ -615,31 +608,6 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
return image(from: attachmentValue)
|
||||
}
|
||||
|
||||
/// Marks highlight ranges on the chapter-length display copy. Ranges stay
|
||||
/// chapter-absolute — the in-context layout frame uses the same indices.
|
||||
private func applyHighlightsToContent(
|
||||
_ content: NSMutableAttributedString,
|
||||
highlights: [RDEPUBHighlight],
|
||||
page: RDEPUBTextPage
|
||||
) {
|
||||
for highlight in highlights {
|
||||
guard highlight.location.href == page.href else { continue }
|
||||
guard let rangeInfo = highlight.rangeInfo,
|
||||
let info = RDEPUBTextOffsetRangeInfo.decode(from: rangeInfo),
|
||||
let absoluteRange = info.nsRange else { continue }
|
||||
let overlap = NSIntersectionRange(absoluteRange, page.contentRange)
|
||||
guard overlap.length > 0,
|
||||
overlap.location + overlap.length <= content.length else { continue }
|
||||
|
||||
switch highlight.style {
|
||||
case .highlight:
|
||||
content.addAttribute(kRDEPUBHighlightAttributeName, value: highlight.uiColor, range: overlap)
|
||||
case .underline:
|
||||
content.addAttribute(kRDEPUBUnderlineAttributeName, value: highlight.uiColor, range: overlap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func normalizedPageContent(from page: RDEPUBTextPage) -> NSMutableAttributedString {
|
||||
let content = NSMutableAttributedString(attributedString: page.content)
|
||||
guard content.length > 0 else { return content }
|
||||
@ -759,12 +727,12 @@ final class RDEPUBTextContentView: UIView, UIGestureRecognizerDelegate, RDReader
|
||||
backgroundOverlayView.updateSnapshot(interactionController.snapshot)
|
||||
|
||||
if let page = currentPage {
|
||||
// Highlights/underlines and search are all drawn as overlay
|
||||
// decorations; the shared chapter display string carries no
|
||||
// page-level annotation attributes.
|
||||
let (bgDecorations, fgDecorations) = overlayView.buildDecorations(
|
||||
page: page,
|
||||
// Text-page highlights/underlines are already painted by the
|
||||
// CoreText render view via display-content attributes. Keep
|
||||
// overlay decorations for search only to avoid double drawing.
|
||||
highlights: [],
|
||||
highlights: currentHighlights,
|
||||
searchState: currentSearchState,
|
||||
interactionController: interactionController
|
||||
)
|
||||
|
||||
@ -22,12 +22,6 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
var attributedDisplayContent: NSAttributedString? {
|
||||
didSet {
|
||||
invalidateStaticContent()
|
||||
}
|
||||
}
|
||||
|
||||
var selectionRects: [CGRect] = [] {
|
||||
didSet {
|
||||
setNeedsDisplay()
|
||||
@ -78,9 +72,6 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
if let cachedStaticImage {
|
||||
cachedStaticImage.draw(in: bounds)
|
||||
} else {
|
||||
if let attributedDisplayContent {
|
||||
drawHighlights(in: context, attributedString: attributedDisplayContent, layoutFrame: layoutFrame)
|
||||
}
|
||||
layoutFrame.draw(in: context, options: drawOptions)
|
||||
}
|
||||
|
||||
@ -96,59 +87,6 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
private func drawHighlights(
|
||||
in context: CGContext,
|
||||
attributedString: NSAttributedString,
|
||||
layoutFrame: DTCoreTextLayoutFrame
|
||||
) {
|
||||
let fullRange = NSRange(location: 0, length: attributedString.length)
|
||||
|
||||
attributedString.enumerateAttribute(kRDEPUBHighlightAttributeName, in: fullRange) { value, range, _ in
|
||||
guard let color = value as? UIColor else { return }
|
||||
let rects = computeHighlightRects(for: range, layoutFrame: layoutFrame)
|
||||
color.setFill()
|
||||
for rect in rects {
|
||||
context.fill(rect)
|
||||
}
|
||||
}
|
||||
|
||||
attributedString.enumerateAttribute(kRDEPUBUnderlineAttributeName, in: fullRange) { value, range, _ in
|
||||
guard let color = value as? UIColor else { return }
|
||||
let rects = computeHighlightRects(for: range, layoutFrame: layoutFrame)
|
||||
color.setStroke()
|
||||
context.setLineWidth(2)
|
||||
for rect in rects {
|
||||
let y = rect.maxY - 1
|
||||
context.move(to: CGPoint(x: rect.minX, y: y))
|
||||
context.addLine(to: CGPoint(x: rect.maxX, y: y))
|
||||
context.strokePath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func computeHighlightRects(for range: NSRange, layoutFrame: DTCoreTextLayoutFrame) -> [CGRect] {
|
||||
var rects: [CGRect] = []
|
||||
guard let lines = layoutFrame.lines as? [DTCoreTextLayoutLine] else { return rects }
|
||||
|
||||
for line in lines {
|
||||
let lineRange = line.stringRange()
|
||||
let overlap = NSIntersectionRange(range, lineRange)
|
||||
guard overlap.length > 0 else { continue }
|
||||
|
||||
let startX = line.offset(forStringIndex: overlap.location)
|
||||
let endX = line.offset(forStringIndex: overlap.location + overlap.length)
|
||||
let rect = CGRect(
|
||||
x: line.baselineOrigin.x + startX,
|
||||
y: line.baselineOrigin.y - line.ascent,
|
||||
width: endX - startX,
|
||||
height: line.ascent + line.descent
|
||||
)
|
||||
rects.append(rect)
|
||||
}
|
||||
|
||||
return rects
|
||||
}
|
||||
|
||||
private func drawSelection(in context: CGContext) {
|
||||
guard !selectionRects.isEmpty else { return }
|
||||
selectionColor.setFill()
|
||||
@ -261,9 +199,6 @@ final class RDEPUBTextPageRenderView: UIView {
|
||||
let renderer = UIGraphicsImageRenderer(size: bounds.size, format: format)
|
||||
return renderer.image { _ in
|
||||
guard let staticContext = UIGraphicsGetCurrentContext() else { return }
|
||||
if let attributedDisplayContent {
|
||||
drawHighlights(in: staticContext, attributedString: attributedDisplayContent, layoutFrame: layoutFrame)
|
||||
}
|
||||
layoutFrame.draw(in: staticContext, options: drawOptions)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user