ReadViewSDK/Sources/RDReaderView/RDReaderView.swift
2026-05-21 19:40:51 +08:00

753 lines
31 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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(contentViewcontentViewWithReuseIdentifier")
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
}
}