Reliably track Page Index in a UIPageViewController (Swift)












2















The problem:



I have a master UIPageViewController (MainPageVC) with three imbedded page views (A, B, & C) that are accessible both with swipe gestures and by pressing the appropriate locations in a custom page indicator* in the MainPageVC (*not a true UIPageControl but comprised of three ToggleButtons - a simple reimplementation of UIButton to become a toggle-button). My setup is as follows:



Schematic of my view hierarchy



Previous reading:
Reliable way to track Page Index in a UIPageViewController - Swift, A reliable way to get UIPageViewController current index, and UIPageViewController: return the current visible view
indicated that the best way to do this was with didFinishAnimating calls, and manually keep track of the current page index, but I'm finding that this does not deal with certain edge cases.



I have been trying to produce a safe way of keeping track of the current page index (with didFinishAnimating and willTransitionTo methods) but am having trouble with the edge case where a user is in view A, and then swipes all the way across to C (without lifting up their finger), and then beyond C, and then releasing their finger... in this instance didFinishAnimating isn't called and the app still believes it is in A (i.e. A toggle button is still pressed and pageIndex is not updated correctly by the viewControllerBefore and viewControllerAfter methods).



My code:



@IBOutlet weak var pagerView: UIView!
@IBOutlet weak var aButton: ToggleButton!
@IBOutlet weak var bButton: ToggleButton!
@IBOutlet weak var cButton: ToggleButton!

let viewControllerNames = ["aVC", "bVC", "cVC"]
lazy var buttonsArray = {
[aButton, bButton, cButton]
}()
var previousPage = "aVC"

var pageVC: UIPageViewController?

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
print("TESTING - will transition to")

let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

if currentViewControllerClass == previousPage {
return
}

let pastIndex = viewControllerNames.index(of: previousPage)
if buttonsArray[pastIndex!]?.isOn == true {
buttonsArray[pastIndex!]?.buttonPressed()
}

if let newPageButton = buttonsArray[viewControllerIndex!] {
newPageButton.buttonPressed()
}

self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
print("TESTING - did finish animating")

let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

if currentViewControllerClass == previousPage {
return
}

let pastIndex = viewControllerNames.index(of: previousPage)
if buttonsArray[pastIndex!]?.isOn == true {
buttonsArray[pastIndex!]?.buttonPressed()
}

if let newPageButton = buttonsArray[viewControllerIndex!] {
newPageButton.buttonPressed()
}

self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let onboardingViewControllerClass = String(describing: viewController.classForCoder)
let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
let newViewControllerIndex = viewControllerIndex! - 1
if(newViewControllerIndex < 0) {
return nil
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
if let vc = vc as? BaseTabVC {
vc.mainPageVC = self
vc.intendedCollectionViewHeight = pagerViewHeight
}
return vc
}
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let onboardingViewControllerClass = String(describing: viewController.classForCoder)
let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
let newViewControllerIndex = viewControllerIndex! + 1
if(newViewControllerIndex > viewControllerNames.count - 1) {
return nil
} else {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
if let vc = vc as? BaseTabVC {
vc.mainPageVC = self
vc.intendedCollectionViewHeight = pagerViewHeight
}
return vc
}
}


I'm at a loss as to how to deal with this edge case, the problem is that it can lead to fatal crashes of the app if the user then tries to press something in C that should otherwise be guaranteed to exist, and an unexpected nil or indexOutOfBounds error is thrown.










share|improve this question





























    2















    The problem:



    I have a master UIPageViewController (MainPageVC) with three imbedded page views (A, B, & C) that are accessible both with swipe gestures and by pressing the appropriate locations in a custom page indicator* in the MainPageVC (*not a true UIPageControl but comprised of three ToggleButtons - a simple reimplementation of UIButton to become a toggle-button). My setup is as follows:



    Schematic of my view hierarchy



    Previous reading:
    Reliable way to track Page Index in a UIPageViewController - Swift, A reliable way to get UIPageViewController current index, and UIPageViewController: return the current visible view
    indicated that the best way to do this was with didFinishAnimating calls, and manually keep track of the current page index, but I'm finding that this does not deal with certain edge cases.



    I have been trying to produce a safe way of keeping track of the current page index (with didFinishAnimating and willTransitionTo methods) but am having trouble with the edge case where a user is in view A, and then swipes all the way across to C (without lifting up their finger), and then beyond C, and then releasing their finger... in this instance didFinishAnimating isn't called and the app still believes it is in A (i.e. A toggle button is still pressed and pageIndex is not updated correctly by the viewControllerBefore and viewControllerAfter methods).



    My code:



    @IBOutlet weak var pagerView: UIView!
    @IBOutlet weak var aButton: ToggleButton!
    @IBOutlet weak var bButton: ToggleButton!
    @IBOutlet weak var cButton: ToggleButton!

    let viewControllerNames = ["aVC", "bVC", "cVC"]
    lazy var buttonsArray = {
    [aButton, bButton, cButton]
    }()
    var previousPage = "aVC"

    var pageVC: UIPageViewController?

    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    print("TESTING - will transition to")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

    if currentViewControllerClass == previousPage {
    return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
    buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
    newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    print("TESTING - did finish animating")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

    if currentViewControllerClass == previousPage {
    return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
    buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
    newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! - 1
    if(newViewControllerIndex < 0) {
    return nil
    } else {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
    if let vc = vc as? BaseTabVC {
    vc.mainPageVC = self
    vc.intendedCollectionViewHeight = pagerViewHeight
    }
    return vc
    }
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! + 1
    if(newViewControllerIndex > viewControllerNames.count - 1) {
    return nil
    } else {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
    if let vc = vc as? BaseTabVC {
    vc.mainPageVC = self
    vc.intendedCollectionViewHeight = pagerViewHeight
    }
    return vc
    }
    }


    I'm at a loss as to how to deal with this edge case, the problem is that it can lead to fatal crashes of the app if the user then tries to press something in C that should otherwise be guaranteed to exist, and an unexpected nil or indexOutOfBounds error is thrown.










    share|improve this question



























      2












      2








      2








      The problem:



      I have a master UIPageViewController (MainPageVC) with three imbedded page views (A, B, & C) that are accessible both with swipe gestures and by pressing the appropriate locations in a custom page indicator* in the MainPageVC (*not a true UIPageControl but comprised of three ToggleButtons - a simple reimplementation of UIButton to become a toggle-button). My setup is as follows:



      Schematic of my view hierarchy



      Previous reading:
      Reliable way to track Page Index in a UIPageViewController - Swift, A reliable way to get UIPageViewController current index, and UIPageViewController: return the current visible view
      indicated that the best way to do this was with didFinishAnimating calls, and manually keep track of the current page index, but I'm finding that this does not deal with certain edge cases.



      I have been trying to produce a safe way of keeping track of the current page index (with didFinishAnimating and willTransitionTo methods) but am having trouble with the edge case where a user is in view A, and then swipes all the way across to C (without lifting up their finger), and then beyond C, and then releasing their finger... in this instance didFinishAnimating isn't called and the app still believes it is in A (i.e. A toggle button is still pressed and pageIndex is not updated correctly by the viewControllerBefore and viewControllerAfter methods).



      My code:



      @IBOutlet weak var pagerView: UIView!
      @IBOutlet weak var aButton: ToggleButton!
      @IBOutlet weak var bButton: ToggleButton!
      @IBOutlet weak var cButton: ToggleButton!

      let viewControllerNames = ["aVC", "bVC", "cVC"]
      lazy var buttonsArray = {
      [aButton, bButton, cButton]
      }()
      var previousPage = "aVC"

      var pageVC: UIPageViewController?

      func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
      print("TESTING - will transition to")

      let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
      let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

      if currentViewControllerClass == previousPage {
      return
      }

      let pastIndex = viewControllerNames.index(of: previousPage)
      if buttonsArray[pastIndex!]?.isOn == true {
      buttonsArray[pastIndex!]?.buttonPressed()
      }

      if let newPageButton = buttonsArray[viewControllerIndex!] {
      newPageButton.buttonPressed()
      }

      self.previousPage = currentViewControllerClass
      }

      func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
      print("TESTING - did finish animating")

      let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

      if currentViewControllerClass == previousPage {
      return
      }

      let pastIndex = viewControllerNames.index(of: previousPage)
      if buttonsArray[pastIndex!]?.isOn == true {
      buttonsArray[pastIndex!]?.buttonPressed()
      }

      if let newPageButton = buttonsArray[viewControllerIndex!] {
      newPageButton.buttonPressed()
      }

      self.previousPage = currentViewControllerClass
      }

      func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
      let onboardingViewControllerClass = String(describing: viewController.classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
      let newViewControllerIndex = viewControllerIndex! - 1
      if(newViewControllerIndex < 0) {
      return nil
      } else {
      let storyboard = UIStoryboard(name: "Main", bundle: nil)
      let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
      if let vc = vc as? BaseTabVC {
      vc.mainPageVC = self
      vc.intendedCollectionViewHeight = pagerViewHeight
      }
      return vc
      }
      }

      func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
      let onboardingViewControllerClass = String(describing: viewController.classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
      let newViewControllerIndex = viewControllerIndex! + 1
      if(newViewControllerIndex > viewControllerNames.count - 1) {
      return nil
      } else {
      let storyboard = UIStoryboard(name: "Main", bundle: nil)
      let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
      if let vc = vc as? BaseTabVC {
      vc.mainPageVC = self
      vc.intendedCollectionViewHeight = pagerViewHeight
      }
      return vc
      }
      }


      I'm at a loss as to how to deal with this edge case, the problem is that it can lead to fatal crashes of the app if the user then tries to press something in C that should otherwise be guaranteed to exist, and an unexpected nil or indexOutOfBounds error is thrown.










      share|improve this question
















      The problem:



      I have a master UIPageViewController (MainPageVC) with three imbedded page views (A, B, & C) that are accessible both with swipe gestures and by pressing the appropriate locations in a custom page indicator* in the MainPageVC (*not a true UIPageControl but comprised of three ToggleButtons - a simple reimplementation of UIButton to become a toggle-button). My setup is as follows:



      Schematic of my view hierarchy



      Previous reading:
      Reliable way to track Page Index in a UIPageViewController - Swift, A reliable way to get UIPageViewController current index, and UIPageViewController: return the current visible view
      indicated that the best way to do this was with didFinishAnimating calls, and manually keep track of the current page index, but I'm finding that this does not deal with certain edge cases.



      I have been trying to produce a safe way of keeping track of the current page index (with didFinishAnimating and willTransitionTo methods) but am having trouble with the edge case where a user is in view A, and then swipes all the way across to C (without lifting up their finger), and then beyond C, and then releasing their finger... in this instance didFinishAnimating isn't called and the app still believes it is in A (i.e. A toggle button is still pressed and pageIndex is not updated correctly by the viewControllerBefore and viewControllerAfter methods).



      My code:



      @IBOutlet weak var pagerView: UIView!
      @IBOutlet weak var aButton: ToggleButton!
      @IBOutlet weak var bButton: ToggleButton!
      @IBOutlet weak var cButton: ToggleButton!

      let viewControllerNames = ["aVC", "bVC", "cVC"]
      lazy var buttonsArray = {
      [aButton, bButton, cButton]
      }()
      var previousPage = "aVC"

      var pageVC: UIPageViewController?

      func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
      print("TESTING - will transition to")

      let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
      let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

      if currentViewControllerClass == previousPage {
      return
      }

      let pastIndex = viewControllerNames.index(of: previousPage)
      if buttonsArray[pastIndex!]?.isOn == true {
      buttonsArray[pastIndex!]?.buttonPressed()
      }

      if let newPageButton = buttonsArray[viewControllerIndex!] {
      newPageButton.buttonPressed()
      }

      self.previousPage = currentViewControllerClass
      }

      func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
      print("TESTING - did finish animating")

      let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

      if currentViewControllerClass == previousPage {
      return
      }

      let pastIndex = viewControllerNames.index(of: previousPage)
      if buttonsArray[pastIndex!]?.isOn == true {
      buttonsArray[pastIndex!]?.buttonPressed()
      }

      if let newPageButton = buttonsArray[viewControllerIndex!] {
      newPageButton.buttonPressed()
      }

      self.previousPage = currentViewControllerClass
      }

      func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
      let onboardingViewControllerClass = String(describing: viewController.classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
      let newViewControllerIndex = viewControllerIndex! - 1
      if(newViewControllerIndex < 0) {
      return nil
      } else {
      let storyboard = UIStoryboard(name: "Main", bundle: nil)
      let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
      if let vc = vc as? BaseTabVC {
      vc.mainPageVC = self
      vc.intendedCollectionViewHeight = pagerViewHeight
      }
      return vc
      }
      }

      func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
      let onboardingViewControllerClass = String(describing: viewController.classForCoder)
      let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
      let newViewControllerIndex = viewControllerIndex! + 1
      if(newViewControllerIndex > viewControllerNames.count - 1) {
      return nil
      } else {
      let storyboard = UIStoryboard(name: "Main", bundle: nil)
      let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
      if let vc = vc as? BaseTabVC {
      vc.mainPageVC = self
      vc.intendedCollectionViewHeight = pagerViewHeight
      }
      return vc
      }
      }


      I'm at a loss as to how to deal with this edge case, the problem is that it can lead to fatal crashes of the app if the user then tries to press something in C that should otherwise be guaranteed to exist, and an unexpected nil or indexOutOfBounds error is thrown.







      ios swift uipageviewcontroller uipagecontrol






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Nov 23 '18 at 14:18







      zb1995

















      asked Nov 23 '18 at 12:37









      zb1995zb1995

      104213




      104213
























          2 Answers
          2






          active

          oldest

          votes


















          1














          Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.



          The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)



          (I did a simple test of this approach and it worked. I didn't test exhaustively however.)



          The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.






          share|improve this answer
























          • Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

            – zb1995
            Nov 23 '18 at 14:12











          • I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

            – zb1995
            Mar 12 at 15:32



















          0














          Own Solution



          I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.



          The solution is as follows:



          Method




          • Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).

          • Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).

          • Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).

          • Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.

          • As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.


          Code



          class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

          fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
          fileprivate let cellId = "cellId"

          fileprivate let pages = ["aVC", "bVC", "cVC"]

          let collectionView: UICollectionView = {
          let layout = UICollectionViewFlowLayout()
          layout.minimumLineSpacing = 0
          layout.scrollDirection = .horizontal
          let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
          cv.backgroundColor = .white
          cv.showsVerticalScrollIndicator = false
          cv.showsHorizontalScrollIndicator = false
          return cv
          }()


          override func viewDidLoad() {
          super.viewDidLoad()

          menuController.delegate = self

          setupLayout()
          }

          fileprivate func setupLayout() {
          guard let menuView = menuController.view else { return }

          view.addSubview(menuView)
          view.addSubview(collectionView)

          collectionView.dataSource = self
          collectionView.delegate = self


          //Setup constraints (placing the menuView above the collectionView

          collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

          //Make the collection view behave like a pager view (no overscroll, paging enabled)
          collectionView.isPagingEnabled = true
          collectionView.bounces = false
          collectionView.allowsSelection = true

          menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

          }

          }

          extension MainPagerVC: MenuVCDelegate {

          // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
          func didTapMenuItem(indexPath: IndexPath) {
          collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
          }

          }

          extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

          func scrollViewDidScroll(_ scrollView: UIScrollView) {
          let x = scrollView.contentOffset.x
          let offset = x / pages.count
          menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
          }

          func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
          let item = Int(scrollView.contentOffset.x / view.frame.width)
          let indexPath = IndexPath(item: item, section: 0)
          collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
          }

          func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
          let x = targetContentOffset.pointee.x
          let item = Int(x / view.frame.width)
          let indexPath = IndexPath(item: item, section: 0)
          menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
          }


          func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
          return pages.count
          }

          func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

          return cell
          }

          func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
          return .init(width: view.frame.width, height: collectionView.bounds.height)
          }

          }

          class MainCell: UICollectionViewCell {

          override init(frame: CGRect) {
          super.init(frame: frame)

          // Custom UIColor extension to return a random colour (to check that everything is working)
          backgroundColor = UIColor().random()

          }

          required init?(coder aDecoder: NSCoder) {
          fatalError()
          }
          }

          protocol MenuVCDelegate {
          func didTapMenuItem(indexPath: IndexPath)
          }

          class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

          fileprivate let cellId = "cellId"
          fileprivate let menuItems = ["A", "B", "C"]

          var delegate: MenuVCDelegate?

          //Sliding bar indicator (slightly different from original question - like Reddit)
          let menuBar: UIView = {
          let v = UIView()
          v.backgroundColor = .red
          return v
          }()

          //1px view to visually separate MenuBar region from "pager"-views
          let menuSeparator: UIView = {
          let v = UIView()
          v.backgroundColor = .gray
          return v
          }()

          override func viewDidLoad() {
          super.viewDidLoad()

          collectionView.backgroundColor = .white
          collectionView.allowsSelection = true
          collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

          if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
          layout.scrollDirection = .horizontal
          layout.minimumLineSpacing = 0
          layout.minimumInteritemSpacing = 0
          }

          //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
          }

          override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
          delegate?.didTapMenuItem(indexPath: indexPath)
          }

          override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
          return menuItems.count
          }

          override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
          let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
          cell.label.text = menuItems[indexPath.item]

          return cell
          }

          func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
          let width = view.frame.width
          return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
          }

          }

          class MenuCell: UICollectionViewCell {

          let label: UILabel = {
          let l = UILabel()
          l.text = "Menu Item"
          l.textAlignment = .center
          l.textColor = .gray
          return l
          }()

          override var isSelected: Bool {
          didSet {
          label.textColor = isSelected ? .black : .gray
          }
          }

          override init(frame: CGRect) {
          super.init(frame: frame)
          //Add label to view and setup constraints to fill Cell
          }

          required init?(coder aDecoder: NSCoder) {
          fatalError()
          }
          }


          References




          1. A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"






          share|improve this answer


























            Your Answer






            StackExchange.ifUsing("editor", function () {
            StackExchange.using("externalEditor", function () {
            StackExchange.using("snippets", function () {
            StackExchange.snippets.init();
            });
            });
            }, "code-snippets");

            StackExchange.ready(function() {
            var channelOptions = {
            tags: "".split(" "),
            id: "1"
            };
            initTagRenderer("".split(" "), "".split(" "), channelOptions);

            StackExchange.using("externalEditor", function() {
            // Have to fire editor after snippets, if snippets enabled
            if (StackExchange.settings.snippets.snippetsEnabled) {
            StackExchange.using("snippets", function() {
            createEditor();
            });
            }
            else {
            createEditor();
            }
            });

            function createEditor() {
            StackExchange.prepareEditor({
            heartbeatType: 'answer',
            autoActivateHeartbeat: false,
            convertImagesToLinks: true,
            noModals: true,
            showLowRepImageUploadWarning: true,
            reputationToPostImages: 10,
            bindNavPrevention: true,
            postfix: "",
            imageUploader: {
            brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
            contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
            allowUrls: true
            },
            onDemand: true,
            discardSelector: ".discard-answer"
            ,immediatelyShowMarkdownHelp:true
            });


            }
            });














            draft saved

            draft discarded


















            StackExchange.ready(
            function () {
            StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53446854%2freliably-track-page-index-in-a-uipageviewcontroller-swift%23new-answer', 'question_page');
            }
            );

            Post as a guest















            Required, but never shown

























            2 Answers
            2






            active

            oldest

            votes








            2 Answers
            2






            active

            oldest

            votes









            active

            oldest

            votes






            active

            oldest

            votes









            1














            Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.



            The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)



            (I did a simple test of this approach and it worked. I didn't test exhaustively however.)



            The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.






            share|improve this answer
























            • Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

              – zb1995
              Nov 23 '18 at 14:12











            • I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

              – zb1995
              Mar 12 at 15:32
















            1














            Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.



            The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)



            (I did a simple test of this approach and it worked. I didn't test exhaustively however.)



            The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.






            share|improve this answer
























            • Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

              – zb1995
              Nov 23 '18 at 14:12











            • I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

              – zb1995
              Mar 12 at 15:32














            1












            1








            1







            Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.



            The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)



            (I did a simple test of this approach and it worked. I didn't test exhaustively however.)



            The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.






            share|improve this answer













            Very well written question. Especially for a newbie. (Voted.) You clearly state the problem you're having, including illustrations and your current code.



            The solution I proposed in another thread was to subclass UIPageControl and have it implement a didSet on its currentPage property. You can then have the page control notify the view controller of the current page index. (By giving your custom subclass a delegate property, by sending a notification center message, or whatever method best fits your needs.)



            (I did a simple test of this approach and it worked. I didn't test exhaustively however.)



            The fact that the UIPageViewController reliably updates the page control but that there's no reliable, obvious way to figure out the current page index seems like an oversight in the design of this class.







            share|improve this answer












            share|improve this answer



            share|improve this answer










            answered Nov 23 '18 at 14:01









            Duncan CDuncan C

            94k13115202




            94k13115202













            • Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

              – zb1995
              Nov 23 '18 at 14:12











            • I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

              – zb1995
              Mar 12 at 15:32



















            • Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

              – zb1995
              Nov 23 '18 at 14:12











            • I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

              – zb1995
              Mar 12 at 15:32

















            Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

            – zb1995
            Nov 23 '18 at 14:12





            Thanks for your suggestion, I shall give that a go and get back to you. I started going down the rabbit hole of trying to prevent "overscroll" of the views inside a UIPageViewController but was getting nowhere. It was probably my inexperience / not quite wording my searches correctly as I was constantly pointed towards UIScrollView's bounce property (which wasn't relevant). Do you think that attempting to disable swiping gestures to the left (on the rightmost page) and vice versa would also adequately eliminate this edge case?

            – zb1995
            Nov 23 '18 at 14:12













            I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

            – zb1995
            Mar 12 at 15:32





            I found the solution to this, to not use a UIPageViewController and to use a UICollectionViewController instead (as its underlying scrollView can be used to work out "page"-position) - I put my updated solution up as an answer. Thanks for helping me to help myself, Duncan :)

            – zb1995
            Mar 12 at 15:32













            0














            Own Solution



            I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.



            The solution is as follows:



            Method




            • Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).

            • Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).

            • Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).

            • Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.

            • As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.


            Code



            class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

            fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
            fileprivate let cellId = "cellId"

            fileprivate let pages = ["aVC", "bVC", "cVC"]

            let collectionView: UICollectionView = {
            let layout = UICollectionViewFlowLayout()
            layout.minimumLineSpacing = 0
            layout.scrollDirection = .horizontal
            let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
            cv.backgroundColor = .white
            cv.showsVerticalScrollIndicator = false
            cv.showsHorizontalScrollIndicator = false
            return cv
            }()


            override func viewDidLoad() {
            super.viewDidLoad()

            menuController.delegate = self

            setupLayout()
            }

            fileprivate func setupLayout() {
            guard let menuView = menuController.view else { return }

            view.addSubview(menuView)
            view.addSubview(collectionView)

            collectionView.dataSource = self
            collectionView.delegate = self


            //Setup constraints (placing the menuView above the collectionView

            collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

            //Make the collection view behave like a pager view (no overscroll, paging enabled)
            collectionView.isPagingEnabled = true
            collectionView.bounces = false
            collectionView.allowsSelection = true

            menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

            }

            }

            extension MainPagerVC: MenuVCDelegate {

            // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
            func didTapMenuItem(indexPath: IndexPath) {
            collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
            }

            }

            extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

            func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let x = scrollView.contentOffset.x
            let offset = x / pages.count
            menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
            }

            func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
            let item = Int(scrollView.contentOffset.x / view.frame.width)
            let indexPath = IndexPath(item: item, section: 0)
            collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
            }

            func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
            let x = targetContentOffset.pointee.x
            let item = Int(x / view.frame.width)
            let indexPath = IndexPath(item: item, section: 0)
            menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
            }


            func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return pages.count
            }

            func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

            return cell
            }

            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            return .init(width: view.frame.width, height: collectionView.bounds.height)
            }

            }

            class MainCell: UICollectionViewCell {

            override init(frame: CGRect) {
            super.init(frame: frame)

            // Custom UIColor extension to return a random colour (to check that everything is working)
            backgroundColor = UIColor().random()

            }

            required init?(coder aDecoder: NSCoder) {
            fatalError()
            }
            }

            protocol MenuVCDelegate {
            func didTapMenuItem(indexPath: IndexPath)
            }

            class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

            fileprivate let cellId = "cellId"
            fileprivate let menuItems = ["A", "B", "C"]

            var delegate: MenuVCDelegate?

            //Sliding bar indicator (slightly different from original question - like Reddit)
            let menuBar: UIView = {
            let v = UIView()
            v.backgroundColor = .red
            return v
            }()

            //1px view to visually separate MenuBar region from "pager"-views
            let menuSeparator: UIView = {
            let v = UIView()
            v.backgroundColor = .gray
            return v
            }()

            override func viewDidLoad() {
            super.viewDidLoad()

            collectionView.backgroundColor = .white
            collectionView.allowsSelection = true
            collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

            if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
            }

            //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
            }

            override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
            delegate?.didTapMenuItem(indexPath: indexPath)
            }

            override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
            return menuItems.count
            }

            override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
            cell.label.text = menuItems[indexPath.item]

            return cell
            }

            func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
            let width = view.frame.width
            return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
            }

            }

            class MenuCell: UICollectionViewCell {

            let label: UILabel = {
            let l = UILabel()
            l.text = "Menu Item"
            l.textAlignment = .center
            l.textColor = .gray
            return l
            }()

            override var isSelected: Bool {
            didSet {
            label.textColor = isSelected ? .black : .gray
            }
            }

            override init(frame: CGRect) {
            super.init(frame: frame)
            //Add label to view and setup constraints to fill Cell
            }

            required init?(coder aDecoder: NSCoder) {
            fatalError()
            }
            }


            References




            1. A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"






            share|improve this answer






























              0














              Own Solution



              I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.



              The solution is as follows:



              Method




              • Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).

              • Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).

              • Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).

              • Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.

              • As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.


              Code



              class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

              fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
              fileprivate let cellId = "cellId"

              fileprivate let pages = ["aVC", "bVC", "cVC"]

              let collectionView: UICollectionView = {
              let layout = UICollectionViewFlowLayout()
              layout.minimumLineSpacing = 0
              layout.scrollDirection = .horizontal
              let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
              cv.backgroundColor = .white
              cv.showsVerticalScrollIndicator = false
              cv.showsHorizontalScrollIndicator = false
              return cv
              }()


              override func viewDidLoad() {
              super.viewDidLoad()

              menuController.delegate = self

              setupLayout()
              }

              fileprivate func setupLayout() {
              guard let menuView = menuController.view else { return }

              view.addSubview(menuView)
              view.addSubview(collectionView)

              collectionView.dataSource = self
              collectionView.delegate = self


              //Setup constraints (placing the menuView above the collectionView

              collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

              //Make the collection view behave like a pager view (no overscroll, paging enabled)
              collectionView.isPagingEnabled = true
              collectionView.bounces = false
              collectionView.allowsSelection = true

              menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

              }

              }

              extension MainPagerVC: MenuVCDelegate {

              // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
              func didTapMenuItem(indexPath: IndexPath) {
              collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
              }

              }

              extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

              func scrollViewDidScroll(_ scrollView: UIScrollView) {
              let x = scrollView.contentOffset.x
              let offset = x / pages.count
              menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
              }

              func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
              let item = Int(scrollView.contentOffset.x / view.frame.width)
              let indexPath = IndexPath(item: item, section: 0)
              collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
              }

              func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
              let x = targetContentOffset.pointee.x
              let item = Int(x / view.frame.width)
              let indexPath = IndexPath(item: item, section: 0)
              menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
              }


              func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
              return pages.count
              }

              func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
              let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

              return cell
              }

              func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
              return .init(width: view.frame.width, height: collectionView.bounds.height)
              }

              }

              class MainCell: UICollectionViewCell {

              override init(frame: CGRect) {
              super.init(frame: frame)

              // Custom UIColor extension to return a random colour (to check that everything is working)
              backgroundColor = UIColor().random()

              }

              required init?(coder aDecoder: NSCoder) {
              fatalError()
              }
              }

              protocol MenuVCDelegate {
              func didTapMenuItem(indexPath: IndexPath)
              }

              class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

              fileprivate let cellId = "cellId"
              fileprivate let menuItems = ["A", "B", "C"]

              var delegate: MenuVCDelegate?

              //Sliding bar indicator (slightly different from original question - like Reddit)
              let menuBar: UIView = {
              let v = UIView()
              v.backgroundColor = .red
              return v
              }()

              //1px view to visually separate MenuBar region from "pager"-views
              let menuSeparator: UIView = {
              let v = UIView()
              v.backgroundColor = .gray
              return v
              }()

              override func viewDidLoad() {
              super.viewDidLoad()

              collectionView.backgroundColor = .white
              collectionView.allowsSelection = true
              collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

              if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
              layout.scrollDirection = .horizontal
              layout.minimumLineSpacing = 0
              layout.minimumInteritemSpacing = 0
              }

              //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
              }

              override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
              delegate?.didTapMenuItem(indexPath: indexPath)
              }

              override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
              return menuItems.count
              }

              override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
              let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
              cell.label.text = menuItems[indexPath.item]

              return cell
              }

              func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
              let width = view.frame.width
              return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
              }

              }

              class MenuCell: UICollectionViewCell {

              let label: UILabel = {
              let l = UILabel()
              l.text = "Menu Item"
              l.textAlignment = .center
              l.textColor = .gray
              return l
              }()

              override var isSelected: Bool {
              didSet {
              label.textColor = isSelected ? .black : .gray
              }
              }

              override init(frame: CGRect) {
              super.init(frame: frame)
              //Add label to view and setup constraints to fill Cell
              }

              required init?(coder aDecoder: NSCoder) {
              fatalError()
              }
              }


              References




              1. A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"






              share|improve this answer




























                0












                0








                0







                Own Solution



                I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.



                The solution is as follows:



                Method




                • Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).

                • Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).

                • Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).

                • Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.

                • As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.


                Code



                class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

                fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
                fileprivate let cellId = "cellId"

                fileprivate let pages = ["aVC", "bVC", "cVC"]

                let collectionView: UICollectionView = {
                let layout = UICollectionViewFlowLayout()
                layout.minimumLineSpacing = 0
                layout.scrollDirection = .horizontal
                let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
                cv.backgroundColor = .white
                cv.showsVerticalScrollIndicator = false
                cv.showsHorizontalScrollIndicator = false
                return cv
                }()


                override func viewDidLoad() {
                super.viewDidLoad()

                menuController.delegate = self

                setupLayout()
                }

                fileprivate func setupLayout() {
                guard let menuView = menuController.view else { return }

                view.addSubview(menuView)
                view.addSubview(collectionView)

                collectionView.dataSource = self
                collectionView.delegate = self


                //Setup constraints (placing the menuView above the collectionView

                collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

                //Make the collection view behave like a pager view (no overscroll, paging enabled)
                collectionView.isPagingEnabled = true
                collectionView.bounces = false
                collectionView.allowsSelection = true

                menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

                }

                }

                extension MainPagerVC: MenuVCDelegate {

                // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
                func didTapMenuItem(indexPath: IndexPath) {
                collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
                }

                }

                extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

                func scrollViewDidScroll(_ scrollView: UIScrollView) {
                let x = scrollView.contentOffset.x
                let offset = x / pages.count
                menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
                }

                func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
                let item = Int(scrollView.contentOffset.x / view.frame.width)
                let indexPath = IndexPath(item: item, section: 0)
                collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
                }

                func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
                let x = targetContentOffset.pointee.x
                let item = Int(x / view.frame.width)
                let indexPath = IndexPath(item: item, section: 0)
                menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
                }


                func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                return pages.count
                }

                func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

                return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
                return .init(width: view.frame.width, height: collectionView.bounds.height)
                }

                }

                class MainCell: UICollectionViewCell {

                override init(frame: CGRect) {
                super.init(frame: frame)

                // Custom UIColor extension to return a random colour (to check that everything is working)
                backgroundColor = UIColor().random()

                }

                required init?(coder aDecoder: NSCoder) {
                fatalError()
                }
                }

                protocol MenuVCDelegate {
                func didTapMenuItem(indexPath: IndexPath)
                }

                class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

                fileprivate let cellId = "cellId"
                fileprivate let menuItems = ["A", "B", "C"]

                var delegate: MenuVCDelegate?

                //Sliding bar indicator (slightly different from original question - like Reddit)
                let menuBar: UIView = {
                let v = UIView()
                v.backgroundColor = .red
                return v
                }()

                //1px view to visually separate MenuBar region from "pager"-views
                let menuSeparator: UIView = {
                let v = UIView()
                v.backgroundColor = .gray
                return v
                }()

                override func viewDidLoad() {
                super.viewDidLoad()

                collectionView.backgroundColor = .white
                collectionView.allowsSelection = true
                collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

                if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
                layout.scrollDirection = .horizontal
                layout.minimumLineSpacing = 0
                layout.minimumInteritemSpacing = 0
                }

                //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
                }

                override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
                delegate?.didTapMenuItem(indexPath: indexPath)
                }

                override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                return menuItems.count
                }

                override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
                cell.label.text = menuItems[indexPath.item]

                return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
                let width = view.frame.width
                return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
                }

                }

                class MenuCell: UICollectionViewCell {

                let label: UILabel = {
                let l = UILabel()
                l.text = "Menu Item"
                l.textAlignment = .center
                l.textColor = .gray
                return l
                }()

                override var isSelected: Bool {
                didSet {
                label.textColor = isSelected ? .black : .gray
                }
                }

                override init(frame: CGRect) {
                super.init(frame: frame)
                //Add label to view and setup constraints to fill Cell
                }

                required init?(coder aDecoder: NSCoder) {
                fatalError()
                }
                }


                References




                1. A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"






                share|improve this answer















                Own Solution



                I found the solution to this: don't use a UIPageView(Controller), use a CollectionView(Controller) instead. It is MUCH easier to keep track of the position of a collection view than to try and manually keep track of the current page in a UIPageViewController.



                The solution is as follows:



                Method




                • Refactor MainPagerVC as a CollectionView(Controller) (or as a regular VC that conforms to the UICollectionViewDelegate UICollectionViewDataSource protocols).

                • Set each page (aVC, bVC, and cVC) as a UICollectionViewCell subclass (MainCell).

                • Set each of these pages to fill the MainPagerVC.collectionView within the screen's bounds - CGSize(width: view.frame.width, height: collectionView.bounds.height).

                • Refactor the toggle-buttons at the top (A, B, and C) as three UICollectionViewCell subclasses (MenuCell) in a MenuController (itself a UICollectionViewController.

                • As collection views inherit from UIScrollView you can implement scrollViewDidScroll, scrollViewDidEndScrollingAnimation and scrollViewWillEndDragging methods, along with delegation (with didSelectItemAt indexPath) to couple the MainPagerVC and MenuController collection views.


                Code



                class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

                fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
                fileprivate let cellId = "cellId"

                fileprivate let pages = ["aVC", "bVC", "cVC"]

                let collectionView: UICollectionView = {
                let layout = UICollectionViewFlowLayout()
                layout.minimumLineSpacing = 0
                layout.scrollDirection = .horizontal
                let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
                cv.backgroundColor = .white
                cv.showsVerticalScrollIndicator = false
                cv.showsHorizontalScrollIndicator = false
                return cv
                }()


                override func viewDidLoad() {
                super.viewDidLoad()

                menuController.delegate = self

                setupLayout()
                }

                fileprivate func setupLayout() {
                guard let menuView = menuController.view else { return }

                view.addSubview(menuView)
                view.addSubview(collectionView)

                collectionView.dataSource = self
                collectionView.delegate = self


                //Setup constraints (placing the menuView above the collectionView

                collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

                //Make the collection view behave like a pager view (no overscroll, paging enabled)
                collectionView.isPagingEnabled = true
                collectionView.bounces = false
                collectionView.allowsSelection = true

                menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

                }

                }

                extension MainPagerVC: MenuVCDelegate {

                // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
                func didTapMenuItem(indexPath: IndexPath) {
                collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
                }

                }

                extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

                func scrollViewDidScroll(_ scrollView: UIScrollView) {
                let x = scrollView.contentOffset.x
                let offset = x / pages.count
                menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
                }

                func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
                let item = Int(scrollView.contentOffset.x / view.frame.width)
                let indexPath = IndexPath(item: item, section: 0)
                collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
                }

                func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
                let x = targetContentOffset.pointee.x
                let item = Int(x / view.frame.width)
                let indexPath = IndexPath(item: item, section: 0)
                menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
                }


                func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                return pages.count
                }

                func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

                return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
                return .init(width: view.frame.width, height: collectionView.bounds.height)
                }

                }

                class MainCell: UICollectionViewCell {

                override init(frame: CGRect) {
                super.init(frame: frame)

                // Custom UIColor extension to return a random colour (to check that everything is working)
                backgroundColor = UIColor().random()

                }

                required init?(coder aDecoder: NSCoder) {
                fatalError()
                }
                }

                protocol MenuVCDelegate {
                func didTapMenuItem(indexPath: IndexPath)
                }

                class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

                fileprivate let cellId = "cellId"
                fileprivate let menuItems = ["A", "B", "C"]

                var delegate: MenuVCDelegate?

                //Sliding bar indicator (slightly different from original question - like Reddit)
                let menuBar: UIView = {
                let v = UIView()
                v.backgroundColor = .red
                return v
                }()

                //1px view to visually separate MenuBar region from "pager"-views
                let menuSeparator: UIView = {
                let v = UIView()
                v.backgroundColor = .gray
                return v
                }()

                override func viewDidLoad() {
                super.viewDidLoad()

                collectionView.backgroundColor = .white
                collectionView.allowsSelection = true
                collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

                if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
                layout.scrollDirection = .horizontal
                layout.minimumLineSpacing = 0
                layout.minimumInteritemSpacing = 0
                }

                //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
                }

                override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
                delegate?.didTapMenuItem(indexPath: indexPath)
                }

                override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
                return menuItems.count
                }

                override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
                cell.label.text = menuItems[indexPath.item]

                return cell
                }

                func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
                let width = view.frame.width
                return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
                }

                }

                class MenuCell: UICollectionViewCell {

                let label: UILabel = {
                let l = UILabel()
                l.text = "Menu Item"
                l.textAlignment = .center
                l.textColor = .gray
                return l
                }()

                override var isSelected: Bool {
                didSet {
                label.textColor = isSelected ? .black : .gray
                }
                }

                override init(frame: CGRect) {
                super.init(frame: frame)
                //Add label to view and setup constraints to fill Cell
                }

                required init?(coder aDecoder: NSCoder) {
                fatalError()
                }
                }


                References




                1. A "Lets Build That App" YouTube Video: "We Made It on /r/iosprogramming! Live coding swiping pages feature"







                share|improve this answer














                share|improve this answer



                share|improve this answer








                edited Feb 21 at 9:58

























                answered Feb 21 at 9:46









                zb1995zb1995

                104213




                104213






























                    draft saved

                    draft discarded




















































                    Thanks for contributing an answer to Stack Overflow!


                    • Please be sure to answer the question. Provide details and share your research!

                    But avoid



                    • Asking for help, clarification, or responding to other answers.

                    • Making statements based on opinion; back them up with references or personal experience.


                    To learn more, see our tips on writing great answers.




                    draft saved


                    draft discarded














                    StackExchange.ready(
                    function () {
                    StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53446854%2freliably-track-page-index-in-a-uipageviewcontroller-swift%23new-answer', 'question_page');
                    }
                    );

                    Post as a guest















                    Required, but never shown





















































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown

































                    Required, but never shown














                    Required, but never shown












                    Required, but never shown







                    Required, but never shown







                    這個網誌中的熱門文章

                    Tangent Lines Diagram Along Smooth Curve

                    Yusuf al-Mu'taman ibn Hud

                    Zucchini