7

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.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Zachary Vincze
  • 427
  • 7
  • 24
  • 1
    Based on the video you posted that sounds like a framework bug. I suggest logging on to the Apple bug reporter and opening a bug on it. – Duncan C Apr 26 '16 at 00:15
  • @DuncanC I suppose so, I'm just afraid it may just be an error with the code. I don't want to have to submit a bug report if it's not a framework bug, but rather a flaw in my code. – Zachary Vincze Apr 26 '16 at 01:49
  • How you manage to swipe with one finger and skip page? unless your pages are not as wide as your pagecontroller's frame, that could be what would confuse the pagecontroller – user3802077 Apr 26 '16 at 03:52
  • 1
    @user3802077 I didn't swipe with only one finger. I made the PageController think that I swiped with one. I carefully swiped one finger across without taking it off, then I put another finger one the screen while relessing the first one. That second finger could now be used to skip the whole page. – Zachary Vincze Apr 26 '16 at 11:16
  • I'm going to log a bug as well. I can easily reproduce the issue. – Mark Moeykens Nov 11 '16 at 23:33
  • I've encountered this when changing pages programmatically as well: https://stackoverflow.com/questions/56266576/uipageviewcontroller-position-is-messed-up-after-setting-new-page-programmatical – Oscar Aug 20 '19 at 06:13
  • Probably the same bug as this one: https://stackoverflow.com/q/12939280/341994 – matt Jul 19 '20 at 20:52

1 Answers1

0

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
    }
}
Hadus
  • 1,551
  • 11
  • 22
  • But doesn't any of the fixes at https://stackoverflow.com/q/12939280/341994 fix it? – matt Jul 19 '20 at 20:53
  • Those are about programmatically moving to a different page and then the order of the pages not being correct. https://stackoverflow.com/a/12939384/6304086 shows in a video what they are talking about. – Hadus Jul 20 '20 at 11:13
  • Anyhow those fixes don't work because all they do is programatically navigate in different ways. – Hadus Jul 20 '20 at 11:16