231 lines
9.2 KiB
Swift
231 lines
9.2 KiB
Swift
//
|
|
// 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 {
|
|
/// 书籍文件 URL
|
|
private let bookURL: URL
|
|
/// EPUB 阅读器配置(字体、行距、主题等)
|
|
private let epubConfiguration: RDEPUBReaderConfiguration
|
|
/// 内嵌的阅读器控制器
|
|
private var embeddedController: UIViewController?
|
|
|
|
/// 初始化方法
|
|
/// - 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()
|
|
}
|
|
|
|
/// 切换阅读器的翻页模式(Demo 用)
|
|
/// - Parameter displayType: 目标翻页模式
|
|
public func applyDemoDisplayType(_ displayType: RDReaderView.DisplayType) {
|
|
readerController?.configuration.displayType = displayType
|
|
logDemoState(prefix: "display=\(displayType.demoArgumentValue)")
|
|
}
|
|
|
|
/// 跳转到指定页码(Demo 用)
|
|
/// - Parameters:
|
|
/// - pageNumber: 目标页码
|
|
/// - animated: 是否动画过渡
|
|
/// - Returns: 是否成功跳转
|
|
@discardableResult
|
|
public func goToDemoPage(_ pageNumber: Int, animated: Bool = false) -> Bool {
|
|
let moved = readerController?.go(toPageNumber: pageNumber, animated: animated) ?? false
|
|
if moved {
|
|
logDemoState(prefix: "page=\(pageNumber)")
|
|
}
|
|
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)
|
|
}
|
|
|
|
/// 获取内嵌的 EPUB 阅读器控制器(如果存在)
|
|
private var readerController: RDEPUBReaderController? {
|
|
embeddedController as? RDEPUBReaderController
|
|
}
|
|
|
|
/// 输出当前阅读器状态日志(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)")
|
|
}
|
|
|
|
/// 计算文本分页的页面尺寸。
|
|
/// 分页器使用完整 viewport,真实可绘制区域由 layoutConfig.edgeInsets 控制,
|
|
/// 以保持与页面展示时的内容几何一致。
|
|
private func currentTextPageSize() -> CGSize {
|
|
UIScreen.main.bounds.size
|
|
}
|
|
|
|
/// 根据当前配置生成文本渲染样式
|
|
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
|
|
let font = UIFont.systemFont(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 ""
|
|
}
|
|
}
|
|
|
|
/// 翻页模式扩展:提供 Demo 命令行参数值
|
|
private extension RDReaderView.DisplayType {
|
|
/// 返回翻页模式对应的 Demo 命令行参数字符串
|
|
var demoArgumentValue: String {
|
|
switch self {
|
|
case .pageCurl:
|
|
return "pageCurl"
|
|
case .horizontalScroll:
|
|
return "horizontalScroll"
|
|
case .verticalScroll:
|
|
return "verticalScroll"
|
|
}
|
|
}
|
|
}
|