Continuous scrolling between two UIPageControllers - ios

I am trying to achieve a continuous scrolling in the following situation:
MainViewController (UIViewController) contains an instance of UIPageViewController (transitionStyle: .scroll) which manages two controllers only; ItemsViewController (UIViewController) & ProfileViewController (UIViewController). MainViewController allows scrolling between main and profile controllers horizontally.
Now ProfileViewController contains another instance of UIPageViewController (transitionStyle: .scroll) to scroll horizontally between two UIViewControllers (A & B).
The issue is that when I scroll to ProfileViewController and start scrolling between A & B then I can't scroll (to the right) to go back to ItemsViewController. It seems that the UIPageViewController of ProfileViewController is intercepting the scroll gesture and doesn't pass them to UIPageViewController instance of MainViewController and you will get stuck in ProfileViewController.
Here is some code:
final class MainViewController: UIViewController {
private let pageController: UIPageViewController
let itemsController: ItemsViewController
let profileController: ProfileViewController
private var controllers = [UIViewController]()
private(set) var currentPageIndex = 0
override init(...) {
itemsController = ItemsViewController(...)
controllers.append(itemsController)
profileController = ProfileViewController(...)
controllers.append(profileController)
pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
super.init(...)
}
override func viewDidLoad() {
super.viewDidLoad()
configurePageController()
}
func configurePageController() {
pageController.dataSource = self
pageController.delegate = self
pageController.setViewControllers([itemsController], direction: .forward, animated: false)
for view in pageController.view.subviews {
if let scrollView = view as? UIScrollView {
scrollView.delegate = self
break
}
}
}
}
extension MainViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController) {
if index > 0 {
return controllers[index - 1]
}
else {
return nil
}
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController) {
if index < controllers.count - 1 {
return controllers[index + 1]
}
else {
return nil
}
}
return nil
}
}
extension MainViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard completed else {
return
}
if let vc = pageController.viewControllers?.first,
let index = controllers.firstIndex(of: vc) {
currentPageIndex = index
}
}
}
// Freeze scrolling on the left and right edges
extension MainViewController: UIScrollViewDelegate {
private func freezScrolling(_ scrollView: UIScrollView) {
if currentPageIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width {
scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
}
else if currentPageIndex == controllers.count - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
freezScrolling(scrollView)
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
freezScrolling(scrollView)
}
}
final class ProfileViewController: UIViewController {
private let pageController: UIPageViewController
private lazy var controllers: [UIViewController] = [ControllerA, ControllerB]
private var currentPageIndex = 0
override init(...) {
pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
super.init(...)
}
override func viewDidLoad() {
super.viewDidLoad()
configurePageController()
}
func configurePageController() {
pageController.dataSource = self
pageController.delegate = self
for view in pageController.view.subviews {
if let scrollView = view as? UIScrollView {
scrollView.delegate = self
break
}
}
}
extension ProfileViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index > 0 {
return controllers[index - 1]
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController), index < controllers.count - 1 {
return controllers[index + 1]
}
return nil
}
}
extension ProfileViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard completed else {
return
}
if let vc = pageController.viewControllers?.first,
let index = controllers.firstIndex(of: vc) {
currentPageIndex = index
}
}
}
extension ProfileViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
freezScrolling(scrollView)
}
private func freezScrolling(_ scrollView: UIScrollView) {
if currentPageIndex == 0 && scrollView.contentOffset.x < scrollView.bounds.size.width {
scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
}
else if currentPageIndex == controllers.count - 1 && scrollView.contentOffset.x > scrollView.bounds.size.width {
scrollView.contentOffset = CGPoint(x: scrollView.bounds.size.width, y: 0)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
freezScrolling(scrollView)
}
}

Related

How to pass data between UIPageViewControllers in swift?

I have a UIPageViewController with 4 View Controllers in it. In the second VC, the user enters some data. In the last VC, there is a button that the user clicks to insert these data into DB. I want to pass the data from the second VC to the fourth one. All my search results show segues, but the VCs are not directly connected.
So I thought of using delegates, but it is not working for me.
Code:
UIPageViewController:
import UIKit
class PageViewController: UIPageViewController {
var pageControl = UIPageControl()
fileprivate lazy var pages: [UIViewController] = {
return [
self.getViewController(withIdentifier: "locationStoryBoard"),
self.getViewController(withIdentifier: "notificationStoryboard"),
self.getViewController(withIdentifier: "weightStoryboard"),
self.getViewController(withIdentifier: "bloodTypeStoryboard"),
self.getViewController(withIdentifier: "ageStoryboard"),
self.getViewController(withIdentifier: "genderStoryboard"),
self.getViewController(withIdentifier: "mobileNumberStoryboard"),
self.getViewController(withIdentifier: "preventionStoryboard")
]
}()
fileprivate func getViewController(withIdentifier identifier: String) -> UIViewController
{
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: identifier)
}
override func viewDidLoad()
{
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let firstVC = pages.first
{
setViewControllers([firstVC], direction: .forward, animated: true, completion: nil)
}
configurePageControl()
}
//Page Control (3 dots)
func configurePageControl() {
pageControl = UIPageControl(frame: CGRect(x: 0,y: UIScreen.main.bounds.maxY - 50,width: UIScreen.main.bounds.width,height: 50))
self.pageControl.numberOfPages = pages.count
self.pageControl.currentPage = 0
self.pageControl.alpha = 0.5
self.pageControl.tintColor = UIColor.black
self.pageControl.pageIndicatorTintColor = UIColor.white
self.pageControl.currentPageIndicatorTintColor = #colorLiteral(red: 0.986409843, green: 0.4042935669, blue: 0.4366002679, alpha: 1)
self.view.addSubview(pageControl)
}
}
//MARK: - UIPageViewControllerDataSource methods
extension PageViewController: UIPageViewControllerDataSource
{
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.firstIndex(of: viewController)!
let previousIndex = currentIndex - 1
return (previousIndex == -1) ? nil : pages[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndex = pages.firstIndex(of: viewController)!
let nextIndex = currentIndex + 1
return (nextIndex == pages.count) ? nil : pages[nextIndex]
}
}
//MARK: - UIPageViewControllerDelegate methods
extension PageViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
self.pageControl.currentPage = pages.firstIndex(of: pageContentViewController)!
}
}
First VC
protocol NameAndMobileDelegates {
func passName(nameToBePassed: String)
}
class MobileNumberViewController: UIViewController, UITextFieldDelegate {
var delegate: NameAndMobileDelegates?
...
func textFieldDidEndEditing(_ textField: UITextField) {
switch textField.tag {
case 1:
name = textField.text!
self.delegate?.passName(nameToBePassed: name)
print(name)
case 2:
mobileNumber = textField.text!
print(mobileNumber)
default: break
}
}
In the 4th VC:
// MARK: - Name and Number Delegate Methods
extension DiseasesViewController: NameAndMobileDelegates {
func passName(nameToBePassed: String) {
print("protocol worked")
}
Thank you in advance
Do
if let fourVC = pages[3] as? FourthVC {
fourVC.delegate = pages.first as! FirstVC
}
then
class FourthVC:UIViewController {
weak var delegate:FirstVC?

Got nil for delegate at ViewController included in UIPageViewController

I have a UIPageViewController and several ViewControllers in it. And I want to perform a function in UIPageViewController from a ViewController using delegate protocol. But get nil in the delegate. Can anybody determine what I'm doing wrong?
PageViewController.swift
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource, Storyboarded {
weak var coordinator: MainCoordinator?
lazy var orderedViewControllers: [UIViewController] = {
return [self.newInstanceVC(viewController: "FirstLoadViewController"), self.newInstanceVC(viewController: "FirstLoad2ViewController")]
}()
var pageControl = UIPageControl()
override func viewDidLoad() {
super.viewDidLoad()
let storyboardVC = newInstanceVC(viewController: "FirstLoad2ViewController") as? FirstLoad2ViewController
storyboardVC?.delegate = self
self.dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
}
self.delegate = self
configurePageControl()
}
func configurePageControl() {
pageControl = UIPageControl(frame: CGRect(x: 0, y: UIScreen.main.bounds.maxY - 50, width: UIScreen.main.bounds.width, height: 50))
pageControl.numberOfPages = orderedViewControllers.count
pageControl.currentPage = 0
//pageControl.tintColor = .red
pageControl.pageIndicatorTintColor = .systemGray5
pageControl.currentPageIndicatorTintColor = .systemGray
self.view.addSubview(pageControl)
}
//return view controller by string identifier
func newInstanceVC(viewController: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
//MARK: - Setup page controllers
// view controller that appears on swiping to the -> (left)
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
//return view controller from the end if it is last index from the left side (infinit scroll)
//return orderedViewControllers.last
return nil
}
guard orderedViewControllers.count >= previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
// view controller that appears on swiping to the <- (right)
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard orderedViewControllers.count != nextIndex else {
//return view controller from the begining if it is last index from the right side (infinit scroll)
//return orderedViewControllers.first
return nil
}
guard orderedViewControllers.count > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
//switch the bullet of the page
let pageContentVievController = pageViewController.viewControllers![0]
self.pageControl.currentPage = orderedViewControllers.firstIndex(of: pageContentVievController)!
}
}
extension PageViewController: PageViewDelegate {
func handleButtonTap() {
coordinator?.goToMainFromPresentation()
}
}
OneOfTheViewControllers.swift
import UIKit
protocol PageViewDelegate: class {
func handleButtonTap()
}
class FirstLoad2ViewController: UIViewController, Storyboarded {
weak var coordinator: MainCoordinator?
weak var child: ChildCoordinator?
weak var delegate: PageViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func toMainScreenTap(_ sender: Any) {
delegate!.handleButtonTap() //got nil for delegate here
}
}
The problem is simple, it's hiding in your viewDidLoad() method inside PageViewController
...
override func viewDidLoad() {
super.viewDidLoad()
let storyboardVC = newInstanceVC(viewController: "FirstLoad2ViewController") as? FirstLoad2ViewController
storyboardVC?.delegate = self
...
}
/// (storyboardVC) --- will be destroyed when out of viewDidLoad() scope.
So, why you getting your storyboardVC?.delegate = self nil? Because you created storyboardVC property inside viewDidLoad() method and nothing is keeping reference to it. So, at the end viewDidLoad() method it just deinitialize as well as it's PageViewDelegate delegate

Fast scroll UIPageViewController prevents viewcontroller from updating

I have a UIPageviewcontroller which got two controllers inside. As you swipe to the next, I use the viewController argument to set the appropriate delegate. But I experience that if you swipe too fast, the function viewControllerAfter isn't updating the viewController correctly. The initially swipe should update the index of the viewcontroller from 0 to 1, but doesn't do so if you swipe too fast.
import UIKit
class WizardPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
lazy var orderedViewControllers: [UIViewController] = {
return [self.newVc(viewController: "intro"),
self.newVc(viewController: "welcome")]
}()
var pageControl = UIPageControl()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
configurePageControl()
// This sets up the first view that will show up on our page control
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
self.pageControl.currentPage = orderedViewControllers.index(of: pageContentViewController)!
}
func newVc(viewController: String) -> UIViewController {
return UIStoryboard(name: "Wizard", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
func configurePageControl() {
// The total number of pages that are available is based on how many available colors we have.
pageControl = UIPageControl(frame: CGRect(x: 0,y: UIScreen.main.bounds.maxY - 225,width: UIScreen.main.bounds.width,height: 50))
self.pageControl.numberOfPages = orderedViewControllers.count
self.pageControl.currentPage = 0
pageControl.isEnabled = false
//self.pageControl.tintColor = UIColor.black
self.pageControl.pageIndicatorTintColor = UIColor.gray
self.pageControl.currentPageIndicatorTintColor = UIColor(red:0.647, green:0.192, blue:0.216, alpha:1.00)
self.view.addSubview(pageControl)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
print(orderedViewControllers.index(of: viewController))
let previousIndex = viewControllerIndex - 1
// User is on the first view controller and swiped left to loop to
// the last view controller.
guard previousIndex >= 0 else {
return nil
// Uncommment the line below, remove the line above if you don't want the page control to loop.
// return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
print(orderedViewControllers.index(of: viewController)) // Returns 0 is I swipe too fast, otherwise 1
if let vc = orderedViewControllers[viewControllerIndex] as? WelcomeViewController {
vc.delegate = self
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
// User is on the last view controller and swiped right to loop to
// the first view controller.
guard orderedViewControllersCount != nextIndex else {
return nil
// Uncommment the line below, remove the line above if you don't want the page control to loop.
// return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
I've encountered exactly the same problem. It essentially boils down to UIPageViewController (_UIQueuingScrollView to be exact) not updating its view hierarchy correctly.
I noticed UIPageViewController is clever enough not to add/remove the content view if the page remains the same (even if it's in wrong place in the view hierarchy it somehow manages to cope with it so it looks good to the user). That's why I added a PageTrackingView to observe the content view being added (or not) to UIPageViewController view hierarchy. By tracking if the change happens I can calculate the current index by flipping it. So currently the workaround is good enough for 2 pages only.
class PageTrackingView: UIView {
var pageIndexable: PageIndexable?
override func willMove(toSuperview newSuperview: UIView?) {
super.willMove(toSuperview: newSuperview)
if var pageIndexable = pageIndexable {
let newIndex = (pageIndexable.internalIndex == 0) ? 1 : 0
pageIndexable.internalIndex = newIndex
}
//Reset pageIndexable so that the index flip is fired only once
pageIndexable = nil
}
}
protocol PageIndexable {
var internalIndex: Int { get set }
}
class PageViewController: UIPageViewController,
UIPageViewControllerDataSource,
UIPageViewControllerDelegate,
PageIndexable {
private var supportedViewControllers = [UIViewController]()
internal var internalIndex: Int = 0 {
didSet {
if supportedViewControllers.indices.contains(internalIndex) {
//Do something with the actual internalIndex value
} else {
assertionFailure()
}
}
init(transitionStyle style: UIPageViewControllerTransitionStyle = .scroll,
navigationOrientation: UIPageViewControllerNavigationOrientation = .horizontal,
options: [String: Any]? = nil,
viewControllers: [T]) {
supportedViewControllers = viewControllers
if !supportedViewControllers.isEmpty {
let viewController = supportedViewControllers[0]
let trackingView = PageTrackingView(frame: viewController.view.frame)
viewController.view.frame = viewController.view.bounds
trackingView.addSubview(viewController.view)
viewController.view = trackingView
}
super.init(transitionStyle: style,
navigationOrientation: navigationOrientation,
options: options)
}
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
if !supportedViewControllers.isEmpty, let trackingView = supportedViewControllers[0].view as? PageTrackingView {
trackingView.pageIndexable = self
}
}
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
datasource = self
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = arrayVCs.index(of: viewController) else {
return nil
}
if index == 0 {
return nil
}
let prevIndex = abs((index - 1) % arrayVCs.count)
return arrayVCs[prevIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = arrayVCs.index(of: viewController) else {
return nil
}
if index == arrayVCs.count - 1 {
return nil
}
let nextIndex = abs((index + 1) % arrayVCs.count)
return arrayVCs[nextIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if let viewController = pageViewController.viewControllers?[0] {
guard let index = arrayVCs.index(of: viewController) else {
return
}
self.segment.selectedSegmentIndex = index
}
}
Try this. It's work for me.

PageControll error number of page swift 4

I created a PageController with 3 pages .. I allocated and declared everything, I implemented the two functions that allow scrolling views, in the same I entered the commands to enable the controller with the right page reported. the result is that if I flow my pages is all ok, while under the page counter works as he wants.. and I can not understand what is wrong
problemproblem2problem3
import UIKit
import AVFoundation
protocol IntroNavigationDelegate: class {
func showNextViewController()
func showPreviousViewController()
var isPagingEnabled: Bool { get set }
}
final class IntroViewController: UIPageViewController, UIPageViewControllerDelegate, IntroNavigationDelegate, UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let viewControllerIndex = self.displayedControllers.index(of: viewController) {
if viewControllerIndex == 0 {
} else if viewControllerIndex == 1 {
return self.displayedControllers[viewControllerIndex - 1]
}
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let viewControllerIndex = self.displayedControllers.index(of: viewController) {
if viewControllerIndex < self.displayedControllers.count - 1 {
self.pageControl.currentPage = 1
return self.displayedControllers[viewControllerIndex + 1]
} else {
self.pageControl.currentPage = 0
}
}
return nil
}
var introRouter: IntroRouter?
var pageControl = UIPageControl()
var displayedControllers: [UIViewController] = []
private var scrollView: UIScrollView? {
for view in view.subviews {
if let subView = view as? UIScrollView {
return subView
}
}
return nil
}
var isPagingEnabled: Bool {
get {
return scrollView?.isScrollEnabled ?? true
}
set {
scrollView?.isScrollEnabled = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
configurePageControl()
setupUI()
self.pageControl.updateCurrentPageDisplay()
arrangeSubviews()
self.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
navigationController?.isNavigationBarHidden = false
}
func setDiplayedControllers(_ controllers: [UIViewController], visualizedController: UIViewController) {
displayedControllers = controllers
setViewControllers([visualizedController], direction: .forward, animated: false, completion: nil)
}
func showNextViewController() {
if let current = viewControllers?.first, let next = IntroViewController(self, viewControllerAfter: current) {
setViewControllers([next], direction: .forward, animated: true, completion: nil)
}
}
func showPreviousViewController() {
if let current = viewControllers?.first, let previous = IntroViewController(self, viewControllerBefore: current) {
setViewControllers([previous], direction: .reverse, animated: true, completion: nil)
}
}
func configurePageControl(){
self.pageControl.frame = CGRect()
self.pageControl.numberOfPages = self.displayedControllers.count
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
self.pageControl.updateCurrentPageDisplay()
self.view.addSubview(self.pageControl)
pageControl.activate([
pageControl.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -20),
pageControl.heightAnchor.constraint(equalTo: self.view.heightAnchor, multiplier: 0.1),
pageControl.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 1),
])
}
override init(transitionStyle style: UIPageViewControllerTransitionStyle, navigationOrientation: UIPageViewControllerNavigationOrientation, options: [String : Any]? = nil) {
super.init(transitionStyle: style, navigationOrientation: navigationOrientation, options: options)
setup()
}
func setRouter(introRouter: IntroRouter) {
self.introRouter = introRouter
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setup() {
view.backgroundColor = .white
dataSource = self
}
}
private extension IntroViewController {
func IntroViewController(_ IntroViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let viewControllerIndex = self.displayedControllers.index(of: viewController) {
if viewControllerIndex == 0 {
// wrap to last page in array
return self.displayedControllers.last
} else {
// go to previous page in array
return self.displayedControllers[viewControllerIndex + 1]
}
}
return nil
}
func IntroViewController(_ IntroViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
self.pageControl.updateCurrentPageDisplay()
if let viewControllerIndex = self.displayedControllers.index(of: viewController) {
if viewControllerIndex < self.displayedControllers.count - 1 {
// go to next page in array
self.pageControl.currentPage = viewControllerIndex
return self.displayedControllers[viewControllerIndex + 1]
} else {
// wrap to first page in array
self.pageControl.currentPage = viewControllerIndex
return self.displayedControllers.first
}
}
return nil
}
func IntroViewController(_ IntroViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
// set the pageControl.currentPage to the index of the current viewController in pages
if let viewControllers = IntroViewController.viewControllers {
if let viewControllerIndex = self.displayedControllers.index(of: viewControllers[0]) {
self.pageControl.currentPage = viewControllerIndex
self.pageControl.updateCurrentPageDisplay()
}
}
}
func setupUI() {
configurePageControl()
pageControl.do {
$0.numberOfPages = 3
// $0.currentPage = 0
$0.pageIndicatorTintColor = .lightGray
$0.currentPageIndicatorTintColor = Theme.Colors.white
$0.currentPage = self.displayedControllers.count
}
}
func arrangeSubviews() {
view.addSubview(pageControl)
}
}
Just try the below code. It would be helpfull.
class PageViewModel: NSObject {
private var timer: Timer?
private var interval: Double?
var enableAutoScroll: Bool = false {
didSet {
self.toggleTimer()
}
}
var viewControllers: [UIViewController]
var currentIndex: Int = 0
var direction: UIPageViewControllerNavigationDirection = .forward
init(viewControllers: [UIViewController]) {
self.viewControllers = viewControllers
super.init()
}
override convenience init() {
self.init(viewControllers: [])
}
private func stopTimer() {
if self.timer != nil {
timer?.invalidate()
timer = nil
return
}
}
private func startTimer() {
if self.timer != nil {
return
}
if #available(iOS 10.0, *) {
if let time = interval {
timer = Timer.scheduledTimer(withTimeInterval: time, repeats: true, block: { (timer: Timer) in
self.timerFire()
})
}
} else {
// Fallback on earlier versions
if let time = interval {
timer = Timer.scheduledTimer(timeInterval: time, target: self, selector: #selector(PageViewModel.timerFire), userInfo: nil, repeats: true)
}
}
}
#objc private func timerFire() {
if direction == .forward {
var index = currentIndex
if index < self.viewControllers.count - 1 {
index += 1
} else {
index = 0
}
currentIndex = index
scrollToNextPage?(currentIndex)
pageNumberChangedObserver?(currentIndex)
} else {
var index = currentIndex
if index <= 0 {
index = (self.viewControllers.count - 1)
} else {
index -= 1
}
currentIndex = index
scrollToNextPage?(index)
pageNumberChangedObserver?(currentIndex)
}
}
// MARK: - Timer
func toggleTimer() {
if enableAutoScroll == true {
startTimer()
} else {
stopTimer()
}
}
fileprivate var scrollToNextPage: ((_ index: Int) -> Void)?
func scrollToNextPage(callBack: #escaping (_ index: Int) -> Void) {
scrollToNextPage = callBack
}
// MARK: - PageNumberChangedObserver
fileprivate var pageNumberChangedObserver: ((_ index: Int) -> Void)?
func pageNumberChanged(callBack: #escaping (_ index: Int) -> Void) {
pageNumberChangedObserver = callBack
}
}
// MARK: - UIPageViewControllerDataSource
extension PageViewModel: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = viewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
// User is on the first view controller and swiped left to loop to
// the last view controller.
guard previousIndex >= 0 else {
//return orderedViewControllers.last
// Uncommment the line below, remove the line above if you don't want the page control to loop.
return nil
}
guard viewControllers.count > previousIndex else {
return nil
}
return viewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = viewControllers.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = viewControllers.count
// User is on the last view controller and swiped right to loop to
// the first view controller.
guard orderedViewControllersCount != nextIndex else {
//return orderedViewControllers.first
// Uncommment the line below, remove the line above if you don't want the page control to loop.
return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return viewControllers[nextIndex]
}
}
// MARK: - UIPageViewControllerDelegate
extension PageViewModel: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if let pageContentViewController = pageViewController.viewControllers?[0] {
if let index = viewControllers.index(of: pageContentViewController) {
currentIndex = index
pageNumberChangedObserver?(currentIndex)
}
}
}
}
Write below code in your Container View Controller: -
// set current page to the page controller
func setNextPage(index: Int, direction: UIPageViewControllerNavigationDirection) {
if let firstViewController = pageViewModel?.viewControllers[selectedCategoryCell] {
pageViewController?.setViewControllers([firstViewController],
direction: direction,
animated: true,
completion: nil)
}
}
// MARK: - SET PAGE CONTROLLER's VIEW's
private func setupPageViewController() {
let pageViewController: UIPageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
pageViewController.dataSource = pageViewModel
pageViewController.delegate = pageViewModel
if let firstViewController = pageViewModel?.viewControllers[selectedCategoryCell] {
pageViewController.setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
for recognizer in pageViewController.gestureRecognizers {
if recognizer is UITapGestureRecognizer {
recognizer.isEnabled = false
}
}
self.addChildViewController(pageViewController)
self.containerView.addSubview(pageViewController.view)
var pageViewRect = self.containerView.bounds
if UIDevice.current.userInterfaceIdiom == .pad {
pageViewRect = pageViewRect.insetBy(dx: 40.0, dy: 40.0)
}
pageViewController.view.frame = pageViewRect
pageViewController.didMove(toParentViewController: self)
self.pageViewController = pageViewController
}
// set the selected page and accordingly set the above collection view ( that is treatment option's collection view)
func pageNumberChanged(index: Int) {
self.selectedCategoryCell = index
}
// MARK: Init View Controller for Page Controller
func initViewController(index: Int) -> UIViewController {
guard let itemsViewController = self.storyboard?.instantiateViewController(withIdentifier: "MatchesV2ViewController") as? MatchesV2ViewController else {
fatalError("Items View Controller Not Found")
}
itemsViewController.delegate = self
itemsViewController.currentMatch = matches.user[index]
return itemsViewController
}
// MARK: Setting VC In Page View Controller
func instantiateViewControllers() {
matchesScreen.removeAll()
for i in 0..<matches.user.count {
matchesScreen.append(initViewController(index: i))
}
pageViewModel = PageViewModel(viewControllers: matchesScreen)
pageViewModel?.pageNumberChanged(callBack: { (index: Int) in
self.pageNumberChanged(index: index)
})
pageViewModel?.scrollToNextPage(callBack: { (index: Int) in
self.setNextPage(index: index, direction: .forward)
})
if matchesScreen.count > 0 {
self.setupPageViewController()
}
}

UIPageViewController can return the wrong index

As the title states, there is a way it can return the wrong index. This messes up the index presentation dots at the bottom of the page. The way this is done is by skipping a page without releasing a finger from the screen. If this happens, it messes of the rest of the presentation dots.
here is what the bug it looks like in action.
And here is the code that was used for the UIPageViewController.
import UIKit
class PageViewController: UIPageViewController {
override func preferredStatusBarStyle() -> UIStatusBarStyle {
return UIStatusBarStyle.LightContent
}
private(set) lazy var orderedViewControllers: [UIViewController] = {
return [self.newColoredViewController("Green"),
self.newColoredViewController("Red"),
self.newColoredViewController("Blue")]
}()
private func newColoredViewController(color: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil) .
instantiateViewControllerWithIdentifier("\(color)ViewController")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController],
direction: .Forward,
animated: true,
completion: nil)
}
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
// Get the new view controller using segue.destinationViewController.
// Pass the selected object to the new view controller.
}
*/
}
//MARK: UIPageViewControllerDataSource
extension PageViewController: UIPageViewControllerDataSource {
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let orderedViewControllersCount = orderedViewControllers.count
guard orderedViewControllersCount != nextIndex else {
return nil
}
guard orderedViewControllersCount > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return orderedViewControllers.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first,
firstViewControllerIndex = orderedViewControllers.indexOf(firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
Any help with this would be greatly appreciated, thanks.
I made a workaround because this is still not fixed!! It has been more than 4 years darn it.
Class to subclass instead of UIPageViewController:
import UIKit
class PageController: UIPageViewController {
lazy var pages: [UIViewController] = []
var newOffset: CGFloat = 20
var showReal = true
var pageControlX: CGFloat = 0 // 0 is the middle of the screen
var pageControlY: CGFloat = 200
private var scrollView: UIScrollView?
private var scrollPoints: [UIView] = []
private var oldScrollPoints: [UIView] = []
private var indexKeeper = IndexKeeper()
private var selectedColor = UIColor(white: 1, alpha: 1)
private var unselectedColor = UIColor(white: 1, alpha: 0.2)
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// whole screen scroll
scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as? UIScrollView
scrollView!.frame = UIScreen.main.bounds
// get pageControl and scroll view from view's subviews
let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
oldScrollPoints = pageControl.subviews
// remove all constraint from view that are tied to pageControl
let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
view.removeConstraints(const)
// customize pageControl
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.frame = CGRect(x: pageControlX, y: pageControlY,
width: view.frame.width, height: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.replacePoints()
self.updateIndex()
}
}
private func replacePoints() {
for point in scrollPoints {
point.removeFromSuperview()
}
scrollPoints = []
for oldPoint in oldScrollPoints {
let newPoint = UIView(frame: oldPoint.frame)
// make rounded
newPoint.layer.borderWidth = 0
newPoint.layer.masksToBounds = false
newPoint.layer.cornerRadius = newPoint.frame.height / 2
newPoint.clipsToBounds = true
newPoint.center = CGPoint(x: oldPoint.center.x, y: newOffset)
newPoint.backgroundColor = oldPoint.backgroundColor
oldPoint.superview?.addSubview(newPoint)
scrollPoints.append(newPoint)
oldPoint.alpha = showReal ? 1 : 0
}
}
private func offsetFromFirstPage() -> CGFloat {
var coordinates: [CGFloat] = []
for (index, page) in pages.enumerated() {
let offset = scrollView!.convert(scrollView!.bounds.origin, to: page.view!)
coordinates.append(offset.x + CGFloat(index) * view.frame.width)
}
let duplicates: [CGFloat] = coordinates.enumerated().map
{ current in
if coordinates.firstIndex(of: current.element) != coordinates.lastIndex(of: current.element) {
return current.element
} else {
return .nan
}
}
return duplicates.first(where: { !$0.isNaN }) ?? 0
}
private var lastIndex: Int = 0
private func pageIndexFrom(offset: CGFloat, visibleRatio: CGFloat=0.6) -> Int {
let adjustedOffset = (offset / view.frame.width)
if adjustedOffset > CGFloat(lastIndex) + visibleRatio {
if lastIndex + 1 < pages.count {
lastIndex += 1
}
} else if adjustedOffset < CGFloat(lastIndex) - visibleRatio {
if lastIndex - 1 >= 0 {
lastIndex -= 1
}
}
return lastIndex
}
private func updateIndex() {
let fastIndex = pages.firstIndex(of: viewControllers!.first!)!
indexKeeper.fastIndexUpdate(index: fastIndex)
let offset = offsetFromFirstPage()
let slowIndex = pageIndexFrom(offset: offset)
indexKeeper.slowIndexUpdate(index: slowIndex)
for (index, point) in scrollPoints.enumerated() {
if index == indexKeeper.finalIndex {
point.backgroundColor = selectedColor
} else {
point.backgroundColor = unselectedColor
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
self.updateIndex()
}
}
}
class IndexKeeper {
var finalIndex = 0
private var slowIndex = 0
private var fastIndex = 0
func slowIndexUpdate(index: Int) {
if index != slowIndex {
slowIndex = index
finalIndex = slowIndex
}
}
func fastIndexUpdate(index: Int) {
if index != fastIndex {
fastIndex = index
finalIndex = fastIndex
}
}
}
Example Use:
import UIKit
class PageViewController: PageController, UIPageViewControllerDataSource {
private func pageInstance(name:String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
override func viewDidLoad() {
super.viewDidLoad()
pages = [
pageInstance(name: "FirstPage"),
pageInstance(name: "SecondPage"),
pageInstance(name: "ThirdPage"),
pageInstance(name: "FourthPage")
]
dataSource = self
setViewControllers([pages.first!], direction: .forward, animated: false, completion: nil)
}
// get page before current page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController), index > 0 else {
return nil
}
return pages[index - 1]
}
// get page after current page
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let index = pages.firstIndex(of: viewController), index + 1 < pages.count else {
return nil
}
return pages[index + 1]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
if let vc = viewControllers?.first {
return pages.firstIndex(of: vc)!
}
return 0
}
}

Resources