12

We have left and right buttons set up for the user to page through different cars quickly. Our Page View Controller loses the view controller if the user taps quickly to the next page 10 or more times.

Here is the vehicle page with the car showing correctly (blurred to hide non-relevant information). See image here:

Shows Vehicle Correctly

If scrolling animation is on (true), it loses the vehicle page after tapping the right arrow 6 or more times quickly. See image here:

enter image description here

Code:

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
    let viewControllers = [viewController]
    let isAnimated = true // false always works. However, animation is required.
    setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: nil)
}

While debugging and when the page view controller has stopped showing the cars, I ensured that the view controller being set is not nil and the listing (car) is also non-nil.

I tried a variant of the solution from UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source? where the completion block is used. However, it did not work.

weak var pvcw: UIPageViewController? = self
setViewControllers(viewControllers, direction: direction, animated: true, completion: {(_ finished: Bool) -> Void in
    let pvcs: UIPageViewController? = pvcw
    if pvcs == nil {
        return
    }
    DispatchQueue.main.async(execute: {() -> Void in
        pvcs?.setViewControllers(viewControllers, direction: direction, animated: false) {(_ finished: Bool) -> Void in }
    })
})

Any ideas? Thank you.

Update

I noticed that sometimes the contained View Controller can be off centered as opposed to entirely missing.

View Controller Off Centered

I looked deeper into the scenario of the view controller missing entirely. Clicking on "Debug View Hierarchy" and turning on "Show Clipped Content" revealed the following when the View Controller is missing entirely:

Turning on Show Clipped Content

Clipped Content

So, it seems the missing content is clipped / out of bounds.

Showing only the wireframes reveals the following:

wire frames only

The Page View Controller has a

  • _UIPageViewControllerContentView that contains a
  • _UIQueuingScrollView that contains a
  • UIView that contains a
  • VehicleDetailTableViewController (the UITableViewController with a car image and details).

I also see the _UIQueuingScrollView's bounds is quite different when things are weird. The bounds have an x of 1125 as opposed to an X of 375 when everything is normal.

This only happens when using a Transition Style of scroll as opposed to Page Curl. When using Page Curl, things work fine.

How can we prevent / fix this?

Second Update

This code makes the problem go away. However, it leaves a more jarring experience. Perhaps due to the delay of 0.4 seconds, the blue background shows sometimes in normal use.

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {

    let viewControllers = [viewController]
    setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
            self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
        })
    })
}

This is not a good user experience. Is there a better approach?

I want the scroll transitions to be smooth, not briefly show the blue background, and to not lose its content aka View Controller content.

finneycanhelp
  • 9,018
  • 12
  • 53
  • 77
  • 1
    Perhaps isolating the problem might help—there's a lot going on in those view controllers. Can you try making a quick test app with some pages and back/forward buttons and see if the problem reproduces there? – Jack Lawrence Feb 21 '18 at 06:13
  • That's a great question. I tried making a quick test app and the issue was not reproducible in the test app. – finneycanhelp Feb 21 '18 at 14:17
  • We're punting on this. Our Vehicle Details Page shows many wonderful and valuable items which we cannot do without. Therefore, it is probably making this error more visible than otherwise. – finneycanhelp Feb 21 '18 at 17:14
  • I would use scroll views and lay them out horizontally with paging enabled, rather than using a `UIPageViewController`. – Tometoyou Feb 22 '18 at 11:37
  • That's an interesting idea and I applaud your ingenuity, @Tometoyou You're saying to create our own UIPageViewController essentially? One would create the view controllers as needed and then place them into the scroll view as needed? Interesting. – finneycanhelp Feb 23 '18 at 11:46
  • Something like the following, @Tometoyou ? https://developer.apple.com/library/content/documentation/WindowsViews/Conceptual/UIScrollView_pg/ScrollViewPagingMode/ScrollViewPagingMode.html ? – finneycanhelp Feb 23 '18 at 11:54
  • 1
    @finneycanhelp Yep that's what I'm suggesting. I've always had bad experiences with `UIPageViewControllers` acting weirdly when I change page so I always create my own when I need to. – Tometoyou Feb 23 '18 at 11:56
  • I would suggest using UICollectionView instead of UIPageViewController. You can easily navigate to indexPaths of CollectionView in a better way instead pages. – rushisangani Feb 27 '18 at 05:30
  • can you please remove deadline delay of 0.4 to 0.0 and redebug the issue? and tell me results ? – Abu Ul Hassan Feb 27 '18 at 10:18
  • Well, most interesting code is missing. Can you post fragments what you do in UIPageViewControllerDataSource? (the code where you pick view controller After and Before current view controller, and how you handle index to current. – Juraj Antas Feb 27 '18 at 22:47
  • @finneycanhelp hey please check my answer, I have created a sample project. Try adding more details to the project and see if this solves your problem – Sahil Manchanda Feb 28 '18 at 11:38
  • @finneycanhelp I just want to know if that sample project has helped you – Sahil Manchanda Mar 01 '18 at 15:41
  • 1
    in my case I forgot to set up the view frame of viewcontroller inside before and after – Raditya Kurnianto Sep 10 '20 at 00:46

4 Answers4

2

Although the real answer is to have View Controllers that are as simple as possible (but no simpler), here is the code that fixed the problem with the side effect of showing the background on occasion when the user navigates to the next View Controller.

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {

    let viewControllers = [viewController]
    setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
            self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
        })
    })
}
finneycanhelp
  • 9,018
  • 12
  • 53
  • 77
2

A simple solution is to decouple the button taps from the view controller changes by adding a small "tap ahead" buffer. Create a button queue (use a simple NSMutableArray that acts as FIFO queue) where you add each navigation button tap, then call a dequeue function if the queue was empty before the add.

In the dequeue function you remove the first entry and change view accordingly, then call itself again in the setViewControllers completion handler if the queue is not empty.

Make sure to do the processing on the main thread only to avoid threading problems. If you want, you can also add restrictions on how many "tap ahead" you allow, and perhaps flush the queue on directional changes.

LGP
  • 4,135
  • 1
  • 22
  • 34
1

I'm not sure if this solution would be suitable for your users but if the problem occurs due to the user navigating quickly you could implement a lock that would disallow this quick navigation. Essentially:

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
    guard !isChangingPages else { return }
    isChangingPages = true
    let viewControllers = [viewController]
    let isAnimated = true // false always works. However, animation is required.
    setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: { [weak self] _ in 
        self?.isChangingPages = false
    })
}

This way you'd have to finish transitioning to the new page before allowing the transition to the next.

This would likely result in confusion for the user if you kept the navigation buttons enabled while this bool was set to true (tapping without seeing a result). But the logic could be changed to disable the buttons and reenable them in the completion block (that way they'd fade in/out during the page change).

Steve
  • 921
  • 1
  • 7
  • 18
1

Hi I have created a Sample Project, which should solve your problem. I have added 100 ViewControllers (via loop) and it's working fine with scroll animation. things are at their place.

What I have done in this project is:

  1. Created a BaseClass for a Page with two properties

    a) pageIndex of type Int

    b) delegate for a protocol for callbacks

  2. Added UIPageViewController to ViewController via ContainerView

  3. Created a ViewController named Page which extends PageViewBase

  4. Run a loop count of 100 and added data to an array and set datasource and delegate to self (PageControlelr) and managed it according to pageIndex property

PageViewController

class PageViewController: UIPageViewController {
    var list = [Page]()
    var sb: UIStoryboard?
    var viewController: ViewController! // settting from ViewController
    override func viewDidLoad() {
        super.viewDidLoad()
        sb = UIStoryboard(name: "Main", bundle: nil)
        DispatchQueue.main.asyncAfter(deadline: .now()+0.4, execute: {
            self.setupList()
        })
    }
    func setupList(){
        for i in 0..<100{
            let model = PageModel(title: "Title \(i + 1)", subTitle: "SubTitle \(i + 1)")
            let page = sb?.instantiateViewController(withIdentifier: "PageID") as! Page
            page.data = model
            page.pageIndex = i
            page.delegate = viewController
            list.append(page)
        }
        self.delegate = self
        self.dataSource = self
        setViewControllers([list[0]], direction: .forward, animated: true, completion: nil)
        self.updateCurrentPageLabel(index: 0)
    }
    func movePage(index: Int){
        let currentIndex = self.viewControllers![0] as! Page
        self.updateCurrentPageLabel(index: index)
        setViewControllers([list[index]], direction: index > currentIndex.pageIndex ? .forward : .reverse, animated: true)
    }
    func getCurrentPageIndex() -> Int{
        return (self.viewControllers![0] as! Page).pageIndex
    }
    func updateCurrentPageLabel(index: Int){
        (self.parent as? ViewController)?.currentListingLabel.text = "\(index + 1) of \(list.count)"
    }
}
extension PageViewController: UIPageViewControllerDelegate{
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        let currentIndex = (self.viewControllers![0] as! Page).pageIndex
        self.updateCurrentPageLabel(index: currentIndex)

    }
}
extension PageViewController: UIPageViewControllerDataSource{
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        let index = (viewController as! Page).pageIndex
        if index > 0 {
            return list[index-1]
        }
        return nil
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        let index = (viewController as! Page).pageIndex
        if index < list.count-1 {
            return list[index+1]
        }
        return nil
    }

}

Page

import UIKit

struct PageModel {
    var title: String
    var subTitle: String
}

class Page: PageViewBase {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subTitleLabel: UILabel!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var btnWorking: UIButton!
    var data: PageModel?
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTags()
        setupActions()
        setupData()
    }
    func setupData(){
        if let data = data{
            self.titleLabel.text = data.title
            self.subTitleLabel.text = data.subTitle
            imageView.image = #imageLiteral(resourceName: "car")
        }

    }

    enum buttonTags: Int{
        case working = 1
    }
    func setupTags(){
        btnWorking.tag = buttonTags.working.rawValue
    }
    func setupActions(){
        btnWorking.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
    }
    @objc func didSelect(_ sender: UIView){
        if let tag = buttonTags.init(rawValue: sender.tag){
            switch tag{
            case .working:
                delegate?.didReceive(withMessage: "wokring button clicked of index \(pageIndex)")
            }
        }
    }
}

ViewController // MainController

import UIKit
protocol CallBack {
    func didReceive(withMessage message: String)
}
class ViewController: UIViewController {
    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var btnCall: UIButton!
    @IBOutlet weak var btnMessage: UIButton!
    @IBOutlet weak var btnNext: UIButton!
    @IBOutlet weak var btnBack: UIButton!
    @IBOutlet weak var currentListingLabel: UILabel!
    var pageController: PageViewController?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTags()
        setupActions()
        setupContainerView()
    }

    enum buttonTags: Int{
        case call = 1
        case message
        case next
        case back
    }
    func setupTags(){
        btnCall.tag = buttonTags.call.rawValue
        btnMessage.tag = buttonTags.message.rawValue
        btnNext.tag = buttonTags.next.rawValue
        btnBack.tag = buttonTags.back.rawValue
    }
    func setupActions(){
        btnCall.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnMessage.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnNext.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnBack.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
    }
    @objc func didSelect(_ sender: UIView){
        if let tag = buttonTags.init(rawValue: sender.tag){
            switch tag{
            case .call:
                print("Call button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
            case .message:
                print("message button called for index  \(pageController?.getCurrentPageIndex() ?? 0)")
            case .next:
                if let p = pageController{
                    let currentIndex = p.getCurrentPageIndex()
                    if currentIndex < p.list.count - 1{
                        p.movePage(index: currentIndex + 1)
                    }
                }
            case .back:
                if let p = pageController{
                    let currentIndex = p.getCurrentPageIndex()
                    if currentIndex > 0{
                        p.movePage(index: currentIndex - 1)
                    }
                }
            }
        }
    }
    func setupContainerView(){
        let sb = UIStoryboard(name: "Main", bundle: nil)
        pageController = sb.instantiateViewController(withIdentifier: "PageViewControllerID") as? PageViewController
        pageController?.viewController = self
        addViewIntoParentViewController(vc: pageController)
    }
    func addViewIntoParentViewController(vc: UIViewController?){
        if let vc = vc{
            for v in self.containerView.subviews{
                v.removeFromSuperview()
            }
            self.containerView.addSubview(vc.view)
            self.containerView.translatesAutoresizingMaskIntoConstraints = false
            vc.view.translatesAutoresizingMaskIntoConstraints = false
            addChildViewController(vc)
            NSLayoutConstraint.activate([
                vc.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                vc.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                vc.view.topAnchor.constraint(equalTo: containerView.topAnchor),
                vc.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
                ])
            vc.didMove(toParentViewController: self)
        }
    }
}
extension ViewController: CallBack{
    func didReceive(withMessage message: String) {
        print("message: \(message)")
    }
}

PageViewBase

import UIKit

class PageViewBase: UIViewController {
    var pageIndex = -1
    var delegate: CallBack?
}
Sahil Manchanda
  • 9,812
  • 4
  • 39
  • 89