My UIPageViewController crashes when the DataSource methods ViewControllerAfter and ViewControllerBefore returns nil (which should only mean that there's no more pages to show). It crashes with the message:
'The number of view controllers provided (0) doesn't match the number
required (1) for the requested transition'
Thanks! Please help!
My initialization code:
self.pageController = UIPageViewController(transitionStyle: .pageCurl, navigationOrientation: .horizontal, options: nil)
self.pageController?.dataSource = self
// self.pageController?.delegate = self
self.pageController?.setViewControllers([self.controller.pages.first!], direction: .forward, animated: true, completion: nil)
self.updateControls()
self.addChildViewController(self.pageController!)
self.view.addSubview((self.pageController?.view)!)
self.pageController?.didMove(toParentViewController: self)
And below my DataSource code:
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
previousIndex = self.controller.pages[self.currentPage].pageIndex!
if previousIndex <= 0 {
print("entrou no nil do before")
return nil
}
self.previousIndex -= 1
self.currentPage = self.previousIndex
return self.controller.pages[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
nextIndex = self.controller.pages[self.currentPage].pageIndex!
nextIndex += 1
if (nextIndex == self.controller.pages.count) {
print("entrou no nil do after")
return nil
}
self.currentPage = nextIndex
return self.controller.pages[nextIndex]
}
Both the methods will return nil due to its behaviour. You need to handle it at your own.
This flexibility is given to put the pages in pageviewcontroller in cycle or end it on number of pages.
You need to use such code:
// MARK: - PageViewControllerDataSource
//Array of viewcontroller (pages) identifiers.
var pageTitles = [String]
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
var index = self.pageTitles.indexOf(identifier!)!
print("index back = \(index)")
if index == 0 {
currentPageCount = -1
nextPageCount = index
}
if index == 0 || index == NSNotFound {
return nil
}
index -= 1
currentPageCount = index-1
nextPageCount = index
return self.viewControllerAtIndex(index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
var index = self.pageTitles.indexOf(identifier!)!
if index == NSNotFound {
return nil;
}
index += 1
if index == self.pageTitles.count {
return nil;
}
return self.viewControllerAtIndex(index)
}
Related
The UIPageViewControllerDataSource is not being called from ViewDidLoad, hence only the first page is displayed and swipe doesn't do anything.
I have followed this tutorial:
https://spin.atomicobject.com/2015/12/23/swift-uipageviewcontroller-tutorial/
The answers in questions similar to this do not work. I have viewed 5 similar questions but none of them work for me.
class SetupViewController: UIPageViewController {
private(set) lazy var pages : [UIViewController] = {
return [self.newPage(1), self.newPage(2), self.newPage(3), self.newPage(4)]
}()
private func newPage(_ page: Int) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Page\(page)")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
if let firstPage = pages.first {
print(#function, "if let firstPage", firstPage, pages) //viewDidLoad() if let firstPage page1name [page1, page2, page3, page4]
setViewControllers([firstPage], direction: .forward, animated: true, completion: nil)
}
}
}
extension SetupViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
print(#function) //It doesn't print
guard let pageIndex = pages.firstIndex(of: viewController) else { return nil }
let nextIndex = pageIndex + 1
let pageCount = pages.count
guard nextIndex != pageCount else { return nil }
guard pageCount > nextIndex else { return nil }
return pages[nextIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
print(#function) //It doesn't print
guard let pageIndex = pages.firstIndex(of: viewController) else { return nil }
let previousIndex = pageIndex - 1
guard previousIndex >= 0 else { return nil }
guard pages.count > previousIndex else { return nil }
return pages[previousIndex]
}
}
Storyboard IDs are: Page1, Page2, Page3 & Page4
What am I doing wrong?
May be you have a configuration problem i have created a demo here
https://github.com/ShKhan9/PagerShow
let wind = (UIApplication.shared.delegate as! AppDelegate).window!
wind.rootViewController = SetupViewController()
I am working on a project with 3 views that are accessed by swiping.
The following code implements the PageViewController I am using to handle the swiping motion:
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
lazy var orderedViewControllers : [UIViewController] = {
return [self.newVC(viewController : "ClimaVC"),
self.newVC(viewController : "WeatherVC"),
self.newVC(viewController : "ClosetVC")]
}()
var myViewControllers = [UIViewController]()
var currentIndex : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.dataSource = self
setViewControllers([orderedViewControllers[1]],
direction: .forward, animated: true, completion: nil)
self.delegate = self
}
func newVC(viewController : String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1;
guard previousIndex >= 0 else {
// return orderedViewControllers.last
//Return nil to avoid swiping forever.
return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
currentIndex = previousIndex
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1;
guard orderedViewControllers.count != nextIndex else {
//return orderedViewControllers.first
//Return nil to avoid swiping forever.
return nil
}
guard orderedViewControllers.count > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
}
The main View Controller is WeatherVC. In this view, time data is retrieved and stored in an integer variable named timeOfDay.
Depending on the value of timeOfDay, the background displayed will differ. While I was able to use this variable in WeatherVC, I need to pass this variable timeOfDay to the two other view controllers ClimaVC and ClosetVC.
I tried setting up a delegate protocol but failed, explored using the different functions provided by UIPageViewControllerDelegate and it did not get me anywhere.
I would really appreciate some help on this,
Thank you very much!
It's not complicated, just assign the values at the pageviewcontroller datasources.
import UIKit
class ClimaViewController: UIViewController{
var timeOfDayClima: Int = 0
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print (timeOfDayClima)
}
}
class WeatherViewController: UIViewController{
var timeOfDayClima: Int = 0
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
timeOfDayClima += 1
}
}
class ClosetViewController: UIViewController{
var timeOfDayClima: Int = 0
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print (timeOfDayClima)
}
}
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
lazy var orderedViewControllers : [UIViewController] = {
return [self.newVC(viewController : "ClimaVC"),
self.newVC(viewController : "WeatherVC"),
self.newVC(viewController : "ClosetVC")]
}()
var myViewControllers = [UIViewController]()
var currentIndex : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.dataSource = self
setViewControllers([orderedViewControllers[1]],
direction: .forward, animated: true, completion: nil)
self.delegate = self
}
func newVC(viewController : String) -> UIViewController {
return UIStoryboard(name: "Second", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
//Guard = some kind of if statement.
guard let viewControllerIndex = orderedViewControllers.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1;
guard previousIndex >= 0 else {
// return orderedViewControllers.last
//Return nil to avoid swiping forever.
return nil
}
guard orderedViewControllers.count > previousIndex else {
return nil
}
currentIndex = previousIndex
if(currentIndex == 0) {
(orderedViewControllers.first as! ClimaViewController).timeOfDayClima = (orderedViewControllers[1] as! WeatherViewController).timeOfDayClima
}
return orderedViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
//Guard = some kind of if statement.
guard let viewControllerIndex = orderedViewControllers.index(of: viewController)
else {
return nil
}
let nextIndex = viewControllerIndex + 1;
guard orderedViewControllers.count != nextIndex else {
//return orderedViewControllers.first
//Return nil to avoid swiping forever.
return nil
}
guard orderedViewControllers.count > nextIndex else {
return nil
}
currentIndex = nextIndex
if(currentIndex == orderedViewControllers.count - 1) {
(orderedViewControllers.last as! ClosetViewController).timeOfDayClima = (orderedViewControllers[1] as! WeatherViewController).timeOfDayClima
}
if(nextIndex == 0) {
let secondVC = self.storyboard?.instantiateViewController(withIdentifier: "ClimaVC") as! ClimaViewController
secondVC.timeOfDayClima = 1
}
return orderedViewControllers[nextIndex]
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Maybe you should try explaining what has failed in setting up your delegate protocol as well because it seems like it is likely the solution you are looking for. I'm writing a protocol here for reference but you have to change it according to your needs.
protocol WeatherVCDelegate {
func changedTime(timeOfDay: Int)
}
Here comes the tricky part. When you declare a delegate in WeatherVC, you need to use the delegate to call the function to relay the timeOfDay to other weatherVC. How this works is that anything that conforms to the delegate protocol will be triggered to use the function.
class WeatherVC {
weak var delegate: WeatherVCDelegate?
func functionName() {
delegate?.changedTime(timeOfDay: value)
}
Now, when you initialize weatherVC in your PageViewController, you need PageViewController to conform to weatherVC delegate.
In PageViewController...
weatherVC.delegate = self
extension PageViewController: WeatherVCDelegate {
...
}
Here is more read up about delegates if my answer is not clear enough
https://medium.com/#jamesrochabrun/implementing-delegates-in-swift-step-by-step-d3211cbac3ef
I have a page view controller that allows me to scroll through the various signup pages and enter the necessary information. The problem is it just scrolls in a circle. How do I make it so that the PlayerInfo page is the last page I can scroll to?
lazy var SignupArray : [UIViewController] = {
return [self.VCInstance(name: "ParkSelect"),
self.VCInstance(name: "SportSelect"),
self.VCInstance(name: "PlayerInfo")]
}()
private func VCInstance(name: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: name)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if let firstVC = SignupArray.first {
setViewControllers([firstVC], direction: .forward, animated: true, completion: nil)
}
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = SignupArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return SignupArray.last
}
guard SignupArray.count > previousIndex else {
return nil
}
return SignupArray[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = SignupArray.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard nextIndex < SignupArray.count else {
return SignupArray.first
}
guard SignupArray.count > nextIndex else {
return nil
}
return SignupArray[nextIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = SignupArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
return SignupArray[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = SignupArray.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard SignupArray.count > nextIndex else {
return nil
}
return SignupArray[nextIndex]
}
The reason it is endless because
when previousIndex becomes zero you pass the last VC in array
guard previousIndex >= 0 else {
return SignupArray.last
}
when nextIndex is equal to count of signUp array u pass the first VC in array.
guard nextIndex < SignupArray.count else {
return SignupArray.first
}
Remove these two code and ur PageViewer will stop
Return nil instead of wrapping around. For example:
public func pageViewController(_ pvc: UIPageViewController, viewControllerAfter vc: UIViewController) -> UIViewController?{
guard
let i = SignupArray.index(of: vc),
i < SignupArray.count - 1
else {
return nil
}
return SignupArray[i + 1]
}
or, even more concisely:
public func pageViewController(_ pvc: UIPageViewController, viewControllerAfter vc: UIViewController) -> UIViewController?{
let i = 1 + (SignupArray.index(of: vc) ?? SignupArray.count)
return i < SignupArray.count ? SignupArray[i] : nil
}
I have a page control in my project and I want to do two things :
1. I want to hide indicator dots in the last page
2. I want to lock the last page in the (page VC(my class)) that I can't come back to the first page
here is my code
class pageVC : UIPageViewController , UIPageViewControllerDataSource , UIPageViewControllerDelegate {
lazy var VCArr : [UIViewController] = {
return [self.VCInstance(name : "FirtsVC"),
self.VCInstance(name : "SecondVC"),
self.VCInstance(name :"ThirdVC"),
self.VCInstance(name :"FourthVC"),
self.VCInstance(name :"FivethVC")]
}()
private func VCInstance(name : String) -> UIViewController {
return UIStoryboard(name : "Main" , bundle : nil).instantiateViewController(withIdentifier: name)
}
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
self.delegate = self
if VCArr.last != nil {
print("first Page Reached!")
}
if let firstVC = VCArr.first {
setViewControllers([firstVC] , direction: .forward , animated: true, completion: nil)
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let pageVC = UIPageControl()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
}else if view is UIPageControl{
view.backgroundColor = UIColor.clear
pageVC.numberOfPages = 5
pageVC.center = self.view.center
pageVC.layer.position.y = self.view.frame.height - 180 ;
}
}
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = VCArr.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex-1
guard previousIndex >= 0 else {
return VCArr.last
}
guard VCArr.count > previousIndex else {
return nil
}
return VCArr[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController?{
guard let viewControllerIndex = VCArr.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex+1
guard nextIndex < VCArr.count
else {
return VCArr.first
}
guard VCArr.count > nextIndex else {
return nil
}
return VCArr[nextIndex]
}
public func presentationCount(for pageViewController: UIPageViewController) -> Int{
return VCArr.count
}
public func presentationIndex(for pageViewController: UIPageViewController) -> Int{
guard let firstViewController = viewControllers?.first , let firstViewControllerIndex = VCArr.index(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if (VCArr.last!.isViewLoaded)
{
print("It is Done!!!")
}
}
Change this, which is making the page view to return to the first one when it is in the last one:
let nextIndex = viewControllerIndex+1
guard nextIndex < VCArr.count
else {
return nil //was VCArr.first
}
And to make it hidden, first move the let pageVC = UIPageControl() below the class pageVC : UIPageViewController , UIPageViewControllerDataSource , UIPageViewControllerDelegate { to make it public within the class. Then add:
if nextIndex == VCArr.count{
pageVC.isHidden = true
}
When I do fast swipe between pages sometimes I get duplicate pages. My index counting may be wrong, but I tried everything and always get the same.
DetailedProfileViewController.swift
class DetailedProfileViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var currentProfileIndex : Int?
var nextProfileIndex :Int?
var data: Main?
var mainPageView : UIPageViewController!
override func viewDidLoad() {
super.viewDidLoad()
let profile = ProfileViewController()
profile.profile = currentProfile()
let arraysOfViews = [profile]
mainPageView = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
mainPageView.setViewControllers(arraysOfViews, direction: .forward, animated: true, completion: nil)
mainPageView.dataSource = self
mainPageView.delegate = self
addChildViewController(mainPageView)
view.addSubview(mainPageView.view)
mainPageView.didMove(toParentViewController: self)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if currentProfileIndex == (data?.filteredProfiles.count)!-1 { return nil }
nextProfileIndex = abs((currentProfileIndex! + 1) % (data?.filteredProfiles.count)!)
let profile = ProfileViewController()
profile.profile = data?.filteredProfiles[nextProfileIndex!]
return profile
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if currentProfileIndex == 0 { return nil }
nextProfileIndex = abs((currentProfileIndex! - 1) % (data?.filteredProfiles.count)!)
let profile = ProfileViewController()
profile.profile = data?.filteredProfiles[nextProfileIndex!]
return profile
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed == true {
currentProfileIndex = nextProfileIndex
}
}
}
I'm not setting all of View Controllers in viewDidLoad because it could be hundreds of them..
The dumbest solution is to add an index to ProfileViewController. If you do so, you can set it as zero to the first page. And whenever you are asked to provide the next or previous view controller, you always know relatively to what index, because you may extract it from the given pageViewController (which is in fact your ProfileViewController).
Otherwise the handling of the currentProfileIndex may be very error-prone.
UPDATE
class DetailedProfileViewController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
var data: Main?
var mainPageView : UIPageViewController!
override func viewDidLoad() {
super.viewDidLoad()
let profile = ProfileViewController()
profile.index = 0
profile.profile = currentProfile()
let arraysOfViews = [profile]
mainPageView = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:])
mainPageView.setViewControllers(arraysOfViews, direction: .forward, animated: true, completion: nil)
mainPageView.dataSource = self
mainPageView.delegate = self
addChildViewController(mainPageView)
view.addSubview(mainPageView.view)
mainPageView.didMove(toParentViewController: self)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let pagesCount = data?.filteredProfiles.count else {
return nil
}
guard let newIndex = (viewController as? ProfileViewController).index + 1, newIndex < pagesCount else {
return nil
}
let profile = ProfileViewController()
profile.profile = data?.filteredProfiles[newIndex]
profile.index = newIndex
return profile
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let pagesCount = data?.filteredProfiles.count else {
return nil
}
guard let newIndex = (viewController as? ProfileViewController).index - 1, newIndex >= 0 else {
return nil
}
let profile = ProfileViewController()
profile.profile = data?.filteredProfiles[newIndex!]
profile.index = newIndex
return profile
}
}