// // RDReaderView.swift // RDReaderDemo // // Created by yangsq on 2021/8/6. // import UIKit @objc public protocol RDReaderDataSource: NSObjectProtocol { func pageCountOfReaderView(readerView: RDReaderView) -> Int func pageContentView(readerView: RDReaderView, pageNum: Int, containerView: UIView?) -> UIView func pageIdentifier(readerView: RDReaderView, pageNum: Int) -> String? @objc optional func topToolView(readerView: RDReaderView) -> UIView? @objc optional func bottomToolView(readerView: RDReaderView) -> UIView? } @objc public protocol RDReaderDelegate: NSObjectProtocol { func pageNum(readerView: RDReaderView, pageNum: Int) /// 横竖屏切换时回调,可在此重新分页 @objc optional func readerViewOrientationWillChange(readerView: RDReaderView, isLandscape: Bool) } extension RDReaderView { public enum DisplayType { case pageCurl case horizontalScroll case verticalScroll } /// 翻页方向 public enum PageDirection { /// 从左往右翻页(默认,适用于中文/英文书籍) case leftToRight /// 从右往左翻页(适用于日文漫画等) case rightToLeft } } public class RDReaderView: UIView { enum TapEvent { case none, left, center, right } private lazy var pageViewController: UIPageViewController = { let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil) pageVC.delegate = self pageVC.dataSource = self return pageVC }() private lazy var layout: RDReaderFlowLayout = { let layout = RDReaderFlowLayout(displayType: .horizontalScroll) layout.dataSource = self layout.delegate = self return layout }() private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) collectionView.backgroundColor = UIColor.clear return collectionView }() public var currentPage: Int = -1 { didSet { if let delegate = delegate, currentPage != oldValue, delegate.responds(to: #selector(RDReaderDelegate.pageNum(readerView:pageNum:))) { delegate.pageNum(readerView: self, pageNum: currentPage) } } } private(set) lazy var tapGestureRecognizer: UITapGestureRecognizer = { let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(tap:))) return tap }() private var tapEvent: TapEvent = .none { didSet { let isRTL = pageDirection == .rightToLeft switch tapEvent { case .left: if currentDisplayType != .pageCurl { if isRTL { goNextPage() } else { goPreviousPage() } } case .right: if currentDisplayType != .pageCurl { if isRTL { goPreviousPage() } else { goNextPage() } } case .center: tapCenter() default: break } } } private func goNextPage() { let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 if pagesPerScreen > 1 { if let target = adjacentDualPage(from: currentPage, forward: true) { transitionToPage(pageNum: target, animated: true) } } else if currentPage + 1 < totalPages { transitionToPage(pageNum: currentPage + 1, animated: true) } } private func goPreviousPage() { if pagesPerScreen > 1 { if let target = adjacentDualPage(from: currentPage, forward: false) { transitionToPage(pageNum: target, animated: true) } } else if currentPage - 1 >= 0 { transitionToPage(pageNum: currentPage - 1, animated: true) } } public weak var dataSource: RDReaderDataSource? = nil public weak var delegate: RDReaderDelegate? = nil public var currentDisplayType: RDReaderView.DisplayType = .pageCurl public var toolViewAnimationDuration: TimeInterval = 0.3 /// 是否启用横屏双页显示 public var landscapeDualPageEnabled: Bool = false /// 翻页方向,默认从左往右(.leftToRight) public var pageDirection: RDReaderView.PageDirection = .leftToRight /// 横屏双页模式下,封面页的索引。设置后该页在横屏时独占一屏,后续页面两两配对。 /// 设为 nil 表示没有封面页(所有页面正常两两配对)。 public var coverPageIndex: Int? = nil /// 是否启用了封面页独占 private var hasCoverPage: Bool { return coverPageIndex != nil } /// 当前是否横屏 private var isLandscape: Bool { return bounds.width > bounds.height } /// 每屏显示的页数 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 { guard landscapeDualPageEnabled, isLandscape, let coverIndex = coverPageIndex else { return false } return pageNum == coverIndex } /// 根据逻辑页码计算横屏双页模式下的配对信息 /// 封面页独占一屏,后续页面两两配对 /// - Parameter pageNum: 逻辑页码 /// - Returns: (左页码, 右页码(可选)) private func dualPagePair(for pageNum: Int) -> (left: Int, right: Int?) { let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 if let coverIndex = coverPageIndex { if pageNum == coverIndex { // 封面页独占一屏 return (coverIndex, nil) } // 封面之后的页面:偏移1后两两配对 // 例如封面是0页,则 1+2, 3+4, 5+6... let adjustedIndex = pageNum - (coverIndex + 1) // 跳过封面后的偏移 let pairStart = coverIndex + 1 + (adjustedIndex / 2) * 2 let left = pairStart let right = pairStart + 1 < totalPages ? pairStart + 1 : nil return (left, right) } else { // 无封面页:正常两两配对 0+1, 2+3, 4+5... let left = (pageNum / 2) * 2 let right = left + 1 < totalPages ? left + 1 : nil return (left, right) } } /// 计算横屏双页下,某页往前/后翻一屏后的起始页码 /// - Parameters: /// - pageNum: 当前页码 /// - forward: 是否向后翻 /// - Returns: 目标页码,nil 表示到头了 private func adjacentDualPage(from pageNum: Int, forward: Bool) -> Int? { let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 let pair = dualPagePair(for: pageNum) if forward { let nextStart = (pair.right ?? pair.left) + 1 return nextStart < totalPages ? nextStart : nil } else { let prevEnd = pair.left - 1 guard prevEnd >= 0 else { return nil } return dualPagePair(for: prevEnd).left } } private var previousIsLandscape: Bool? private var contentViews = [String : UIView.Type]() private var willPreviousTransitionToViewController: UIViewController? = nil private var willNextTransitionToViewController: UIViewController? = nil private var willTransitionToViewController: UIViewController? = nil private var topToolView: UIView? private var bottomToolView: UIView? private var isShowToolView: Bool = false private var isTransitioning: Bool = false private var didBuildUI = false /// 用于 pageCurl 双页模式下封面页旁边的空白页 static let blankPageNum = Int.max /// 用于 pageCurl 双页模式下末尾不成对页旁边的空白页 static let blankEndPageNum = Int.max - 1 override init(frame: CGRect) { super.init(frame: frame) } public override func layoutSubviews() { super.layoutSubviews() guard bounds.width > 0, bounds.height > 0 else { return } 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) } } else if previousIsLandscape == nil { previousIsLandscape = nowLandscape layout.isLandscapeDualPage = landscapeDualPageEnabled && nowLandscape } } /// 横竖屏切换处理 /// - 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) default: // 滚动模式:禁用动画防止旋转过渡中出现尺寸抖动 UIView.performWithoutAnimation { // 强制使布局完全失效并重新计算 collectionView.collectionViewLayout.invalidateLayout() collectionView.reloadData() collectionView.layoutIfNeeded() // 3. 根据保存的页码恢复滚动位置 let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 let safePage = min(savedPage, max(0, totalPages - 1)) let targetOffset = layout.currentContentOffset(count: safePage) collectionView.setContentOffset(targetOffset, animated: false) } } } /// 创建带指定 spine 位置的 UIPageViewController /// - Parameter isDualPage: 是否双页模式(横屏时书脊在中间:.mid) private func createPageViewController(isDualPage: Bool) -> UIPageViewController { let options: [UIPageViewController.OptionsKey: Any]? if isDualPage { options = [.spineLocation: NSNumber(value: UIPageViewController.SpineLocation.mid.rawValue)] } else { options = nil } let pageVC = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: options) pageVC.delegate = self pageVC.dataSource = self pageVC.isDoubleSided = isDualPage return pageVC } /// 重建 UIPageViewController。横竖屏切换时需要重建,因为 spineLocation 只能在初始化时设置 private func rebuildPageViewController() { detachPageViewControllerIfNeeded() let isDualPage = landscapeDualPageEnabled && isLandscape let pageVC = createPageViewController(isDualPage: isDualPage) pageViewController = pageVC attachPageViewControllerIfNeeded() } public override func didMoveToSuperview() { super.didMoveToSuperview() if superview != nil { makeUI() } } private func attachPageViewControllerIfNeeded() { guard let parentViewController = self.ss_superViewController else { return } var didAddToParent = false if pageViewController.parent !== parentViewController { if pageViewController.parent != nil { pageViewController.willMove(toParent: nil) pageViewController.view.removeFromSuperview() pageViewController.removeFromParent() } parentViewController.addChild(pageViewController) didAddToParent = true } pageViewController.view.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height) pageViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] if pageViewController.view.superview !== self { insertSubview(pageViewController.view, at: 0) } if didAddToParent { pageViewController.didMove(toParent: parentViewController) } } private func detachPageViewControllerIfNeeded() { if pageViewController.parent != nil { pageViewController.willMove(toParent: nil) } pageViewController.view.removeFromSuperview() if pageViewController.parent != nil { pageViewController.removeFromParent() } } private func makeUI() { guard !didBuildUI else { if currentDisplayType == .pageCurl { attachPageViewControllerIfNeeded() } return } didBuildUI = true collectionView.dataSource = self collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self)) collectionView.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height) collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight] if currentDisplayType == .pageCurl { attachPageViewControllerIfNeeded() } addGestureRecognizer(tapGestureRecognizer) // 不取消底层触摸事件,确保工具栏按钮(返回等)的 touchUpInside 能正常触发 tapGestureRecognizer.cancelsTouchesInView = false } @objc private func tapAction(tap: UITapGestureRecognizer) { let point = tap.location(in: tap.view) 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 } } let viewFrame = tap.view!.frame let leftFrame = CGRect(x: 0, y: 0, width: viewFrame.width / 3, height: viewFrame.height) let centerFrame = CGRect(x: viewFrame.width / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) let rightFrame = CGRect(x: viewFrame.width * 2 / 3, y: 0, width: viewFrame.width / 3, height: viewFrame.height) if leftFrame.contains(point) { if isShowToolView { tapEvent = .center } else { tapEvent = .left } } if centerFrame.contains(point) { tapEvent = .center } if rightFrame.contains(point) { if isShowToolView { tapEvent = .center } else { tapEvent = .right } } } private func tapCenter() { isShowToolView = !isShowToolView if isShowToolView { if let topToolView = topToolView { addSubview(topToolView) topToolView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ topToolView.leadingAnchor.constraint(equalTo: leadingAnchor), topToolView.trailingAnchor.constraint(equalTo: trailingAnchor), topToolView.topAnchor.constraint(equalTo: topAnchor) ]) layoutIfNeeded() topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) UIView.animate(withDuration: toolViewAnimationDuration) { topToolView.transform = .identity } } if let bottomToolView = bottomToolView { addSubview(bottomToolView) bottomToolView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ bottomToolView.leadingAnchor.constraint(equalTo: leadingAnchor), bottomToolView.trailingAnchor.constraint(equalTo: trailingAnchor), bottomToolView.bottomAnchor.constraint(equalTo: bottomAnchor) ]) layoutIfNeeded() bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) UIView.animate(withDuration: toolViewAnimationDuration) { bottomToolView.transform = .identity } } collectionView.isUserInteractionEnabled = false pageViewController.view.isUserInteractionEnabled = false } else { if let topToolView = topToolView { UIView.animate(withDuration: toolViewAnimationDuration, animations: { topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) }) { _ in topToolView.removeFromSuperview() } } if let bottomToolView = bottomToolView { UIView.animate(withDuration: toolViewAnimationDuration, animations: { bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) }) { _ in bottomToolView.removeFromSuperview() } } collectionView.isUserInteractionEnabled = true pageViewController.view.isUserInteractionEnabled = true } } private func isHitView(_ hitView: UIView?, inside toolView: UIView, point: CGPoint) -> Bool { if toolView.frame.contains(point) { return true } guard let hitView else { return false } return hitView === toolView || hitView.isDescendant(of: toolView) } /// 切换翻页模式(仿真/水平滚动/上下滚动) /// 会重建底层视图(PageViewController 或 CollectionView),并恢复到当前页 public func switchReaderDisplayType(_ displayType: RDReaderView.DisplayType) { self.currentDisplayType = displayType if currentPage == -1 { currentPage = 0 } // 同步横屏双页标记到布局 layout.isLandscapeDualPage = landscapeDualPageEnabled && isLandscape layout.coverPageIndex = coverPageIndex switch displayType { case .pageCurl: self.collectionView.removeFromSuperview() self.collectionView.transform = .identity attachPageViewControllerIfNeeded() rebuildPageViewController() transitionToPage(pageNum: currentPage) 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 transitionToPage(pageNum: currentPage) } } /// 跳转到指定页码 /// - Parameters: /// - pageNum: 目标页码(item 索引) /// - animated: 是否动画过渡 public func transitionToPage(pageNum: Int, animated: Bool = false) { switch currentDisplayType { case .pageCurl: let isDualPage = landscapeDualPageEnabled && isLandscape // RTL 时动画方向需要反转 let direction: UIPageViewController.NavigationDirection if pageDirection == .rightToLeft { direction = pageNum > currentPage ? .reverse : .forward } else { direction = pageNum > currentPage ? .forward : .reverse } if isDualPage { let pair = dualPagePair(for: pageNum) let leftContent = dataSource?.pageContentView(readerView: self, pageNum: pair.left, containerView: nil) let leftVC = RDReaderPageChildViewController(contentView: leftContent, pageNum: pair.left) if let rightPage = pair.right { let rightContent = dataSource?.pageContentView(readerView: self, pageNum: rightPage, containerView: nil) let rightVC = RDReaderPageChildViewController(contentView: rightContent, pageNum: rightPage) pageViewController.setViewControllers([leftVC, rightVC], direction: animated ? direction : .forward, animated: animated, completion: nil) } 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, completion: nil) } currentPage = pair.left } else { let contentView = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: nil) let vc = RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum) pageViewController.setViewControllers([vc], direction: animated ? direction : .forward, animated: animated, completion: nil) currentPage = pageNum } default: collectionView.reloadData() collectionView.layoutIfNeeded() collectionView.setContentOffset(layout.currentContentOffset(count: pageNum), animated: animated) } } public func reloadData() { switchReaderDisplayType(currentDisplayType) topToolView = self.dataSource?.topToolView?(readerView: self) bottomToolView = self.dataSource?.bottomToolView?(readerView: self) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } extension RDReaderView: UIPageViewControllerDataSource, UIPageViewControllerDelegate { private func makeSinglePageChildVC(for pageNum: Int) -> RDReaderPageChildViewController { if pageNum == RDReaderView.blankPageNum || pageNum == RDReaderView.blankEndPageNum { return RDReaderPageChildViewController(contentView: UIView(), pageNum: pageNum) } let contentView = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: nil) return RDReaderPageChildViewController(contentView: contentView, pageNum: pageNum) } /// 计算某页的"下一页"页码(考虑封面页插入空白页) private func nextPageNum(after pageNum: Int, isDualPage: Bool) -> Int? { let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 // 末尾空白页之后没有更多页 if pageNum == RDReaderView.blankEndPageNum { return nil } if isDualPage, let coverIndex = coverPageIndex { if pageNum == coverIndex { return RDReaderView.blankPageNum } if pageNum == RDReaderView.blankPageNum { let firstContent = coverIndex + 1 return firstContent < totalPages ? firstContent : nil } } let next = pageNum + 1 if next < totalPages { return next } // pageNum 是最后一页,检查在双页模式下是否需要空白页配对 if isDualPage { if let coverIndex = coverPageIndex { // 有封面时:封面之后的页面两两配对,偶数偏移=左页需要配对 let adjustedIndex = pageNum - (coverIndex + 1) if adjustedIndex >= 0 && adjustedIndex % 2 == 0 { return RDReaderView.blankEndPageNum } } else { // 无封面:偶数索引=左页需要配对 if pageNum % 2 == 0 { return RDReaderView.blankEndPageNum } } } return nil } /// 计算某页的"上一页"页码(考虑封面页插入空白页) private func prevPageNum(before pageNum: Int, isDualPage: Bool) -> Int? { // 末尾空白页的前一页是最后一个真实页 if pageNum == RDReaderView.blankEndPageNum { let totalPages = dataSource?.pageCountOfReaderView(readerView: self) ?? 0 return totalPages > 0 ? totalPages - 1 : nil } if isDualPage, let coverIndex = coverPageIndex { if pageNum == RDReaderView.blankPageNum { return coverIndex } if pageNum == coverIndex + 1 { return RDReaderView.blankPageNum } } let prev = pageNum - 1 return prev >= 0 ? prev : nil } public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let vc = viewController as? RDReaderPageChildViewController else { return nil } let isDualPage = landscapeDualPageEnabled && isLandscape let isRTL = pageDirection == .rightToLeft // RTL 时 before/after 语义互换:before(向右翻)= 下一页 let targetNum = isRTL ? nextPageNum(after: vc.pageNum, isDualPage: isDualPage) : prevPageNum(before: vc.pageNum, isDualPage: isDualPage) guard let num = targetNum else { return nil } let targetVC = makeSinglePageChildVC(for: num) willPreviousTransitionToViewController = targetVC return targetVC } public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let vc = viewController as? RDReaderPageChildViewController else { return nil } let isDualPage = landscapeDualPageEnabled && isLandscape let isRTL = pageDirection == .rightToLeft let targetNum = isRTL ? prevPageNum(before: vc.pageNum, isDualPage: isDualPage) : nextPageNum(after: vc.pageNum, isDualPage: isDualPage) guard let num = targetNum else { return nil } let targetVC = makeSinglePageChildVC(for: num) willNextTransitionToViewController = targetVC return targetVC } 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 if pn != RDReaderView.blankPageNum && pn != RDReaderView.blankEndPageNum { currentPage = pn } } } public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) { willTransitionToViewController = pendingViewControllers.first } } extension RDReaderView: UICollectionViewDataSource, RDReaderFlowLayoutDelegate, RDReaderFlowLayoutDataSoure { public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let identifer = self.dataSource?.pageIdentifier(readerView: self, pageNum: indexPath.row) { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifer, for: IndexPath(item: indexPath.row, section: 0)) as! RDReaderContentCell let conttainerView = self.dataSource?.pageContentView(readerView: self, pageNum: indexPath.row, containerView: cell.containerView) if let conttainerView = conttainerView { cell.containerView = conttainerView } // RTL 水平模式:翻转 cell 内容使文字方向正常(collectionView 已整体翻转) if pageDirection == .rightToLeft && currentDisplayType != .verticalScroll { cell.contentView.transform = CGAffineTransform(scaleX: -1, y: 1) } else { cell.contentView.transform = .identity } return cell } let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(UICollectionViewCell.self), for: indexPath) return cell } public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return self.dataSource?.pageCountOfReaderView(readerView: self) ?? 0 } public func pageNum(flowLayout: RDReaderFlowLayout, pageIndex: Int) { currentPage = pageIndex } public func heigtOfVerticalScrollPage(flowLayout: RDReaderFlowLayout, pageIndex: Int) -> CGFloat? { return nil } } extension RDReaderView { public func register(contentView: UIView.Type, contentViewWithReuseIdentifier identifier: String) { contentViews[identifier] = contentView collectionView.register(RDReaderContentCell.self, forCellWithReuseIdentifier: identifier) } 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 } let contentViewClass = contentViews[identifier] assert(contentViewClass != nil, "请调用register(contentView:contentViewWithReuseIdentifier:)") var contentView = contentViewClass!.init() return contentView } public func pageContentView(pageNum: Int) -> UIView? { if currentDisplayType == .pageCurl { return (self.pageViewController.viewControllers?.first as? RDReaderPageChildViewController)?.contentView } else { let cell = collectionView.cellForItem(at: IndexPath(item: pageNum, section: 0)) as? RDReaderContentCell return cell?.containerView } } } private var cellViewKey: Int8 = 0 extension UIView { var ss_superViewController: UIViewController? { var next = self.next while next != nil { if next is UIViewController { return next as? UIViewController } else { next = next!.next } } return nil } }