feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化

- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
This commit is contained in:
shenlei
2026-06-22 20:26:34 +08:00
parent f50495ad91
commit c65c190b71
178 changed files with 11380 additions and 6728 deletions
@@ -1,27 +1,18 @@
import UIKit
/// UIPageViewController
///
/// RDReaderView pageCurl
struct RDReaderPagingController {
/// pageCurl
struct PageTransitionRequest: Equatable {
let pageNum: Int
let animated: Bool
}
/// pageCurl
var pendingTransitionRequest: PageTransitionRequest?
///
var isTransitioning: Bool = false
/// UI
var didBuildUI = false
// MARK: - PageViewController
/// spine UIPageViewController
static func createPageViewController(isDualPage: Bool) -> UIPageViewController {
let options: [UIPageViewController.OptionsKey: Any]?
if isDualPage {
@@ -34,16 +25,12 @@ struct RDReaderPagingController {
return pageVC
}
// MARK: -
/// pageCurl
mutating func shouldQueuePageTransition(_ request: PageTransitionRequest, currentDisplayType: RDReaderView.DisplayType) -> Bool {
guard currentDisplayType == .pageCurl, isTransitioning else { return false }
pendingTransitionRequest = request
return true
}
/// pageCurl
mutating func finishPageCurlTransition() -> PageTransitionRequest? {
isTransitioning = false
guard let pending = pendingTransitionRequest else { return nil }
@@ -51,7 +38,6 @@ struct RDReaderPagingController {
return pending
}
///
mutating func resetPendingState() {
isTransitioning = false
pendingTransitionRequest = nil
@@ -1,49 +1,52 @@
//
// RDReaderPreloadController.swift
// ReadViewSDK
//
//
//
import UIKit
///
///
/// 仿
final class RDReaderPreloadController {
/// 1
var radius: Int = 1
private let preloadHostView = UIView()
private var preloadedPageViews: [Int: UIView] = [:]
private var pageCurlCachedViews: [Int: UIView] = [:]
private var cacheSignature: CacheSignature?
///
struct Environment {
let displayType: RDReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
let landscapeDualPageEnabled: Bool
let coverPageIndex: Int?
let totalPages: Int
let spreadResolver: RDReaderSpreadResolver
}
private struct CacheSignature: Equatable {
let displayType: RDReaderView.DisplayType
let isLandscape: Bool
let pagesPerScreen: Int
let boundsSize: CGSize
}
/// 宿 frame
func setHostFrame(_ frame: CGRect) {
preloadHostView.frame = frame
}
/// 宿
func ensureHostView(in parentView: UIView) {
guard preloadHostView.superview == nil else { return }
preloadHostView.isHidden = true
@@ -54,12 +57,10 @@ final class RDReaderPreloadController {
parentView.insertSubview(preloadHostView, at: 0)
}
///
func initializeSignature(_ environment: Environment) {
cacheSignature = currentCacheSignature(environment)
}
///
func invalidate(environment: Environment) {
pageCurlCachedViews.values.forEach { $0.removeFromSuperview() }
preloadedPageViews.values.forEach { $0.removeFromSuperview() }
@@ -68,7 +69,6 @@ final class RDReaderPreloadController {
cacheSignature = currentCacheSignature(environment)
}
///
func pageViewForDisplay(
pageNum: Int,
environment: Environment,
@@ -80,14 +80,12 @@ final class RDReaderPreloadController {
return view
}
///
func takePreloadedView(for pageNum: Int) -> UIView? {
let preloaded = preloadedPageViews.removeValue(forKey: pageNum)
preloaded?.removeFromSuperview()
return preloaded
}
///
func prime(
around pageNum: Int,
preferredForward: Bool? = nil,
@@ -1,17 +1,8 @@
//
// RDReaderSpreadResolver.swift
// ReadViewSDK
//
// spread
//
import Foundation
///
///
///
struct RDReaderSpreadResolver {
///
func isFullScreenPage(
_ pageNum: Int,
landscapeDualPageEnabled: Bool,
@@ -22,8 +13,6 @@ struct RDReaderSpreadResolver {
return pageNum == coverIndex
}
/// (, ?)
///
func dualPagePair(
for pageNum: Int,
totalPages: Int,
@@ -45,7 +34,6 @@ struct RDReaderSpreadResolver {
return (left, right)
}
///
func adjacentDualPage(
from pageNum: Int,
totalPages: Int,
@@ -63,7 +51,6 @@ struct RDReaderSpreadResolver {
return dualPagePair(for: prevEnd, totalPages: totalPages, coverPageIndex: coverPageIndex).left
}
/// /
func nextPage(
from currentPage: Int,
totalPages: Int,
@@ -1,20 +1,8 @@
//
// RDReaderTapRegionHandler.swift
// ReadViewSDK
//
//
//
import CoreGraphics
///
///
///
struct RDReaderTapRegionHandler {
///
/// - 1/3 center
/// - 1/3
/// - 1/3 center
func resolveTapEvent(
point: CGPoint,
viewFrame: CGRect,
@@ -1,29 +1,10 @@
//
// RDReaderContentCell.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/7.
//
// UICollectionViewCell 宿
// cell containerView
// RDReaderDataSource.pageContentView() containerView
//
// RDReaderView cell UICollectionView 使
//
import UIKit
/// cell 宿
/// UICollectionView
/// ``containerView`` layoutSubviews
/// frame contentView
class RDReaderContentCell: UICollectionViewCell {
///
private var _containerView: UIView? = nil
///
/// contentView
///
var containerView: UIView? {
set {
guard newValue !== _containerView else { return }
@@ -39,8 +20,6 @@ class RDReaderContentCell: UICollectionViewCell {
}
}
///
/// containerView frame contentView
override func layoutSubviews() {
super.layoutSubviews()
_containerView?.frame = CGRect(x: 0, y: 0, width: contentView.frame.width, height: contentView.frame.height)
@@ -1,56 +1,24 @@
//
// RDReaderFlowLayout.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/6.
//
// UICollectionViewFlowLayout
// item +
//
//
// RDReaderView RDReaderView
//
import UIKit
///
///
public protocol RDReaderFlowLayoutDataSoure: NSObjectProtocol {
///
/// - Parameters:
/// - flowLayout:
/// - pageIndex:
/// - Returns: nil 使collectionView.frame.height
func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat?
}
///
///
@objc public protocol RDReaderFlowLayoutDelegate: NSObjectProtocol {
///
/// - Parameters:
/// - flowLayout:
/// - pageIndex:
func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int)
}
///
/// - horizontalScroll12
/// - verticalScroll
///
/// ``displayType``
/// ``isLandscapeDualPage`` ``coverPageIndex``
public class RDReaderFlowLayout: UICollectionViewFlowLayout {
/// 使
var displayType: RDReaderView.DisplayType = .horizontalScroll {
didSet {
invalidateLayout()
}
}
///
///
var isLandscapeDualPage: Bool = false {
didSet {
if oldValue != isLandscapeDualPage {
@@ -59,8 +27,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
///
///
var coverPageIndex: Int? = nil {
didSet {
if oldValue != coverPageIndex {
@@ -69,11 +35,8 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
/// prepare bounds
private var lastPreparedBoundsSize: CGSize = .zero
///
/// +21
var pagesPerScreen: Int {
guard isLandscapeDualPage else { return 1 }
switch displayType {
@@ -87,24 +50,14 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
///
weak var dataSource: RDReaderFlowLayoutDataSoure? = nil
///
weak var delegate: RDReaderFlowLayoutDelegate? = nil
///
private var hasCoverPageInDualMode: Bool {
return pagesPerScreen > 1 && coverPageIndex != nil
}
/// item frame
///
/// - Parameters:
/// - index: item
/// - screenWidth:
/// - halfWidth:
/// - height:
/// - Returns: item frame
private func coverAwareFrame(for index: Int, screenWidth: CGFloat, halfWidth: CGFloat, height: CGFloat) -> CGRect {
guard let coverIndex = coverPageIndex else {
let pairIdx = index / 2
@@ -125,11 +78,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
return CGRect(x: x, y: 0, width: halfWidth, height: height)
}
/// item
/// - Parameters:
/// - offset:
/// - screenWidth:
/// - Returns: item
private func coverAwareStartIndex(for offset: CGFloat, screenWidth: CGFloat) -> Int {
let pps = pagesPerScreen
guard let coverIdx = coverPageIndex, hasCoverPageInDualMode else {
@@ -147,7 +95,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
/// delegate
private var currentPage: Int = 0 {
didSet {
@@ -157,15 +104,12 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
///
/// - Parameter displayType:
init(displayType: RDReaderView.DisplayType) {
self.displayType = displayType
super.init()
}
/// item
public override func prepare() {
super.prepare()
guard let collectionView = self.collectionView else {
@@ -203,8 +147,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
/// bounds 使
/// true true
public override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if newBounds.size != lastPreparedBoundsSize {
return true
@@ -219,9 +161,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
/// collectionView
///
/// +
public override var collectionViewContentSize: CGSize {
var size = super.collectionViewContentSize
guard let collectionView = collectionView else {
@@ -243,9 +182,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
return size
}
///
/// item
/// cell
public override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributes = super.layoutAttributesForElements(in: rect)
guard let collectionView = collectionView else {
@@ -279,7 +215,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
})
}
if self.displayType == .horizontalScroll {
let pps = pagesPerScreen
@@ -325,10 +260,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
return attributes
}
/// contentOffset
///
/// - Parameter count:
/// - Returns: contentOffset
func currentContentOffset(count: Int) -> CGPoint {
guard let collectionView = collectionView else {
return .zero
@@ -368,7 +299,6 @@ public class RDReaderFlowLayout: UICollectionViewFlowLayout {
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -1,33 +1,12 @@
//
// RDReaderGestureController.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
//
//
//
// RDReaderView tapGestureRecognizer
//
// RDReaderView
//
import UIKit
///
///
/// RDReaderView 使
class RDReaderGestureController: UIViewController {
///
var topToolView: UIView?
///
var bottomToolView: UIView?
///
/// - Parameters:
/// - topToolView:
/// - bottomToolView:
init(topToolView: UIView?, bottomToolView: UIView?) {
self.topToolView = topToolView
self.bottomToolView = bottomToolView
@@ -41,18 +20,6 @@ class RDReaderGestureController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
@@ -1,28 +1,10 @@
//
// RDReaderPageChildViewController.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/9.
//
// UIPageViewController
// pageCurl 仿 RDReaderPageChildViewController
// contentContainerView
//
// RDReaderView UIPageViewController 使
//
import UIKit
/// 仿
/// UIPageViewController
/// ``contentView`` ``pageNum``
/// contentContainerView
class RDReaderPageChildViewController: UIViewController {
/// contentView
private let contentContainerView = UIView()
///
/// contentContainerView
var contentView: UIView? {
didSet {
guard isViewLoaded else { return }
@@ -30,21 +12,14 @@ class RDReaderPageChildViewController: UIViewController {
}
}
///
var pageNum: Int = 0
///
/// - Parameters:
/// - contentView:
/// - pageNum: 0
init(contentView: UIView?, pageNum: Int = 0) {
self.contentView = contentView
self.pageNum = pageNum
super.init(nibName: nil, bundle: nil)
}
///
/// contentContainerView
override func loadView() {
view = UIView()
view.backgroundColor = .clear
@@ -61,11 +36,8 @@ class RDReaderPageChildViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
/// contentContainerView
/// contentContainerView contentView
private func installContentView() {
contentContainerView.subviews.forEach { $0.removeFromSuperview() }
guard let contentView else { return }
@@ -75,15 +47,4 @@ class RDReaderPageChildViewController: UIViewController {
contentContainerView.addSubview(contentView)
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}
@@ -1,18 +1,8 @@
//
// RDReaderView+CollectionView.swift
// ReadViewSDK
//
// UICollectionView
//
import UIKit
/// UICollectionView
///
extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure {
/// UICollectionViewCell
/// DataSource cell
/// RTL cell 使
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let identifer = pageReuseIdentifier(for: indexPath.row) {
@@ -20,7 +10,7 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
let preloadedView = preloadController.takePreloadedView(for: indexPath.row)
let reusableView = cell.containerView ?? preloadedView
cell.containerView = contentViewForPage(indexPath.row, reusableView: reusableView)
// RTL cell 使collectionView
if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll {
cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1)
} else {
@@ -34,12 +24,10 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
return cell
}
/// item
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numberOfPages()
}
///
public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) {
if currentPage >= 0, currentPage != pageIndex {
predictedPageDirection = pageIndex >= currentPage
@@ -48,8 +36,6 @@ extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate,
primePageCache(around: pageIndex, preferredForward: predictedPageDirection)
}
///
/// nil 使
public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? {
return nil
}
@@ -1,23 +1,13 @@
//
// RDReaderView+ContentAccess.swift
// ReadViewSDK
//
// 访 UITableView register/dequeueReusable
//
import UIKit
///
/// UITableView register/dequeueReusable
extension RDReaderView {
///
public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) {
contentViews[identifier] = contentView
collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier)
}
///
/// cell
public func dequeueReusableContentView(withReuseIdentifier identifier: String, for pageNum: Int) -> UIView {
if self.currentDisplayType != .pageCurl, let cell = self.collectionView.cellForItem(at: IndexPath(row: pageNum, section: 0)) as? RDReaderContentCell, let containerView = cell.containerView {
return containerView
@@ -28,8 +18,6 @@ extension RDReaderView {
return contentView
}
///
/// pageCurl PageViewController CollectionView cell
public func pageContentView(pageNum: Int) -> UIView? {
if currentDisplayType == .pageCurl {
return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView
@@ -39,8 +27,6 @@ extension RDReaderView {
}
}
///
/// 使退
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize {
let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil)
if let targetPage,
@@ -1,18 +1,8 @@
//
// RDReaderView+PageCurl.swift
// ReadViewSDK
//
// UIPageViewController 仿
//
import UIKit
/// UIPageViewController
/// 仿
extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate {
///
///
private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController {
if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum {
return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum)
@@ -21,12 +11,9 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum)
}
/// ""
///
private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? {
let totalPages = numberOfPages()
//
if pageNum == RDReaderView.blankEndPageNum {
return nil
}
@@ -46,7 +33,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return next
}
// pageNum
if isDualPage {
if let coverIndex = coverPageIndex {
let adjustedIndex = pageNum - (coverIndex + 1)
@@ -62,8 +48,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return nil
}
/// ""
///
private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? {
if pageNum == RDReaderView.blankEndPageNum {
let totalPages = numberOfPages()
@@ -81,8 +65,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return prev >= 0 ? prev : nil
}
/// UIPageViewController /
/// RTL before/after before=
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
let isDualPage = landscapeDualPageEnabled && isLandscape
@@ -98,8 +80,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return targetVC
}
/// UIPageViewController /
/// RTL before/after
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? RDReaderPageChildViewController else { return nil }
let isDualPage = landscapeDualPageEnabled && isLandscape
@@ -115,8 +95,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
return targetVC
}
/// UIPageViewController
/// PageViewController
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed, let firstVC = pageViewController.viewControllers?.first as? RDReaderPageChildViewController {
let pn = firstVC.pageNum
@@ -130,8 +108,6 @@ extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDele
finishPageCurlTransition()
}
/// UIPageViewController
///
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
pagingController.isTransitioning = true
willTransitionToViewController = pendingViewControllers.first
@@ -1,16 +1,8 @@
//
// RDReaderView+ToolView.swift
// ReadViewSDK
//
// /
//
import UIKit
/// /
extension RDReaderView {
/// /
func tapCenter() {
refreshToolViewsFromProviderIfNeeded()
isShowToolView = !isShowToolView
@@ -58,7 +50,6 @@ extension RDReaderView {
onToolViewVisibilityChanged?(isShowToolView)
}
///
func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool {
if toolView.frame.contains(point) {
return true
@@ -68,15 +59,13 @@ extension RDReaderView {
return hitView === toolView || hitView.isDescendant(of: toolView)
}
///
enum ToolViewPosition {
///
case top
///
case bottom
}
///
func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) {
guard toolView.superview !== self else { return }
@@ -109,7 +98,6 @@ extension RDReaderView {
}
}
/// safeAreaInsets
func updateToolViewHeightConstraintsIfNeeded() {
topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top)
bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom)
+146 -171
View File
@@ -1,58 +1,19 @@
//
// RDReaderView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/6.
//
// RDReaderView
// UIView RDReaderView
// 1. 仿
// 2. 1/31/31/3
// 3.
// 4. (LTR)(RTL)
// 5. DataSource/Delegate
// 6.
//
// RDReaderView
// EPUBCore EPUBTextRendering RDReaderView EPUBUI UI
//
import UIKit
///
/// RDReaderView
///
///
/// 使
/// 1. ``dataSource``
/// 2. ``delegate``
/// 3. ``switchReaderDisplayType(_:)``
/// 4. ``reloadData()``
///
///
/// - pageCurl / horizontalScroll / verticalScroll
/// -
/// - RTL
/// - //
/// -
public class RDReaderView: UIView {
///
///
enum TapEvent {
///
case none
/// 1/3RTL
case left
/// 1/3/
case center
/// 1/3RTL
case right
}
/// 仿使 UIPageViewController
/// pageCurl 仿
lazy var pageViewController: UIPageViewController = {
let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
pageVC.delegate = self
@@ -60,7 +21,6 @@ public class RDReaderView: UIView {
return pageVC
}()
///
lazy var layout: RDReaderFlowLayout = {
let layout = RDReaderFlowLayout(displayType: .horizontalScroll)
layout.dataSource = self
@@ -68,8 +28,6 @@ public class RDReaderView: UIView {
return layout
}()
/// 使 UICollectionView
/// horizontalScroll verticalScroll
lazy var collectionView: UICollectionView = {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
@@ -77,14 +35,15 @@ public class RDReaderView: UIView {
collectionView.accessibilityIdentifier = "epub.reader.paging"
return collectionView
}()
private let spreadResolver = RDReaderSpreadResolver()
private let tapRegionHandler = RDReaderTapRegionHandler()
let preloadController = RDReaderPreloadController()
var pagingController = RDReaderPagingController()
///
/// delegate
/// -1
public var currentPage: Int = -1 {
didSet {
if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDReaderDelegate.pageNum(readerView:pageNum:))) {
@@ -93,16 +52,12 @@ public class RDReaderView: UIView {
}
}
///
///
private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = {
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:)))
tap.delegate = self
return tap
}()
///
///
private var tapEvent: TapEvent = .none {
didSet {
let isRTL = pageDirection == .rightToLeft
@@ -123,8 +78,6 @@ public class RDReaderView: UIView {
}
}
///
/// 使 adjacentDualPage +1
private func goNextPage() {
let totalPages = numberOfPages()
if let target = spreadResolver.nextPage(
@@ -138,8 +91,6 @@ public class RDReaderView: UIView {
}
}
///
/// 使 adjacentDualPage -1
private func goPreviousPage() {
let totalPages = numberOfPages()
if let target = spreadResolver.nextPage(
@@ -154,30 +105,29 @@ public class RDReaderView: UIView {
}
private let legacyDataSourceAdapter = RDReaderLegacyDataSourceAdapter()
///
public weak var dataSource: RDReaderDataSource? {
didSet {
legacyDataSourceAdapter.dataSource = dataSource
}
}
/// ``dataSource``
public weak var pageProvider: RDReaderPageProvider?
///
public weak var delegate: RDReaderDelegate? = nil
/// 仿
public var currentDisplayType: RDReaderView.DisplayType = .pageCurl
/// / 0.3
public var toolViewAnimationDuration: TimeInterval = 0.3
///
var onToolViewVisibilityChanged: ((Bool) -> Void)?
/// tapCenter
var searchBarView: UIView?
///
public var landscapeDualPageEnabled: Bool = false
/// /
public var pageDirection: RDReaderView.PageDirection = .leftToRight
///
/// nil 0+1, 2+3, 4+5...
public var coverPageIndex: Int? = nil
private var resolvedPageProvider: RDReaderPageProvider? {
@@ -213,27 +163,20 @@ public class RDReaderView: UIView {
}
}
///
private var hasCoverPage: Bool {
return coverPageIndex != nil
}
/// >
var isLandscape: Bool {
return bounds.width > bounds.height
}
///
/// 12
public var pagesPerScreen: Int {
if !landscapeDualPageEnabled { return 1 }
if currentDisplayType == .verticalScroll { return 1 }
return isLandscape ? 2 : 1
}
///
/// - Parameter pageNum:
/// - Returns:
public func isFullScreenPage(_ pageNum: Int) -> Bool {
spreadResolver.isFullScreenPage(
pageNum,
@@ -243,10 +186,6 @@ public class RDReaderView: UIView {
)
}
///
///
/// - Parameter pageNum:
/// - Returns: (, (nil))
private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) {
let totalPages = numberOfPages()
return spreadResolver.dualPagePair(
@@ -256,11 +195,6 @@ public class RDReaderView: UIView {
)
}
/// /
/// - Parameters:
/// - pageNum:
/// - forward: true=false=
/// - Returns: nil
private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? {
let totalPages = numberOfPages()
return spreadResolver.adjacentDualPage(
@@ -271,60 +205,63 @@ public class RDReaderView: UIView {
)
}
///
private func clampedPageNumber(_ pageNum: Int) -> Int? {
let totalPages = numberOfPages()
guard totalPages > 0 else { return nil }
return min(max(pageNum, 0), totalPages - 1)
}
/// layoutSubviews
private var previousIsLandscape: Bool?
/// key
var contentViews = [String : UIView.Type]()
/// pageCurl
var willPreviousTransitionToViewController: UIViewController? = nil
/// pageCurl
var willNextTransitionToViewController: UIViewController? = nil
/// pageCurl
var willTransitionToViewController: UIViewController? = nil
///
var topToolView: UIView?
///
var bottomToolView: UIView?
///
var topToolViewHeightConstraint: NSLayoutConstraint?
///
var bottomToolViewHeightConstraint: NSLayoutConstraint?
///
var isShowToolView: Bool = false
///
private var isTransitioning: Bool {
get { pagingController.isTransitioning }
set { pagingController.isTransitioning = newValue }
}
/// UI
private var didBuildUI: Bool {
get { pagingController.didBuildUI }
set { pagingController.didBuildUI = newValue }
}
/// true=false=
var predictedPageDirection: Bool?
/// 1
public var preloadRadius: Int {
get { preloadController.radius }
set { preloadController.radius = newValue }
}
/// pageCurl
static let blankPageNum = Int.max
/// pageCurl
static let blankEndPageNum = Int.max - 1
/// pageCurl
private typealias PageTransitionRequest = RDReaderPagingController.PageTransitionRequest
private let registeredSelectionLongPressRecognizers = NSHashTable<UILongPressGestureRecognizer>.weakObjects()
private let selectionTapSuppressedContentViews = NSHashTable<UIView>.weakObjects()
private let selectionPagingSuppressedContentViews = NSHashTable<UIView>.weakObjects()
private var isPagingInteractionSuppressed = false
private var pendingTransitionRequest: PageTransitionRequest? {
get { pagingController.pendingTransitionRequest }
set { pagingController.pendingTransitionRequest = newValue }
@@ -336,8 +273,6 @@ public class RDReaderView: UIView {
isAccessibilityElement = false
}
///
///
public override func layoutSubviews() {
super.layoutSubviews()
guard bounds.width > 0, bounds.height > 0 else { return }
@@ -346,8 +281,7 @@ public class RDReaderView: UIView {
let nowLandscape = isLandscape
if let prev = previousIsLandscape, prev != nowLandscape {
previousIsLandscape = nowLandscape
// RunLoop layoutSubviews reloadData/layoutIfNeeded
// collectionView
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.orientationChanged(isNowLandscape: nowLandscape)
@@ -358,33 +292,28 @@ public class RDReaderView: UIView {
}
}
///
///
/// - pageCurl UIPageViewController spineLocation
/// -
/// - Parameter isNowLandscape:
private func orientationChanged(isNowLandscape: Bool) {
let savedPage = max(0, currentPage)
// 1.
delegate?.readerViewOrientationWillChange?(readerView: self, isLandscape: isNowLandscape)
// 2.
layout.isLandscapeDualPage = landscapeDualPageEnabled && isNowLandscape
layout.coverPageIndex = coverPageIndex
switch currentDisplayType {
case .pageCurl:
// 仿 spineLocation PageViewController
rebuildPageViewController()
transitionToPage(pageNum: savedPage)
primePageCache(around: savedPage, preferredForward: predictedPageDirection)
default:
//
UIView.performWithoutAnimation {
// 使
collectionView.collectionViewLayout.invalidateLayout()
collectionView.reloadData()
collectionView.layoutIfNeeded()
// 3.
let totalPages = numberOfPages()
let safePage = min(savedPage, max(0, totalPages - 1))
let targetOffset = layout.currentContentOffset(count: safePage)
@@ -394,9 +323,6 @@ public class RDReaderView: UIView {
}
}
/// spine UIPageViewController
/// - Parameter isDualPage: .mid
/// - Returns: UIPageViewController
private func createPageViewController(isDualPage: Bool) -> UIPageViewController {
let options: [UIPageViewController.OptionsKey: Any]?
if isDualPage {
@@ -411,9 +337,6 @@ public class RDReaderView: UIView {
return pageVC
}
/// UIPageViewController
/// spineLocation
/// PageViewController
private func rebuildPageViewController() {
detachPageViewControllerIfNeeded()
@@ -423,12 +346,6 @@ public class RDReaderView: UIView {
attachPageViewControllerIfNeeded()
}
// MARK: - UIPageViewController Fault Detection
/// UIPageViewController
///
/// - Parameter pageVC: UIPageViewController
/// - Returns: true
private func detectPageViewControllerFault(_ pageVC: UIPageViewController) -> Bool {
guard currentDisplayType == .pageCurl else { return false }
let expectedCount = (landscapeDualPageEnabled && isLandscape) ? 2 : 1
@@ -453,8 +370,6 @@ public class RDReaderView: UIView {
|| childViewControllers[1].pageNum != expectedRightPage
}
/// UIPageViewController
/// PageViewController
private func patchPageViewControllerFault() {
guard currentPage >= 0 else { return }
DispatchQueue.main.async { [weak self] in
@@ -469,12 +384,10 @@ public class RDReaderView: UIView {
}
}
/// pageCurl
private func shouldQueuePageTransition(_ request: PageTransitionRequest) -> Bool {
pagingController.shouldQueuePageTransition(request, currentDisplayType: currentDisplayType)
}
/// pageCurl
func finishPageCurlTransition(repairFaultIfNeeded: Bool = true) {
willPreviousTransitionToViewController = nil
willNextTransitionToViewController = nil
@@ -490,8 +403,6 @@ public class RDReaderView: UIView {
}
}
// MARK: - Preload / Forecast
private var preloadEnvironment: RDReaderPreloadController.Environment {
RDReaderPreloadController.Environment(
displayType: currentDisplayType,
@@ -531,7 +442,6 @@ public class RDReaderView: UIView {
contentViewForPage(pageNum, reusableView: reusableView)
}
/// UI
public override func didMoveToSuperview() {
super.didMoveToSuperview()
@@ -540,8 +450,6 @@ public class RDReaderView: UIView {
}
}
/// pageViewController
///
private func attachPageViewControllerIfNeeded() {
guard let parentViewController = self.ss_superViewController else { return }
@@ -571,8 +479,6 @@ public class RDReaderView: UIView {
}
}
/// pageViewController
/// willMove/didMove
private func detachPageViewControllerIfNeeded() {
if pageViewController.parent != nil {
pageViewController.willMove(toParent: nil)
@@ -583,8 +489,6 @@ public class RDReaderView: UIView {
}
}
/// UI
/// cell
private func makeUI() {
guard !didBuildUI else {
if currentDisplayType == .pageCurl {
@@ -606,21 +510,80 @@ public class RDReaderView: UIView {
preloadController.initializeSignature(preloadEnvironment)
addGestureRecognizer(tapGestureRecognizer)
// touchUpInside
tapGestureRecognizer.cancelsTouchesInView = false
}
///
/// tapEvent
@objc private func tapAction(tap: UITapGestureRecognizer) {
let point = tap.location(in: tap.view)
let hitView = hitTest(point, with: nil)
if containsTextContentView(in: hitView) {
return
}
if shouldSuppressChromeToggle(for: hitView, point: point) {
return
}
handleResolvedTap(at: point, hitView: hitView, in: tap.view)
}
private func shouldSuppressChromeToggle(for hitView: UIView?, point: CGPoint) -> Bool {
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
return true
}
var currentView = hitView
while let view = currentView {
if let textContentView = view as? RDEPUBTextContentView {
let localPoint = convert(point, to: textContentView)
return textContentView.shouldSuppressReaderTap(at: localPoint)
}
currentView = view.superview
}
return false
}
func registerSelectionGestureDependenciesIfNeeded(for contentView: UIView) {
guard let textContentView = contentView as? RDEPUBTextContentView else { return }
let longPressGesture = textContentView.selectionLongPressGestureRecognizer
if registeredSelectionLongPressRecognizers.allObjects.contains(where: { $0 === longPressGesture }) {
return
}
tapGestureRecognizer.require(toFail: longPressGesture)
registeredSelectionLongPressRecognizers.add(longPressGesture)
}
func updateSelectionTapSuppression(for contentView: UIView, isSuppressed: Bool) {
if isSuppressed {
if selectionTapSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
selectionTapSuppressedContentViews.add(contentView)
}
} else {
selectionTapSuppressedContentViews.remove(contentView)
}
}
func updateSelectionPagingSuppression(for contentView: UIView, isSuppressed: Bool) {
if isSuppressed {
if selectionPagingSuppressedContentViews.allObjects.contains(where: { $0 === contentView }) == false {
selectionPagingSuppressedContentViews.add(contentView)
}
} else {
selectionPagingSuppressedContentViews.remove(contentView)
}
updatePagingInteractionSuppression()
}
func handleContentTap(at point: CGPoint, in sourceView: UIView) {
let localPoint = sourceView.convert(point, to: self)
handleResolvedTap(at: localPoint, hitView: nil, in: self)
}
private func handleResolvedTap(at point: CGPoint, hitView: UIView?, in tapView: UIView?) {
if isShowToolView {
let hitView = hitTest(point, with: nil)
if let top = topToolView, isHitView(hitView, inside: top, point: point) { return }
if let bottom = bottomToolView, isHitView(hitView, inside: bottom, point: point) { return }
}
guard let viewFrame = tap.view?.frame else { return }
guard let viewFrame = tapView?.frame else { return }
tapEvent = tapRegionHandler.resolveTapEvent(
point: point,
viewFrame: viewFrame,
@@ -628,9 +591,29 @@ public class RDReaderView: UIView {
)
}
/// 仿//
/// PageViewController CollectionView
/// - Parameter displayType:
private func containsTextContentView(in hitView: UIView?) -> Bool {
var currentView = hitView
while let view = currentView {
if view is RDEPUBTextContentView {
return true
}
currentView = view.superview
}
return false
}
private func updatePagingInteractionSuppression() {
let shouldSuppress = selectionPagingSuppressedContentViews.allObjects.isEmpty == false
guard shouldSuppress != isPagingInteractionSuppressed else { return }
isPagingInteractionSuppressed = shouldSuppress
setPagingInteractionEnabled(!shouldSuppress)
}
private func setPagingInteractionEnabled(_ isEnabled: Bool) {
collectionView.isScrollEnabled = isEnabled
pageViewController.gestureRecognizers.forEach { $0.isEnabled = isEnabled }
}
public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType) {
let previousDisplayType = currentDisplayType
self.currentDisplayType = displayType
@@ -640,7 +623,7 @@ public class RDReaderView: UIView {
if previousDisplayType != displayType {
invalidatePageCaches()
}
//
layout.isLandscapeDualPage = landscapeDualPageEnabled && isLandscape
layout.coverPageIndex = coverPageIndex
switch displayType {
@@ -653,13 +636,13 @@ public class RDReaderView: UIView {
primePageCache(around: currentPage, preferredForward: predictedPageDirection)
default:
detachPageViewControllerIfNeeded()
// RTL collectionView
if pageDirection == .rightToLeft && displayType != .verticalScroll {
collectionView.transform = CGAffineTransform(scaleX: -1, y: 1)
} else {
collectionView.transform = .identity
}
// collectionView frame
collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
insertSubview(self.collectionView, at: 0)
layout.displayType = displayType
@@ -668,13 +651,6 @@ public class RDReaderView: UIView {
}
}
///
///
/// - pageCurl UIPageViewController.setViewControllers
/// - setContentOffset
/// - Parameters:
/// - pageNum: item
/// - animated:
public func transitionToPage(pageNum: Int, animated: Bool = false) {
guard let safePageNum = clampedPageNumber(pageNum) else { return }
switch currentDisplayType {
@@ -685,7 +661,7 @@ public class RDReaderView: UIView {
}
let isDualPage = landscapeDualPageEnabled && isLandscape
// RTL
let direction: UIPageViewController.NavigationDirection
if pageDirection == .rightToLeft {
direction = safePageNum > currentPage ? .reverse : .forward
@@ -707,7 +683,7 @@ public class RDReaderView: UIView {
self.finishPageCurlTransition()
}
} else {
//
let blankNum = isFullScreenPage(pair.left) ? RDReaderView.blankPageNum : RDReaderView.blankEndPageNum
let emptyVC = RDReaderPageChildViewController(contentView: UIView(), pageNum: blankNum)
pageViewController.setViewControllers([leftVC, emptyVC], direction: animated ? direction : .forward, animated: animated) { [weak self] _ in
@@ -738,22 +714,18 @@ public class RDReaderView: UIView {
}
}
///
///
public func reloadData() {
switchReaderDisplayType(currentDisplayType)
topToolView = resolvedTopChromeView()
bottomToolView = resolvedBottomChromeView()
}
///
public func reloadPageCountOnly() {
collectionView.reloadData()
topToolView = resolvedTopChromeView()
bottomToolView = resolvedBottomChromeView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@@ -774,7 +746,14 @@ extension RDReaderView: UIGestureRecognizerDelegate {
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
guard gestureRecognizer === tapGestureRecognizer else { return true }
if selectionTapSuppressedContentViews.allObjects.isEmpty == false {
return false
}
let point = touch.location(in: self)
if containsTextContentView(in: touch.view) {
return false
}
if let topToolView, isHitView(touch.view, inside: topToolView, point: point) {
return false
}
@@ -795,14 +774,10 @@ extension RDReaderView: UIGestureRecognizerDelegate {
}
}
private var cellViewKey: Int8 = 0
/// UIView UIViewController
extension UIView {
/// 沿 UIViewController
///
var ss_superViewController: UIViewController? {
var next = self.next
while next != nil {
@@ -1,91 +1,69 @@
//
// RDReaderViewProtocols.swift
// ReadViewSDK
//
//
//
import UIKit
///
/// RDReaderView
/// RDReaderView
@objc public protocol RDReaderDataSource: NSObjectProtocol {
///
func pageCountOfReaderView(readerView: RDReaderView) -> Int
///
func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView
/// UICollectionViewCell
func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String?
/// /
@objc optional func topToolView(readerView: RDReaderView) -> UIView?
/// /
@objc optional func bottomToolView(readerView: RDReaderView) -> UIView?
}
///
/// EPUB ``RDReaderDataSource``便 PDF
@objc public protocol RDReaderPageProvider: NSObjectProtocol {
///
func numberOfPages(in readerView: RDReaderView) -> Int
/// reusableView
func readerView(_ readerView: RDReaderView, viewForPageAt index: Int, reusableView: UIView?) -> UIView
/// UICollectionViewCell
@objc optional func pageIdentifier(in readerView: RDReaderView, index: Int) -> String?
///
@objc optional func readerViewTopChrome(_ readerView: RDReaderView) -> UIView?
///
@objc optional func readerViewBottomChrome(_ readerView: RDReaderView) -> UIView?
}
///
///
@objc public protocol RDReaderDelegate: NSObjectProtocol {
///
func pageNum(readerView: RDReaderView, pageNum: Int)
///
@objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool)
}
///
///
public protocol RDReaderPageNavigating: AnyObject {
///
var currentPage: Int { get }
///
func reloadPages()
///
func transition(to page: Int, animated: Bool)
}
// MARK: -
extension RDReaderView {
/// RDReaderView 使
public enum DisplayType {
/// 仿使 UIPageViewController
case pageCurl
/// 使 UICollectionView +
case horizontalScroll
/// 使 UICollectionView
case verticalScroll
}
///
public enum PageDirection {
/// /
case leftToRight
///
case rightToLeft
}
}
// MARK: - Legacy
///
/// ``RDReaderDataSource`` ``RDReaderPageProvider``
final class RDReaderLegacyDataSourceAdapter: NSObject, RDReaderPageProvider {
///
weak var dataSource: RDReaderDataSource?
func numberOfPages(in readerView: RDReaderView) -> Int {