I am trying to implement a PageViewController that switches to a different ViewController when a button is pressed. I can successfully load each "child" ViewController using the setViewControllers method during viewDidLoad().
However when I call the setViewControllers method outside viewDidLoad(), it does not change the current "child" ViewController.
Can you call the method setViewControllers at any point or just once during viewDidLoad()? Also is there a way to change a variable in the pageviewcontrollers dataSource extensions?
My project consists of a entry ViewController with a "Settings" button and a ContainerView. Inside the ContainerView I have the UIPageViewController.
Code for PageViewController:
import UIKit
class PageViewController: UIPageViewController {
var slides = [SlideItem]()
var currentIndex: Int!
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
initSlides()
}
override func viewDidLoad() {
super.viewDidLoad()
if let viewController = viewSlideViewController(currentIndex ?? 0){
let viewControllers = [viewController]
setViewControllers(viewControllers, direction: .forward, animated: true, completion: nil)
}
dataSource = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func viewSlideViewController(_ index: Int) -> SlideViewController? {
guard let storyboard = storyboard,
let page = storyboard.instantiateViewController(withIdentifier: "SlideViewController") as? SlideViewController else {
return nil
}
page.slide = slides[index]
page.slideIndex = index
return page
}
func viewSettingsViewController(_ index: Int) -> SettingsViewController? {
guard let storyboard = storyboard,
let page = storyboard.instantiateViewController(withIdentifier: "SettingsViewController") as? SettingsViewController else {
return nil
}
return page
}
func initSlides(){
slides.append(SlideItem.init(filename: "Slide 1", comparison: false, highYield: false, videoURL: nil,
groupOrganSystem: ["A"],
groupMedicalSpecialty: ["B"]))
slides.append(SlideItem.init(filename: "Slide 2", comparison: false, highYield: false, videoURL: nil,
groupOrganSystem: ["A"],
groupMedicalSpecialty: ["B"]))
}
func sidebarCommands(button: String, state: Bool){
switch button {
case "settings":
self.setViewControllers([viewSettingsViewController(0)!], direction: .forward, animated: false, completion: nil)
print("settings")
default:
return
}
}
}
Extensions:
extension PageViewController : UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let viewController = viewController as? SlideViewController,
let index = viewController.slideIndex,
index > 0 {
return viewSlideViewController(index - 1)
}
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let viewController = viewController as? SlideViewController,
let index = viewController.slideIndex,
(index + 1) < slides.count {
return viewSlideViewController(index + 1)
}
return nil
}
}
You can use UIPageviewController in this way: (inheritance)
https://medium.com/how-to-swift/how-to-create-a-uipageviewcontroller-a948047fb6af
You can call setViewControllers at any point (I've been doing that).
The fact that it does not produce expected results is probably a result of bug in your code, include your code if you want to help out with that.
Thank you for your help, I figured out what I was doing wrong. I didn't have the right reference for the active PageViewController in the ContainerView.
So of course called the setViewControllers function on random PageViewController did not produce the correct results.
I found this answer explains how to correctly get a reference from a ContainerView
Related
I currently have a button that currently displays a UIPageViewController as an overlay on the homescreen. The user can always click on it and it would still display this ViewControllers embedded in a UIPageViewController. However at the moment, when the user clicks on the button to display the overlay and closes it and opens it again. The last page index / state of the last page is saved and displayed again. What i would like to do is to always display the first ViewController in the UIPageviewController. I have tried different ways but haven't had any luck.
Here is the screen capture that shows how the current mechanism works
https://gph.is/g/Z2QlyDa
HomeHelpViewController that contains the UIPageViewController
class HomeHelpViewController: UIPageViewController, UIPageViewControllerDataSource {
var helpPages: [UIViewController] = []
var currentIndex: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
let helpStoryboard = UIStoryboard(name: "HomeHelp", bundle: nil)
for scanIdentifier in ["homeHelpVC1","homeHelpVC2","homeHelpVC3","homeHelpVC4"] {
let scanVC = helpStoryboard.instantiateViewController(withIdentifier: scanIdentifier)
self.helpPages.append(scanVC)
}
self.dataSource = self
self.setViewControllers([self.helpPages.first!], direction: .forward, animated: true)
}
//MARK:- UIPageViewControllerDataSource
func pageViewController(_ pPageViewController: UIPageViewController, viewControllerBefore pViewController: UIViewController) -> UIViewController? {
self.currentIndex = self.helpPages.firstIndex(of: pViewController)!
if self.currentIndex > 0 {
return self.helpPages[self.currentIndex - 1]
}
return nil
}
func pageViewController(_ pPageViewController: UIPageViewController, viewControllerAfter pViewController: UIViewController) -> UIViewController? {
self.currentIndex = self.helpPages.firstIndex(of: pViewController)!
if self.currentIndex < (self.helpPages.count - 1) {
return self.helpPages[self.currentIndex + 1]
}
return nil
}
func presentationCount(for pPageViewController: UIPageViewController) -> Int {
return self.helpPages.count
}
func presentationIndex(for pPageViewController: UIPageViewController) -> Int {
return self.currentIndex
}
}
HomePageViewController that has the button display the UIPageViewController as an overlay
class HomePageViewController: UIViewController {
#IBOutlet weak var helpOverlay: BaseOverlayView!
#IBAction func helpButton(_ pSender: Any) {
self.helpButton.setImage(AppDelegate.helpButtonImage(tutorial: true), for: .normal)
self.helpOverlay.showOnView(self.view)
}
#IBAction func closeHelp(_ pSender: UIButton) {
self.helpOverlay.removeFromView()
self.helpButton.setImage(AppDelegate.helpButtonImage(tutorial: false), for: .normal)
}
}
declare a variable to access your HomeHelpViewController inside your HomePageViewController
class HomePageViewController: UIViewController {
weak var homePageController : HomeHelpViewController?
#IBOutlet weak var helpOverlay: BaseOverlayView!
#IBAction func helpButton(_ pSender: Any) {
if let homePageController = homePageController {
homePageController.setViewControllers([homePageController.helpPages.first!], direction: .forward, animated: true)
}
self.helpButton.setImage(AppDelegate.helpButtonImage(tutorial: true), for: .normal)
self.helpOverlay.showOnView(self.view)
}
#IBAction func closeHelp(_ pSender: UIButton) {
self.helpOverlay.removeFromView()
self.helpButton.setImage(AppDelegate.helpButtonImage(tutorial: false), for: .normal)
}
}
inside this loop assign your HomeHelpViewController
for scanIdentifier in ["homeHelpVC1","homeHelpVC2","homeHelpVC3","homeHelpVC4"] {
let scanVC = helpStoryboard.instantiateViewController(withIdentifier: scanIdentifier) as? HomePageViewController
scanVc?.homePageController = self
self.helpPages.append(scanVC)
}
This may be because the UIPageViewController was not properly dismissed.
func dismiss(animated flag: Bool, completion: (() -> Void)? = nil)
You can refer to the documentation here: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621505-dismiss
This effectively deallocates the UIPageViewController so that once the button is pressed, it will reinitialized again at the first page.
I would like to change the pages showed by my UIPageView programatically.
I created two ViewController that are intended to be displayed in the PageView, and I set the initial View with "setViewControllers" in my viewDidLoad. I works, the view is displayed and I can swipe on the screen to switch between the two views.
My problem being that when I try to call again "setViewControllers" to change the displayed view when I click a button, nothing happens.
I'm using delegates since the buttons are part of another ViewController, and used NSLog to make sure the functions were called, which is the case.
If anyone could help me understand why the "setViewControllers" method doesn't do anything, that would be great.
Here is my code for my UIPageViewController:
import UIKit
class FrontContentController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, sideMenuDelegate {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return subViewControllers.count
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.index(of: viewController) ?? 0
if currentIndex <= 0 {
return nil
}
return subViewControllers[currentIndex - 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.index(of: viewController) ?? 0
if currentIndex >= subViewControllers.count - 1 {
return nil
}
return subViewControllers[currentIndex + 1]
}
lazy var subViewControllers:[UIViewController] = {
return [
UIStoryboard(name: "frontPage", bundle: nil).instantiateViewController(withIdentifier: "FirstContent") as! FirstContentController,
UIStoryboard(name: "frontPage", bundle: nil).instantiateViewController(withIdentifier: "SecondContent") as! SecondContentController
]
}()
var sideMenuVC : SideMenuController!
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
self.dataSource = self
self.sideMenuVC = (self.storyboard?.instantiateViewController(withIdentifier: "SideMenu") as! SideMenuController)
self.setViewControllers([subViewControllers[0]], direction: .forward, animated: true, completion: nil)
// Do any additional setup after loading the view.
}
func changeColor(_ color: UIColor) {
if color == .blue {
self.setViewControllers([subViewControllers[0]], direction: .forward, animated: false, completion: nil)
NSLog("Blue Delegate")
}
if color == .red {
self.setViewControllers([subViewControllers[1]], direction: .forward, animated: false, completion: nil)
NSLog("Red Delegate")
}
}
}
And in case it's relevant, the separate UIViewController with the buttons (a side menu) :
import UIKit
protocol sideMenuDelegate: AnyObject {
func changeColor(_ color : UIColor)
}
class SideMenuController: UIViewController {
weak var delegate : sideMenuDelegate?
var frontPageVC : FrontPageController!
var frontContentPC : FrontContentController!
override func viewDidLoad() {
super.viewDidLoad()
let storyboard = UIStoryboard(name: "frontPage", bundle: nil)
frontPageVC = (storyboard.instantiateViewController(withIdentifier: "FrontPage") as! FrontPageController)
frontContentPC = (storyboard.instantiateViewController(withIdentifier: "FrontContent") as! FrontContentController)
self.delegate = frontContentPC
self.view.backgroundColor = UIColor.black
}
#IBAction func pressBlueButton(_ sender: Any) {
delegate?.changeColor(UIColor .blue)
}
#IBAction func pressRedButton(_ sender: Any) {
delegate?.changeColor(UIColor .red)
}
}
Thanks
I have been trying to use a UIPageViewController in my storyboard to create a set of sliding images, but I keep getting this error:
Type 'PageViewController' does not conform to protocol 'UIPageViewControllerDataSource'
And I don't understand why.
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
//for scroll view
lazy var subViewControllers:[UIViewController] = {
return[
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Slide_1") as! ViewController_0,
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Slide_2") as! ViewController_1,
UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "Slide_3") as! ViewController_2
]
}()
//after viewcontroller
func pageViewController(_pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.index(of: viewController) ?? 0
if (currentIndex >= subViewControllers.count - 1) {
return nil
}
return subViewControllers[currentIndex + 1]
}
//before viewcontroller
func pageViewController(_pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
let currentIndex:Int = subViewControllers.index(of: viewController) ?? 0
if (currentIndex <= 0) {
return nil
}
return subViewControllers[currentIndex - 1]
}
override func viewDidLoad() {
super.viewDidLoad()
//setting the initial view for the slider
setViewControllers([subViewControllers[0]],direction: .forward, animated: true, completion: nil)
}
//making style a normal slide
required init?(coder: NSCoder) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Navigation
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return subViewControllers.count
}
}
There should be a space between the _ and the pageViewController parameter in your two methods that are part of the protocol (before/after pages).
The underscore denotes that the method, when called, does not need a label for that parameter.
For example:
func setBlob(_ blob: Blob) -> Bool {
}
Would look like this when it's called:
let myBlob = Blob()
setBlob(myBlob)
If it didn't have the _ there it would expect setBlob(blob: blob). It's convenient to be able to change or ignore the parameter names for the sake of code cleanliness. But since you don't have a space between the parameter label and the actual parameter name, it thinks the parameter name is _pageViewController.
There should be a red icon on the same line where the error shows and you can click that to automatically fill in the missing methods so you can easily see what is missing / incorrect. Your class has to implement all the required methods of the protocol and follow the same method signatures or else you'll get this error.
My problem is as follows: I am using pageViewControllers to create a tutorial/onboarding page for an app i'm creating. A left swipe gesture on the last (3rd) page of my tutorial should perform a segue.
This is what I have as of now, but it is giving me a key value coding-compliancy error. I have double, triple checked my outlets, and swipeView is very much so connected properly.
class GrowController: UIViewController {
#IBOutlet weak var swipeView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(swipeView)
createGesture()
}
func createGesture() {
let showReg = UISwipeGestureRecognizer(target: self, action: #selector(showRegister))
showReg.direction = .left
self.swipeView.addGestureRecognizer(showReg)
}
func showRegister(gesture: UISwipeGestureRecognizer) {
performSegue(withIdentifier: "showRegister", sender: self)
}
}
That is the controller for the actual UIViewController (the specific page that is being displayed)
now ive also tried messing around with some logic in my TutorialViewController which is the UIPageViewController controlling the swipes form page to page etc.
Logic for that here
class TutorialViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
var viewControllerIndex: Int?
//Array of my pages to load (GrowController is page3)
lazy var tutorialArray: [UIViewController] = {
return [self.tutorialInstance(name: "page1"), self.tutorialInstance(name: "page2"), self.tutorialInstance(name: "page3")]
}()
private func tutorialInstance(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 firstViewController = tutorialArray.first {
setViewControllers([firstViewController], direction: .forward, animated: false, completion: nil)
}
}
// Scroll view
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews {
if view is UIScrollView {
view.frame = UIScreen.main.bounds
}
else if view is UIPageControl {
view.backgroundColor = UIColor.clear
}
}
}
// Page View Controller delegate functions
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = tutorialArray.index(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard tutorialArray.count > previousIndex else {
// Added this line just testing around, nothing happened here though.
performSegue(withIdentifier: "ShowRegister", sender: self)
return nil
}
return tutorialArray[previousIndex]
}
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = tutorialArray.index(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard nextIndex < tutorialArray.count else {
return nil
}
guard tutorialArray.count > nextIndex else {
return nil
}
return tutorialArray[nextIndex]
}
public func presentationCount(for pageViewController: UIPageViewController) -> Int {
return tutorialArray.count
}
public func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first, let firstViewControllerIndex = tutorialArray.index(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
Does is have something to do with the willTransitionTo pageView delegate method? I'm not familiar how to implement that, I have tried.
I thought that i could just add a subView to the Grow controller, put a swipe gesture in it, and perform a segue whenever the user swipes left from that page. As of right now, this code crashes the app upon loading of page3 (GrowController)
Any help GREATLY appreciated, I've been trying to figure this out for over a week and this is the second question on the topic i've posed. Thanks!
Don't use a UIPageViewController for this. That's very ironic, because I am usually the first to advise using UIPageViewController. But in this case it is doing too much of the work for you, and you cannot interfere in such a way as to customize it the way you're describing. You will need to use a simple paging UIScrollView; that way, you will be totally in charge of what happens, with fine-grained response thanks to the scroll view's delegate, and you'll have access to the underlying pan gesture recognizer and can modify its behavior.
There is a single viewController with some data in it. After swiping left or right I want to create new viewControllers having the same UI but having different data. What is the best method to do this ? Should I be using UIPageViewController ?
Here is a solution for Swift 3 setting up a UIPageViewController programmatically.
The trick is to declare a lazy array called pages on your UIPageViewController.
In viewDidLoad using this array you can set up the viewControllers. Also, your dataSource is able to work with this array, handling the logic of changing the viewControllers. Right now, the dataSource is being implemented to continuously display the viewControllers, like a carousel. Modify it to your needs ;)
import Foundation
import UIKit
struct DisplayableData {
let title: String
let description: String
// etc...
}
class DataViewController: UIViewController {
var data: DisplayableData?
// You can add data as an initalizer parameter, depending on your design needs
init() {
// If you would create a xib for your DataViewController, than replace nib name with DataViewController to make it work
super.init(nibName: nil, bundle: nil)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class PageViewController: UIPageViewController {
lazy internal var pages: [DataViewController] = {
// If the UI is the same, reuse the viewController, no need to create multiple ones
// You could create a xib, and draw the UI there
let firstVC = DataViewController()
// Assign your data structure to your viewController
firstVC.data = DisplayableData(title: "first", description: "desc")
let secondVC = DataViewController()
secondVC.data = DisplayableData(title: "second", description: "desc")
let thirdVC = DataViewController()
thirdVC.data = DisplayableData(title: "third", description: "desc")
return [firstVC, secondVC, thirdVC]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
// Set your viewControllers
setViewControllers([pages.first!], direction: .forward, animated: false, completion: nil)
}
}
extension PageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
// Lets check if the viewController is the right type
guard let viewController = viewController as? DataViewController else {
fatalError("Invalid viewController type in PageViewController")
}
// Load the next one, if it is the last, load the first one
let presentedVCIndex: Int! = pages.index(of: viewController)
if presentedVCIndex + 1 > pages.count - 1 {
return pages.first
}
return pages[presentedVCIndex + 1]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
// Lets check if the viewController is the right type
guard let viewController = viewController as? DataViewController else {
fatalError("Invalid viewController type in PageViewController")
}
// Load the previous one, if it is the first, load the last one
let presentedVCIndex: Int! = pages.index(of: viewController)
if presentedVCIndex - 1 < 0 {
return pages.last
}
return pages[presentedVCIndex - 1]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return pages.count
}
}