The direct way to run into this assert is to use cycled source for UIPageController
defined with scroll transition style.
When the source contains two pages each one is the previous and the next for another one. If you swipe UIPageController
containing two pages and then try to set source with 3 pages you will get the assertion mentioned above with guarantee assuming that UIPageControllerDataSource
before/after methods allow cycled transition in case of 2 pages.
The main rules of crash-free using UIPageController
with scroll transition:
1) set dataSource
before calling setViewControllers
method
2) use setViewControllers
method without animation (animated: false
)
3) set dataSource
to nil for single page mode
4) don't allow cycles for 2-page mode
All these recommendations together make UIPageController
absolutely stable.
import UIKit
/// Convenient subclass of UIPageViewController
@objc class AMPageViewController: UIPageViewController {
/// Turn on/off PageControl at the bottom
@objc var showPageControl: Bool = true
/// Array of all viewControllers
@objc var source: [UIViewController]? {
didSet {
let count = source?.count ?? 0
if count > 0 {
dataSource = count > 1 ? self : nil
}
else {
dataSource = nil
delegate = nil
}
}
}
/// Index of the current viewController from source
@objc var pageIndex: Int {
get {
var currentPageIndex: Int = 0
if let vc = viewControllers?.first, let source = source, let pageIndex = source.index(of: vc) {
currentPageIndex = pageIndex
}
return currentPageIndex
}
set {
guard newValue >= 0, let source = source, newValue < source.count else { return }
let vc = source[newValue]
let direction: UIPageViewControllerNavigationDirection = newValue < pageIndex ? .reverse : .forward
setViewController(vc, direction: direction)
}
}
override weak var delegate: UIPageViewControllerDelegate? {
get { return super.delegate }
set {
if source?.count ?? 0 > 0 {
super.delegate = newValue
}
else {
super.delegate = nil
}
}
}
/// Initializer in scroll-mode with interPageSpacing
@objc init(navigationOrientation: UIPageViewControllerNavigationOrientation = .horizontal, interPageSpacing: Int = 0) {
let options = (interPageSpacing > 0) ? [UIPageViewControllerOptionInterPageSpacingKey : 5] : nil
super.init(transitionStyle: .scroll, navigationOrientation: navigationOrientation, options: options)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// Set viewcontroller by index from source
@objc func setPageIndex(_ index: Int, completion: ((Bool) -> Void)? = nil) {
guard index > 0, let source = source, index < source.count else { return }
let vc = source[index]
let direction: UIPageViewControllerNavigationDirection = index < pageIndex ? .reverse : .forward
setViewController(vc, direction: direction, completion: completion)
}
private func setViewController(_ viewController: UIViewController, direction: UIPageViewControllerNavigationDirection = .forward, completion: ((Bool) -> Void)? = nil) {
super.setViewControllers([viewController], direction: direction, animated: false, completion: completion)
}
}
extension FFPageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let source = source, let index = source.index(of: viewController) else { return nil }
let count = source.count
if count == 2, index == 0 {
return nil
}
let prevIndex = (index - 1) < 0 ? count - 1 : index - 1
let pageContentViewController: UIViewController = source[prevIndex]
return pageContentViewController
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let source = source, let index = source.index(of: viewController) else { return nil }
let count = source.count
if count == 2, index == 1 {
return nil
}
let nextIndex = (index + 1) >= count ? 0 : index + 1
let pageContentViewController = source[nextIndex]
return pageContentViewController
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return showPageControl ? (source?.count ?? 0) : 0
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard showPageControl else { return 0 }
return pageIndex
}
}
The overall implementation and usage examples one can find at GitHub project.