From d830311894040f6d137fd812628f89dd26f56c58 Mon Sep 17 00:00:00 2001 From: shenlei Date: Thu, 30 Jul 2026 17:52:29 +0900 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20PDF=20=E4=BB=BF=E7=9C=9F?= =?UTF-8?q?=E7=BF=BB=E9=A1=B5=E6=A8=A1=E5=BC=8F=E4=B8=8B=E7=BF=BB=E9=A1=B5?= =?UTF-8?q?=E5=90=8E=E9=A1=B5=E9=9D=A2=E5=85=A8=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 打开普通 PDF 并使用仿真翻页阅读时,除首页外每翻一页都是纯白:没有页面 内容,也没有页面四周的衬底色。横向滚动与竖滑模式不受影响。样张 英汉大词典App需求说明.pdf 上第 2 到第 9 页无一例外。 根因是页面数据回来时找不到对应的视图。UIPageViewController 会通过 viewControllerBefore/After 提前构建相邻页的内容视图,构建时页面描述尚未 就绪,configure 发起异步 readerPage 请求后先返回一张空白页。描述返回后 acceptPageDescriptor 调用 refreshVisiblePage,而它经由 RDPDFReaderView.pageContentView(pageNum:) 定位视图,该方法在仿真翻页下 只检索 nativeCurlPageController.viewControllers,也就是已经上屏的 child。 预建的 child 此时还不在其中,图片因此从未被装配上去;翻页动画结束后又 没有任何路径重新 configure 该视图,白页就固定下来。此前偶尔能看到内容, 是 OCR 完成回调顺带触发了一次刷新,并非稳定行为。 改为按页码刷新该页的全部活动视图:控制器用弱引用表记录已创建的页面视图, refreshVisiblePage 改名为 refreshPageViews(at:),除已上屏视图外一并装配 尚未上屏的预建视图,pageContentView 创建时即写入 tag 以便按页码匹配。 翻页落定时在 pageNum 回调中补一次装配,使内存告警清掉页面描述的场景也能 重新发起请求并恢复。另外把 image 为空的页面描述改按无效结果处理,走既有 的重试与失败重载入口,不再缓存出一张永远不会重试的白页。 新增 PDFPageTurnBlankPageTests 作为回归:翻页稳定后截屏统计画面中部深色 像素占比,白页为 0,扉页约 0.002,正文页在 0.01 以上,以 0.0005 为判定 阈值,覆盖仿真翻页正翻、仿真回翻和横向滚动三种路径。 验证:iPhone 17 Pro / iOS 26.4.1 模拟器执行 PDFPageTurnBlankPageTests、 PDFAnnotationTests、PDFDrawingModeTests、PageNavigationTests、 DisplayTypeTests 共 23 条用例,22 条通过。修复前仿真翻页第 2 到 9 页墨迹 占比全为 0.0000,修复后与横向滚动模式逐页吻合。唯一失败的 DisplayTypeTests.testDisplayTypeSwitchMidSession 属 EPUB 设置面板既有问题, 将本次改动还原后同样失败。文档补充了 Demo 因链接 FoundationModels 而必须 使用 iOS 26 及以上模拟器的说明。 Co-Authored-By: Claude Opus 5 --- Doc/TESTING.md | 7 + .../ReadViewDemo.xcodeproj/project.pbxproj | 4 + .../PDFPageTurnBlankPageTests.swift | 147 ++++++++++++++++++ .../Sources/RDPDFReaderViewController.swift | 61 +++++--- 4 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 ReadViewDemo/ReadViewDemoUITests/ReaderUITests/PDFPageTurnBlankPageTests.swift diff --git a/Doc/TESTING.md b/Doc/TESTING.md index f506a58..1cc55bf 100644 --- a/Doc/TESTING.md +++ b/Doc/TESTING.md @@ -34,6 +34,7 @@ | LocationPersistenceTests.swift | 阅读位置持久化 | | MetadataParseBenchmarkTests.swift | 元数据解析性能基准 | | PageNavigationTests.swift | 页面导航 | +| PDFPageTurnBlankPageTests.swift | PDF 翻页后白页回归(截屏墨迹占比判定) | | ReaderAnnotationTests.swift | 标注基础功能 | | ReaderAnnotationExtendedTests.swift | 标注扩展功能 | | ReaderOpenCloseTests.swift | 阅读器打开/关闭 | @@ -72,6 +73,12 @@ cd ReadViewDemo && pod install (依赖入口:`ReadViewDemo/Podfile`) +### 模拟器要求 + +Demo 链接了 `FoundationModels.framework`(AI 助手示例入口)。该框架自 iOS 26 起提供, +在 iOS 18 及更早的模拟器上 App 会在启动时因 `Library not loaded` 直接崩溃,测试表现为 +卡在 Launch 阶段。请选择 iOS 26 及以上的模拟器运行 UI 测试。 + ### 运行 UI 测试 ```bash diff --git a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj index 0da53f8..7232b2b 100644 --- a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj +++ b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj @@ -41,6 +41,7 @@ 63B2852E59996922386A3111 /* AccessibilityIdentifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00F495E17B90CCF1C7FE8C27 /* AccessibilityIdentifiers.swift */; }; 9AAFB855EC7AC09D1E0B6742 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 697A5BCF0D93DBEC18E21A09 /* Foundation.framework */; }; A1C5E34F6B7192D400A1B234 /* PDFAnnotationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1C5E34F6B7192D400A1B235 /* PDFAnnotationTests.swift */; }; + C0FFEE0100000001AABBCC01 /* PDFPageTurnBlankPageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0FFEE0100000001AABBCC02 /* PDFPageTurnBlankPageTests.swift */; }; C08FF8D030048DC5147729E9 /* ReaderOpenCloseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */; }; DE1437A969DA1C5F0CBB047D /* Pods_ReadViewDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 792DF85CE4A3DD80D67843C7 /* Pods_ReadViewDemo.framework */; }; FEDB5937CEB858CB06E38E2D /* SettingsPanelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201C2B482287866487EFAE66 /* SettingsPanelTests.swift */; }; @@ -94,6 +95,7 @@ 8FFD606A5A1CBDCC3CA87F1C /* ReadViewDemoUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReadViewDemoUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 9EDF066BA12974E6CFBE519F /* ReaderOpenCloseTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderOpenCloseTests.swift; sourceTree = ""; }; A1C5E34F6B7192D400A1B235 /* PDFAnnotationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PDFAnnotationTests.swift; sourceTree = ""; }; + C0FFEE0100000001AABBCC02 /* PDFPageTurnBlankPageTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PDFPageTurnBlankPageTests.swift; sourceTree = ""; }; BADF0A18AD034B74A482A4C1 /* ReaderToolbarTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReaderToolbarTests.swift; sourceTree = ""; }; CFE01DBDCB8D790832A4DE3F /* SearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = ""; }; DE070C1D2FBF0CC900ED065F /* ReadViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReadViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -213,6 +215,7 @@ FB49AFCCBC2BE04C82B8F286 /* DisplayTypeTests.swift */, 1A2B3C4D00000008AABBCC01 /* PageNavigationTests.swift */, A1C5E34F6B7192D400A1B235 /* PDFAnnotationTests.swift */, + C0FFEE0100000001AABBCC02 /* PDFPageTurnBlankPageTests.swift */, 2B3C4D5E00000001AABBCC02 /* BookmarkChromeStateTests.swift */, 2B3C4D5E00000002AABBCC02 /* BookmarkManagementTests.swift */, 2B3C4D5E00000003AABBCC02 /* ErrorAndEdgeCaseTests.swift */, @@ -394,6 +397,7 @@ 23BB1155EA379786DAA10A89 /* DisplayTypeTests.swift in Sources */, 1A2B3C4D00000007AABBCC01 /* PageNavigationTests.swift in Sources */, A1C5E34F6B7192D400A1B234 /* PDFAnnotationTests.swift in Sources */, + C0FFEE0100000001AABBCC01 /* PDFPageTurnBlankPageTests.swift in Sources */, 2B3C4D5E00000001AABBCC01 /* BookmarkChromeStateTests.swift in Sources */, 2B3C4D5E00000002AABBCC01 /* BookmarkManagementTests.swift in Sources */, 2B3C4D5E00000003AABBCC01 /* ErrorAndEdgeCaseTests.swift in Sources */, diff --git a/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/PDFPageTurnBlankPageTests.swift b/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/PDFPageTurnBlankPageTests.swift new file mode 100644 index 0000000..71798f3 --- /dev/null +++ b/ReadViewDemo/ReadViewDemoUITests/ReaderUITests/PDFPageTurnBlankPageTests.swift @@ -0,0 +1,147 @@ +import XCTest +import UIKit + +/// PDF 翻页后白页回归。 +/// +/// 判定方式:每次翻页稳定后截屏,统计画面中部的深色像素占比。正常 PDF 正文页 +/// 含文字与线条,占比明显高于阈值;页面图片没有装配到视图上时几乎全白。 +final class PDFPageTurnBlankPageTests: XCTestCase { + private let app = XCUIApplication() + + /// 深色像素占比低于该值即视为白页。样张实测:白页为 0.0000, + /// 内容最少的扉页约 0.0020,正文页在 0.0100 以上。 + private let blankInkRatioThreshold = 0.0005 + + override func setUpWithError() throws { + continueAfterFailure = false + } + + func testPageCurlForwardTurnsRenderPageContent() { + openDictionaryPDF(displayType: "pagecurl") + assertForwardTurnsRenderContent(turns: 8, mode: "仿真翻页") + } + + func testHorizontalScrollForwardTurnsRenderPageContent() { + openDictionaryPDF(displayType: "horizontalscroll") + assertForwardTurnsRenderContent(turns: 8, mode: "横向滚动") + } + + /// 回翻同样依赖 `viewControllerBefore` 预建的页面视图,需要单独覆盖。 + func testPageCurlBackwardTurnsRenderPageContent() { + openDictionaryPDF(displayType: "pagecurl") + assertForwardTurnsRenderContent(turns: 5, mode: "仿真翻页") + assertBackwardTurnsRenderContent(from: 6, turns: 5, mode: "仿真回翻") + } + + // MARK: - 步骤 + + private func openDictionaryPDF(displayType: String) { + app.launchAndOpenSampleBook( + bookTitleQuery: "英汉大词典", + displayType: displayType, + resetsReaderState: true + ) + app.waitForReader(timeout: 30) + app.waitForReaderPage(1, timeout: 30) + // 首页首次渲染需要等待异步解码完成,避免把加载中误判成白页。 + RunLoop.current.run(until: Date().addingTimeInterval(2.5)) + } + + private func assertForwardTurnsRenderContent(turns: Int, mode: String) { + var blankPages: [Int] = [] + + recordInk(page: 1, mode: mode, blankPages: &blankPages) + + for step in 1...turns { + let expectedPage = step + 1 + turnToNextPage() + app.waitForDemoReaderState(timeout: 15, description: "\(mode) page=\(expectedPage)") { + $0.page == expectedPage + } + // 翻页动画结束后再取样,排除卷页过程中的中间帧。 + RunLoop.current.run(until: Date().addingTimeInterval(1.2)) + recordInk(page: expectedPage, mode: mode, blankPages: &blankPages) + } + + XCTAssertTrue( + blankPages.isEmpty, + "\(mode):翻页后出现白页,页码 \(blankPages.map(String.init).joined(separator: ", "))" + ) + } + + private func assertBackwardTurnsRenderContent(from startPage: Int, turns: Int, mode: String) { + var blankPages: [Int] = [] + + for step in 1...turns { + let expectedPage = startPage - step + guard expectedPage >= 1 else { break } + turnPage(forward: false) + app.waitForDemoReaderState(timeout: 15, description: "\(mode) page=\(expectedPage)") { + $0.page == expectedPage + } + RunLoop.current.run(until: Date().addingTimeInterval(1.2)) + recordInk(page: expectedPage, mode: mode, blankPages: &blankPages) + } + + XCTAssertTrue( + blankPages.isEmpty, + "\(mode):翻页后出现白页,页码 \(blankPages.map(String.init).joined(separator: ", "))" + ) + } + + private func turnToNextPage() { turnPage(forward: true) } + + private func turnPage(forward: Bool) { + let content = app.otherElements[IDs.readerContent].firstMatch + let target: XCUIElement = content.exists ? content : app.windows.firstMatch + let leading = CGVector(dx: 0.08, dy: 0.5) + let trailing = CGVector(dx: 0.88, dy: 0.5) + let start = target.coordinate(withNormalizedOffset: forward ? trailing : leading) + let end = target.coordinate(withNormalizedOffset: forward ? leading : trailing) + start.press(forDuration: 0.05, thenDragTo: end) + } + + private func recordInk(page: Int, mode: String, blankPages: inout [Int]) { + let screenshot = XCUIScreen.main.screenshot() + let ratio = inkRatio(of: screenshot.image) + + let attachment = XCTAttachment(screenshot: screenshot) + attachment.name = "\(mode)-page-\(page)-ink-\(String(format: "%.4f", ratio))" + attachment.lifetime = .keepAlways + add(attachment) + + if ratio < blankInkRatioThreshold { + blankPages.append(page) + } + } + + /// 画面中部深色像素占比。裁掉上下边缘以排除状态栏、工具栏和页面外底色。 + private func inkRatio(of image: UIImage) -> Double { + guard let cgImage = image.cgImage else { return 0 } + let cropRect = CGRect( + x: CGFloat(cgImage.width) * 0.12, + y: CGFloat(cgImage.height) * 0.16, + width: CGFloat(cgImage.width) * 0.76, + height: CGFloat(cgImage.height) * 0.68 + ).integral + guard let cropped = cgImage.cropping(to: cropRect) else { return 0 } + + let side = 400 + var pixels = [UInt8](repeating: 255, count: side * side) + guard let context = CGContext( + data: &pixels, + width: side, + height: side, + bitsPerComponent: 8, + bytesPerRow: side, + space: CGColorSpaceCreateDeviceGray(), + bitmapInfo: CGImageAlphaInfo.none.rawValue + ) else { return 0 } + context.draw(cropped, in: CGRect(x: 0, y: 0, width: side, height: side)) + + let darkCount = pixels.reduce(into: 0) { partial, value in + if value < 150 { partial += 1 } + } + return Double(darkCount) / Double(side * side) + } +} diff --git a/Sources/RDPDFReaderView/Sources/RDPDFReaderViewController.swift b/Sources/RDPDFReaderView/Sources/RDPDFReaderViewController.swift index 8c71f9f..825cdc4 100644 --- a/Sources/RDPDFReaderView/Sources/RDPDFReaderViewController.swift +++ b/Sources/RDPDFReaderView/Sources/RDPDFReaderViewController.swift @@ -48,6 +48,10 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS private var book: RDPDFReaderBookDescriptor private var currentTheme: RDPDFReaderThemeOption private var pageDescriptors: [Int: RDPDFReaderPageDescriptor] = [:] + /// 已创建但不一定在屏幕上的页面视图。UIPageViewController 与 UICollectionView + /// 都会提前构建相邻页,这些视图在异步页面数据返回时还不在 + /// `visiblePageContentViews()` 里;只刷新可见页会让它们翻到时停留在白页。 + private let livePageViews = NSHashTable.weakObjects() /// Provider 不要求自行去重;同一页在旋转、cell 重建期间只能保留一个请求。 private var pageDescriptorRequestTokens: [Int: UUID] = [:] private var pageDescriptorRequestTimeouts: [Int: DispatchWorkItem] = [:] @@ -244,7 +248,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS guard !runs.isEmpty else { return [] } ocrRuns[pageIndex] = runs ocrDiskCache?.save(runs, pageIndex: pageIndex) - refreshVisiblePage(pageIndex) + refreshPageViews(at: pageIndex) return runs } @@ -266,13 +270,13 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS guard pageIndex >= 0, pageIndex < book.totalPages else { return } speechHighlight = normalizedRects.isEmpty ? nil : (pageIndex, normalizedRects) goToPage(pageIndex, animated: animated) - refreshVisiblePage(pageIndex) + refreshPageViews(at: pageIndex) } public func clearSpeechHighlight() { let highlightedPage = speechHighlight?.pageIndex speechHighlight = nil - if let highlightedPage { refreshVisiblePage(highlightedPage) } + if let highlightedPage { refreshPageViews(at: highlightedPage) } } /// 首版画笔模式入口。横屏双页下左右内容页各自承载画布,笔迹严格裁剪在所属页内。 @@ -292,6 +296,10 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS public func pageContentView(readerView: RDPDFReaderView, pageNum: Int) -> UIView { let page = RDPDFReaderPageView() page.delegate = self + // ReaderView 会在返回后再写一次 tag;这里提前写入,异步页面数据返回时 + // 才能按页码找到尚未上屏的预建视图。 + page.tag = pageNum + livePageViews.add(page) configure(page, at: pageNum) return page } @@ -323,6 +331,10 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS guard pageNum >= 0 else { return } // OCR 队列是串行的。快速翻页后取消过期任务,避免停留页排在大量离开页之后。 cancelOutstandingOCRRequests() + // 翻到的页可能是提前构建的视图:它在离屏期间收到过页面数据,也可能因内存 + // 警告被清掉了描述。上屏后重新装配一次,缺数据时由 configure 重新发起请求。 + // 置于取消之后,避免刚排入的识别任务立刻被同一次调用清掉。 + refreshPageViews(at: pageNum) // 取消是全量的:双页的右页和竖滑可见邻页也被一并取消,必须重新排队, // 否则它们在 cell 重建前一直没有文字层。当前页先入队以获得串行队列优先权。 startOCRIfNeeded(pageNum) @@ -394,8 +406,9 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS pageDescriptors[index] = descriptor // 宽度适配竖滑下,占位的一屏高 cell 要按真实纸张比例重新排版。 readerView.invalidateWidthFitLayoutIfNeeded(updatedPage: index) - // 初始请求的 cell 可能已经因旋转或复用离开屏幕;只刷新仍显示的实际页面。 - refreshVisiblePage(index) + // 初始请求的 cell 可能已经因旋转或复用离开屏幕,也可能是仿真翻页提前构建、 + // 尚未上屏的页;两者都要装配,否则翻到它时是白页。 + refreshPageViews(at: index) } private func handleInvalidPageDescriptor(at index: Int, token: UUID, attempt: Int) { @@ -404,7 +417,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS if attempt >= maximumPageDescriptorRequestAttempts { pageLoadFailedPages.insert(index) } - refreshVisiblePage(index) + refreshPageViews(at: index) } private func trimOCRCache(around pageIndex: Int, radius: Int) { @@ -474,7 +487,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS self.pageLoadFailedPages.insert(index) } // 前两次超时会重新请求,第三次停止并展示手动重试入口。 - self.refreshVisiblePage(index) + self.refreshPageViews(at: index) } pageDescriptorRequestTimeouts[index] = timeout let timeoutSeconds = pow(2.0, Double(attempt - 1)) * 8.0 @@ -482,7 +495,9 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS pageProvider.readerPage(at: index) { [weak self] descriptor in DispatchQueue.main.async { [weak self] in guard let self else { return } - guard descriptor.index == index else { + // 没有图片的描述同样无法显示。缓存它只会得到一张永远不再重试的 + // 白页,因此按无效结果处理:重试耗尽后给出可点击的重新加载入口。 + guard descriptor.index == index, descriptor.image != nil else { self.handleInvalidPageDescriptor(at: index, token: token, attempt: attempt) return } @@ -550,8 +565,8 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS recognizingPages.remove(pageIndex) ocrRuns[pageIndex] = runs if shouldPersist { ocrDiskCache?.save(runs, pageIndex: pageIndex) } - // 只有仍在屏幕上的页会被重新配置;快速翻过的页面不抢占当前页 UI 更新。 - refreshVisiblePage(pageIndex) + // 只重新配置该页自己的视图,快速翻过的其它页面不抢占当前页 UI 更新。 + refreshPageViews(at: pageIndex) } private func cancelOutstandingOCRRequests() { @@ -560,9 +575,19 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS ocrRequestTokens.removeAll() } - private func refreshVisiblePage(_ index: Int) { - guard let page = readerView.pageContentView(pageNum: index) as? RDPDFReaderPageView else { return } - configure(page, at: index) + /// 重新装配某一页的全部活动视图。仿真翻页的相邻页由 UIPageViewController 预先 + /// 构建,收到页面数据时还不在 `viewControllers` 中;只按“可见”查找会漏掉它们, + /// 翻页后就会显示白页。 + private func refreshPageViews(at index: Int) { + var targets: [RDPDFReaderPageView] = [] + if let displayed = readerView.pageContentView(pageNum: index) as? RDPDFReaderPageView { + targets.append(displayed) + } + for page in livePageViews.allObjects + where page.tag == index && !targets.contains(where: { $0 === page }) { + targets.append(page) + } + targets.forEach { configure($0, at: index) } } /// 屏幕上所有内容页视图。双页模式下 `visiblePageContentViews` 返回的是 spread @@ -821,7 +846,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS view.backgroundColor = theme.contentBackgroundColor readerView.backgroundColor = pageSurroundingColor applyChromeTheme() - if readerView.currentPage >= 0 { refreshVisiblePage(readerView.currentPage); readerView.refreshCurrentPageIfNeeded() } + if readerView.currentPage >= 0 { refreshPageViews(at: readerView.currentPage); readerView.refreshCurrentPageIfNeeded() } } private func applyChromeTheme() { @@ -834,7 +859,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS do { _ = try annotationPersistence.addAnnotation(.init(pageIndex: page, selectedText: selection.text, normalizedRects: selection.normalizedRects, color: color, note: note, source: selection.source)) reloadAnnotationCache() - refreshVisiblePage(page) + refreshPageViews(at: page) } catch { delegate?.pdfReaderViewController(self, didFailAnnotationPersistence: error) } } @@ -843,7 +868,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS do { _ = try annotationPersistence?.deleteAnnotation(id: annotation.id) reloadAnnotationCache() - refreshVisiblePage(annotation.pageIndex) + refreshPageViews(at: annotation.pageIndex) } catch { delegate?.pdfReaderViewController(self, didFailAnnotationPersistence: error) } } @@ -855,7 +880,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS public func pageView(_ pageView: RDPDFReaderPageView, didRequestHighlight selection: RDPDFReaderImageTextSelection, color: String) { add(selection, page: pageView.pageIndex, color: color, note: nil) } public func pageView(_ pageView: RDPDFReaderPageView, didRequestAnnotation selection: RDPDFReaderImageTextSelection) { presentEditor(selection: selection, page: pageView.pageIndex) } public func pageView(_ pageView: RDPDFReaderPageView, didOpenAnnotation annotation: RDPDFReaderAnnotation) { presentEditor(annotation: annotation) } - public func pageView(_ pageView: RDPDFReaderPageView, didRequestHighlightMenuAction action: RDPDFReaderExistingHighlightMenuAction, highlight: RDPDFReaderAnnotation) { if action == .deleteUnderline { delete(highlight) } else if action == .deleteAnnotation { var item = highlight; item.note = nil; do { _ = try annotationPersistence?.updateAnnotation(item); refreshVisiblePage(item.pageIndex) } catch { delegate?.pdfReaderViewController(self, didFailAnnotationPersistence: error) } } else if action == .annotate { presentEditor(annotation: highlight) } } + public func pageView(_ pageView: RDPDFReaderPageView, didRequestHighlightMenuAction action: RDPDFReaderExistingHighlightMenuAction, highlight: RDPDFReaderAnnotation) { if action == .deleteUnderline { delete(highlight) } else if action == .deleteAnnotation { var item = highlight; item.note = nil; do { _ = try annotationPersistence?.updateAnnotation(item); refreshPageViews(at: item.pageIndex) } catch { delegate?.pdfReaderViewController(self, didFailAnnotationPersistence: error) } } else if action == .annotate { presentEditor(annotation: highlight) } } private func presentEditor(selection: RDPDFReaderImageTextSelection, page: Int) { let editor = RDPDFReaderAnnotationEditorViewController(quote: selection.text?.isEmpty == false ? selection.text! : "区域标注", theme: currentTheme, onSave: { [weak self] note in self?.add(selection, page: page, note: note) }) @@ -869,7 +894,7 @@ public final class RDPDFReaderViewController: UIViewController, RDPDFReaderDataS do { _ = try self?.annotationPersistence?.updateAnnotation(item) self?.reloadAnnotationCache() - self?.refreshVisiblePage(item.pageIndex) + self?.refreshPageViews(at: item.pageIndex) } catch { if let self { self.delegate?.pdfReaderViewController(self, didFailAnnotationPersistence: error) } }