Files
ReadViewSDK/Sources/RDPDFReaderView/Sources/RDPDFReaderKitView.swift
T
shenleiandClaude Fable 5 1422461224 将 PDF 成品阅读控制器下沉为 RDPDFReaderView SDK,并修复手机横屏单页连续竖滑
SDK 化收尾:
- 新增 RDPDFReaderView 本地 pod(阅读容器、成品控制器、划线/注释、画笔、
  目录/设置面板、主题与持久化契约),Demo 精简为 PDFKit 页面提供者 + 持久化宿主
- Podfile/工程接入 RDPDFReaderView 与 SnapKit,示例书改用 PDF 阅读器示例文件

手机横屏单页竖滑修复:
- cell 高度改为宽度适配后的纸张高度(sizeForItemAt + 宿主按页提供纵横比),
  纵向超出一屏的内容由外层 collectionView 连续滚动承接,不再被裁掉
- 页面内部不再开启同向嵌套的兜底竖向拖动,修复外层滚动被吞、无法滚到下一页
- 修复 applyContentSize 先评估拖动开关后写 contentSize 的顺序缺陷
- 页码按视口中心命中 cell 计算,补 didEndDragging 兜底;旋转重锚点与
  transitionToPage 改用真实布局位置
- 横竖屏同为竖滑时也重建 cell 并刷新全部可见页的适配配置
- 横屏单页点击不再跳页,只显隐工具栏;翻页交给连续竖滑

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 18:49:44 +09:00

151 lines
5.6 KiB
Swift

import UIKit
public protocol RDPDFReaderKitViewDataSource: AnyObject {
func numberOfPages(in readerView: RDPDFReaderKitView) -> Int
func readerView(_ readerView: RDPDFReaderKitView, imageForPageAt index: Int) -> UIImage?
}
public protocol RDPDFReaderKitViewDelegate: AnyObject {
func readerView(_ readerView: RDPDFReaderKitView, didMoveToPage index: Int)
}
/// 独立 PDF 阅读容器:仅处理分页和页面缩放,不依赖主工程的下载/缓存/数据库。
public final class RDPDFReaderKitView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
public weak var dataSource: RDPDFReaderKitViewDataSource?
public weak var delegate: RDPDFReaderKitViewDelegate?
public private(set) var currentPage = 0
/// 业务层可把画笔叠加到 `RDPDFZoomablePageView.contentView`;此状态禁止单指翻页,
/// 同时保留页面的双指缩放和双指拖动。
public var isDrawingMode = false {
didSet { updatePagingInteraction() }
}
public var onConfigurePage: ((RDPDFZoomablePageView, Int) -> Void)?
private let layout: UICollectionViewFlowLayout = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0
return layout
}()
private lazy var collectionView: UICollectionView = {
let view = UICollectionView(frame: .zero, collectionViewLayout: layout)
view.backgroundColor = UIColor(white: 0.93, alpha: 1)
view.isPagingEnabled = true
view.showsHorizontalScrollIndicator = false
view.dataSource = self
view.delegate = self
view.panGestureRecognizer.maximumNumberOfTouches = 1
view.register(PageCell.self, forCellWithReuseIdentifier: PageCell.reuseIdentifier)
return view
}()
private var isCurrentPageZoomed = false
public override init(frame: CGRect) {
super.init(frame: frame)
addSubview(collectionView)
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
addSubview(collectionView)
}
public override func layoutSubviews() {
super.layoutSubviews()
collectionView.frame = bounds
layout.itemSize = bounds.size
}
public func reloadData() {
let pageCount = dataSource?.numberOfPages(in: self) ?? 0
currentPage = max(0, min(currentPage, max(0, pageCount - 1)))
isCurrentPageZoomed = false
collectionView.reloadData()
collectionView.layoutIfNeeded()
collectionView.setContentOffset(CGPoint(x: CGFloat(currentPage) * bounds.width, y: 0), animated: false)
updatePagingInteraction()
}
public func go(to page: Int, animated: Bool = true) {
let count = dataSource?.numberOfPages(in: self) ?? 0
guard count > 0 else { return }
let target = max(0, min(page, count - 1))
guard !isDrawingMode, !isCurrentPageZoomed else { return }
currentPage = target
collectionView.setContentOffset(CGPoint(x: CGFloat(target) * bounds.width, y: 0), animated: animated)
delegate?.readerView(self, didMoveToPage: target)
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
dataSource?.numberOfPages(in: self) ?? 0
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PageCell.reuseIdentifier, for: indexPath) as! PageCell
let pageView = cell.pageView
pageView.image = dataSource?.readerView(self, imageForPageAt: indexPath.item)
pageView.isDrawingMode = isDrawingMode
pageView.zoomStateChanged = { [weak self, weak pageView] zoomed in
guard let self, let pageView, pageView.tag == self.currentPage else { return }
self.isCurrentPageZoomed = zoomed
self.updatePagingInteraction()
}
pageView.tag = indexPath.item
onConfigurePage?(pageView, indexPath.item)
return cell
}
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
updateCurrentPageFromOffset()
}
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
updateCurrentPageFromOffset()
}
private func updateCurrentPageFromOffset() {
guard bounds.width > 0 else { return }
let page = Int((collectionView.contentOffset.x / bounds.width).rounded())
guard page != currentPage else { return }
currentPage = page
isCurrentPageZoomed = false
updatePagingInteraction()
delegate?.readerView(self, didMoveToPage: page)
}
private func updatePagingInteraction() {
collectionView.isScrollEnabled = !isDrawingMode && !isCurrentPageZoomed
collectionView.visibleCells
.compactMap { ($0 as? PageCell)?.pageView }
.forEach { $0.isDrawingMode = isDrawingMode }
}
}
private final class PageCell: UICollectionViewCell {
static let reuseIdentifier = "RDPDFReaderKitView.PageCell"
let pageView = RDPDFZoomablePageView()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(pageView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
pageView.frame = contentView.bounds
}
override func prepareForReuse() {
super.prepareForReuse()
pageView.resetZoom()
pageView.zoomStateChanged = nil
}
}