// // RDURLReaderController.swift // RDReaderDemo // // 文件职责:URL 阅读器入口控制器,根据文件类型自动选择阅读器。 // 该文件是 ReadViewSDK 的最简使用入口: // - .epub 文件:直接创建 RDEPUBReaderController // - 其他文本文件:通过 RDPlainTextBookBuilder 分页后创建 RDEPUBReaderController // - 分页失败时回退到纯文本 UITextView 展示 // // 架构位置:RDReaderView 四层架构中第四层(读者 UI)的入口控制器。 // import UIKit /// URL 阅读器入口控制器 /// 接收一个书籍文件 URL,根据文件类型自动创建对应的阅读器控制器: /// - EPUB 文件:直接使用 RDEPUBReaderController /// - 纯文本文件:先通过 RDPlainTextBookBuilder 分页,再使用 RDEPUBReaderController /// - 分页失败:回退到纯文本 UITextView 展示 /// /// 使用方式: /// ```swift /// let controller = RDURLReaderController(bookURL: fileURL) /// navigationController?.pushViewController(controller, animated: true) /// ``` public final class RDURLReaderController: UIViewController { private struct PendingDemoPageRequest { let pageNumber: Int let animated: Bool var attemptCount: Int } /// 书籍文件 URL private let bookURL: URL /// EPUB 阅读器配置(字体、行距、主题等) private let epubConfiguration: RDEPUBReaderConfiguration /// 内嵌的阅读器控制器 private var embeddedController: UIViewController? private let demoStateLabel: UILabel = { let label = UILabel() label.accessibilityIdentifier = "demo.reader.state" label.font = .systemFont(ofSize: 1) label.textColor = .clear label.alpha = 0.01 label.isAccessibilityElement = true label.text = "reader=opening" return label }() private var pendingDemoPageRequest: PendingDemoPageRequest? private var isRetryingPendingDemoPage = false private let maxPendingDemoPageAttempts = 24 private let pendingDemoPageRetryDelay: TimeInterval = 0.25 /// 初始化方法 /// - Parameters: /// - bookURL: 书籍文件的本地 URL /// - epubConfiguration: EPUB 阅读器配置,默认使用标准配置 public init( bookURL: URL, epubConfiguration: RDEPUBReaderConfiguration = RDEPUBReaderConfiguration() ) { self.bookURL = bookURL self.epubConfiguration = epubConfiguration super.init(nibName: nil, bundle: nil) } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } /// 视图加载完成后,设置背景色、标题并嵌入阅读器控制器 public override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground title = bookURL.deletingPathExtension().lastPathComponent embedReaderController() installDemoStateLabel() } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) emitDemoState() } /// 切换阅读器的翻页模式(Demo 用) /// - Parameter displayType: 目标翻页模式 public func applyDemoDisplayType(_ displayType: RDReaderView.DisplayType) { readerController?.configuration.displayType = displayType emitDemoState(prefix: "display=\(displayType.demoArgumentValue)") } /// 跳转到指定页码(Demo 用) /// - Parameters: /// - pageNumber: 目标页码 /// - animated: 是否动画过渡 /// - Returns: 是否成功跳转 @discardableResult public func goToDemoPage(_ pageNumber: Int, animated: Bool = false) -> Bool { let moved = performDemoPageNavigation(pageNumber, animated: animated) if moved { pendingDemoPageRequest = nil isRetryingPendingDemoPage = false } else { queuePendingDemoPageNavigation(pageNumber, animated: animated) } return moved } /// 执行翻页模式切换序列(Demo 用) /// 按照指定顺序和延迟依次切换翻页模式 /// - Parameters: /// - displayTypes: 翻页模式数组 /// - initialPageNumber: 可选的初始跳转页码 /// - stepDelay: 每步之间的延迟时间(秒) public func runDemoDisplaySequence( _ displayTypes: [RDReaderView.DisplayType], initialPageNumber: Int? = nil, stepDelay: TimeInterval = 1.0 ) { let normalizedDelay = max(stepDelay, 0.1) if let initialPageNumber { DispatchQueue.main.asyncAfter(deadline: .now() + normalizedDelay) { [weak self] in _ = self?.goToDemoPage(initialPageNumber) } } for (index, displayType) in displayTypes.enumerated() { let delay = normalizedDelay * Double(index + 1 + (initialPageNumber == nil ? 0 : 1)) DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in self?.applyDemoDisplayType(displayType) } } } /// 嵌入阅读器控制器到当前视图层级 /// 根据文件类型选择合适的阅读器控制器,并通过 Child View Controller 方式嵌入 private func embedReaderController() { let controller: UIViewController if bookURL.pathExtension.lowercased() == "epub" { controller = RDEPUBReaderController( epubURL: bookURL, configuration: epubConfiguration ) } else { let bookIdentifier = bookURL.lastPathComponent let bookTitle = bookURL.deletingPathExtension().lastPathComponent let pageSize = currentTextPageSize() let renderStyle = currentTextRenderStyle() let builder = RDPlainTextBookBuilder( layoutConfig: RDEPUBTextLayoutConfig( frameWidth: pageSize.width, frameHeight: pageSize.height, edgeInsets: epubConfiguration.reflowableContentInsets, numberOfColumns: 1, columnGap: 20, avoidOrphans: true, avoidWidows: true, avoidPageBreakInsideEnabled: true, hyphenation: true, imageMaxHeightRatio: 0.85 ) ) if let textBook = try? builder.build(textFileURL: bookURL, pageSize: pageSize, style: renderStyle) { controller = RDEPUBReaderController( textBook: textBook, bookIdentifier: bookIdentifier, title: bookTitle, textFileURL: bookURL, configuration: epubConfiguration ) } else { // 分页失败时回退到纯文本展示 let fallback = UIViewController() let textView = UITextView() textView.isEditable = false textView.text = rd_decodeTextFile(url: bookURL) fallback.view = textView controller = fallback } } embeddedController = controller addChild(controller) controller.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(controller.view) NSLayoutConstraint.activate([ controller.view.topAnchor.constraint(equalTo: view.topAnchor), controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) controller.didMove(toParent: self) readerController?.delegate = self emitDemoState() } /// 获取内嵌的 EPUB 阅读器控制器(如果存在) private var readerController: RDEPUBReaderController? { embeddedController as? RDEPUBReaderController } @discardableResult private func performDemoPageNavigation(_ pageNumber: Int, animated: Bool) -> Bool { guard let readerController else { return false } guard pageNumber > 0 else { return false } let totalPages = readerController.textBook?.pages.count ?? readerController.activePages.count let numPages = readerController.readerView.numberOfPages() print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): textBook.pages=\(totalPages), readerView.numberOfPages=\(numPages), currentPage=\(readerController.readerView.currentPage)") guard pageNumber <= totalPages else { return false } readerController.readerView.transitionToPage(pageNum: pageNumber - 1, animated: animated) print("[ReadViewDemo] performDemoPageNavigation(\(pageNumber)): after transition, currentPage=\(readerController.readerView.currentPage)") emitDemoState(prefix: "page=\(pageNumber)") return true } private func queuePendingDemoPageNavigation(_ pageNumber: Int, animated: Bool) { if let pendingDemoPageRequest, pendingDemoPageRequest.pageNumber == pageNumber, pendingDemoPageRequest.animated == animated { return } pendingDemoPageRequest = PendingDemoPageRequest( pageNumber: pageNumber, animated: animated, attemptCount: 0 ) retryPendingDemoPageIfNeeded() } private func retryPendingDemoPageIfNeeded() { guard !isRetryingPendingDemoPage else { return } guard pendingDemoPageRequest != nil else { return } isRetryingPendingDemoPage = true DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in self?.attemptPendingDemoPageNavigation() } } private func attemptPendingDemoPageNavigation() { guard var pendingDemoPageRequest else { isRetryingPendingDemoPage = false return } if performDemoPageNavigation(pendingDemoPageRequest.pageNumber, animated: pendingDemoPageRequest.animated) { self.pendingDemoPageRequest = nil isRetryingPendingDemoPage = false return } pendingDemoPageRequest.attemptCount += 1 if pendingDemoPageRequest.attemptCount >= maxPendingDemoPageAttempts { self.pendingDemoPageRequest = nil isRetryingPendingDemoPage = false print("[ReadViewDemo] automation page=\(pendingDemoPageRequest.pageNumber) failed after \(pendingDemoPageRequest.attemptCount) attempts") return } self.pendingDemoPageRequest = pendingDemoPageRequest DispatchQueue.main.asyncAfter(deadline: .now() + pendingDemoPageRetryDelay) { [weak self] in self?.attemptPendingDemoPageNavigation() } } /// 输出当前阅读器状态日志(Demo 调试用) private func logDemoState(prefix: String) { guard let readerController else { return } let location = readerController.currentLocation let href = location?.href ?? "nil" let progression = location.map { String(format: "%.4f", $0.navigationProgression) } ?? "nil" let page = readerController.currentPageNumber.map(String.init) ?? "nil" print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)") } private func installDemoStateLabel() { view.addSubview(demoStateLabel) demoStateLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ demoStateLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor), demoStateLabel.topAnchor.constraint(equalTo: view.topAnchor), demoStateLabel.widthAnchor.constraint(equalToConstant: 1), demoStateLabel.heightAnchor.constraint(equalToConstant: 1) ]) } private func emitDemoState(prefix: String? = nil) { let page = readerController?.currentPageNumber.map(String.init) ?? "nil" let display = readerController?.configuration.displayType.demoArgumentValue ?? epubConfiguration.displayType.demoArgumentValue let toolbar = readerController?.readerView.isShowToolView == true ? "visible" : "hidden" let highlights = readerController?.highlights.count ?? 0 let selection = readerController?.currentSelection == nil ? 0 : 1 let state = "reader=opened page=\(page) display=\(display) toolbar=\(toolbar) highlights=\(highlights) selection=\(selection)" demoStateLabel.text = state if let prefix { logDemoState(prefix: prefix) } else { print("[ReadViewDemo] automation \(state)") } } /// 计算文本分页的页面尺寸。 /// 分页器使用完整 viewport,真实可绘制区域由 layoutConfig.edgeInsets 控制, /// 以保持与页面展示时的内容几何一致。 private func currentTextPageSize() -> CGSize { UIScreen.main.bounds.size } /// 根据当前配置生成文本渲染样式 private func currentTextRenderStyle() -> RDEPUBTextRenderStyle { let font = epubConfiguration.fontChoice.font(ofSize: epubConfiguration.fontSize) let lineSpacing = max(font.lineHeight * (epubConfiguration.lineHeightMultiple - 1), 4) return RDEPUBTextRenderStyle( font: font, lineSpacing: lineSpacing, textColor: epubConfiguration.theme.contentTextColor, backgroundColor: epubConfiguration.theme.contentBackgroundColor ) } /// 解码文本文件内容 /// 尝试 UTF-8 编码,失败则尝试 GBK 和 GB2312 编码 private func rd_decodeTextFile(url: URL) -> String { if let content = try? NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String { return content } if let content = try? NSString(contentsOf: url, encoding: 0x80000632) as String { return content } if let content = try? NSString(contentsOf: url, encoding: 0x80000631) as String { return content } return "" } } extension RDURLReaderController: RDEPUBReaderDelegate { public func epubReader(_ reader: UIViewController, didOpen publication: RDEPUBPublication) { retryPendingDemoPageIfNeeded() emitDemoState() } public func epubReader(_ reader: UIViewController, didUpdateLocation location: RDEPUBLocation) { retryPendingDemoPageIfNeeded() emitDemoState() } public func epubReader(_ reader: UIViewController, didChangeSelection selection: RDEPUBSelection?) { emitDemoState(prefix: "selection=\(selection == nil ? 0 : 1)") } public func epubReader(_ reader: UIViewController, didUpdateHighlights highlights: [RDEPUBHighlight]) { emitDemoState(prefix: "highlights=\(highlights.count)") } } /// 翻页模式扩展:提供 Demo 命令行参数值 private extension RDReaderView.DisplayType { /// 返回翻页模式对应的 Demo 命令行参数字符串 var demoArgumentValue: String { switch self { case .pageCurl: return "pageCurl" case .horizontalScroll: return "horizontalScroll" case .verticalScroll: return "verticalScroll" } } }