Disable UIPageViewController Scroll - ios

I have UIPageViewController with 3 child viewControllers. Is it possible to disable/prevent a user from scrolling to one specific viewController (i.e. User can scroll from view controller B to A, but cannot from B to C - until I later toggle permission allowing user to go from B to C).
Any guidance would be greatly appreciated!
I've been able to disable the scroll via UIPageViewController scrollView, but can't isolate disabling to a specific viewController.
func togglePageControllerScroll(shouldScroll: Bool) {
for view in view.subviews {
if let scrollView = view as? UIScrollView {
scrollView.isScrollEnabled = shouldScroll
break
}
}
}
I've also tried putting a check in UIPageViewController delegate functions viewControllerBefore and viewControllerAfter where I would return nil if the user tried scrolling from B to C, but it seems like doing so also returns nil for viewController A...
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = childViewControllers.firstIndex(of: viewController) else { return nil }
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else { return nil }
guard childViewControllers.count > previousIndex else { return nil }
guard let previousViewController = childViewControllers[safe: previousIndex] else { return nil }
if viewController is B && previousViewController is A {
return nil
} else {
return previousViewController
}
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = childViewControllers.firstIndex(of: viewController) else { return nil }
let nextIndex = viewControllerIndex + 1
guard childViewControllers.count != nextIndex else {
return nil
}
guard childViewControllers.count > nextIndex else { return nil }
guard let nextViewController = childViewControllers[safe: nextIndex] else { return nil }
if viewController is B && nextViewController is C {
return nil
} else {
return nextViewController
}
}

One approach - which, I think, is pretty straight-forward and would be a reasonable solution...
Add a maxPages property to your page view controller. Then, in viewControllerAfter, do this:
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
let nextIndex = viewControllerIndex + 1
// if we are at the Last controller,
// OR
// we are at the "maxPages" controller
// return nil
guard nextIndex < pageViewControllers.count, nextIndex < maxPages else { return nil }
return pageViewControllers[nextIndex]
}
Start maxPages at 2, then in the class that is controlling your page view controller, when you want to allow the user to go to the 3rd page, change the maxPages property to 3.
Here is a quick example...
We'll start with a simple "page" view controller - a label centered vertically:
class ExamplePageVC: UIViewController {
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .white
v.textAlignment = .center
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(theLabel)
NSLayoutConstraint.activate([
theLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
theLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
theLabel.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.9),
])
}
}
Then, a view controller to hold our page view controller, along with a switch to Allow/Not-Allow scrolling to the 3rd page:
class PagesViewController: UIViewController {
var myPVC: MyPageViewController!
override func viewDidLoad() {
super.viewDidLoad()
// a label and toggle switch to enable/disable the 3rd page
let label = UILabel()
label.text = "Allow 3rd Page?"
let sw = UISwitch()
sw.isOn = false
sw.addTarget(self, action: #selector(switchChanged(_:)), for: .valueChanged)
let stack = UIStackView(arrangedSubviews: [label, sw])
stack.axis = .horizontal
stack.spacing = 8
// a UIView to hold the page view controller
let pvcContainer = UIView()
pvcContainer.backgroundColor = .gray
[stack, pvcContainer].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
pvcContainer.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
pvcContainer.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
pvcContainer.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pvcContainer.heightAnchor.constraint(equalTo: pvcContainer.widthAnchor, multiplier: 0.75),
stack.bottomAnchor.constraint(equalTo: pvcContainer.topAnchor, constant: -20.0),
stack.centerXAnchor.constraint(equalTo: g.centerXAnchor),
])
let pvc = MyPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal)
addChild(pvc)
pvc.view.translatesAutoresizingMaskIntoConstraints = false
pvcContainer.addSubview(pvc.view)
NSLayoutConstraint.activate([
pvc.view.topAnchor.constraint(equalTo: pvcContainer.topAnchor),
pvc.view.leadingAnchor.constraint(equalTo: pvcContainer.leadingAnchor),
pvc.view.trailingAnchor.constraint(equalTo: pvcContainer.trailingAnchor),
pvc.view.bottomAnchor.constraint(equalTo: pvcContainer.bottomAnchor),
])
pvc.didMove(toParent: self)
myPVC = pvc
}
#objc func switchChanged(_ sender: UISwitch) {
let n: Int = sender.isOn ? 3 : 2
myPVC.maxPages = n
}
}
and our custom Page View Controller:
class MyPageViewController: UIPageViewController {
var maxPages: Int = 2 {
didSet {
// get the current page index
var n = currentIndex
// set controllers, keeping the current page in view
// unless, we are decreasing the count and a higher-index page is showing
if n > maxPages - 1 {
n = maxPages - 1
}
setViewControllers([pageViewControllers[n]], direction: .forward, animated: false, completion: nil)
}
}
// so we can get the index of the current page
var currentIndex: Int {
guard let vc = viewControllers?.first else { return 0 }
return pageViewControllers.firstIndex(of: vc) ?? 0
}
let colors: [UIColor] = [
.systemRed,
.systemGreen,
.systemBlue,
]
var pageViewControllers: [UIViewController] = [UIViewController]()
override init(transitionStyle style: UIPageViewController.TransitionStyle, navigationOrientation: UIPageViewController.NavigationOrientation, options: [UIPageViewController.OptionsKey : Any]? = nil) {
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
// instantiate all "pages"
for i in 0..<colors.count {
let vc = ExamplePageVC()
vc.theLabel.text = "Page: \(i)"
vc.view.backgroundColor = colors[i]
pageViewControllers.append(vc)
}
setViewControllers([pageViewControllers[0]], direction: .forward, animated: false, completion: nil)
}
}
extension MyPageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else { return nil }
return pageViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = pageViewControllers.firstIndex(of: viewController) else { return nil }
let nextIndex = viewControllerIndex + 1
// if we are at the Last controller,
// OR
// we are at the "maxPages" controller
// return nil
guard nextIndex < pageViewControllers.count, nextIndex < maxPages else { return nil }
return pageViewControllers[nextIndex]
}
}
It will look like this:
Arrays are zero-based, so we've set the labels to match the array index.
When the switch is Off, we can only scroll back-and-forth between Page 0 and Page 1.
When the switch is On, we can scroll on to the 3rd page - Page 2:
Notice that we have a little bit of code inside the didSet {} block of our maxPages var. When we set maxPages to a new value, we need to "re-set" the view controllers array of our Page View Controller, while keeping the current page visible.

Related

UIPageViewController: Reverse .scroll animation direction for right-to-left languages

Is it possible to reverse the animation direction for UIPageViewController for right-to-left languages when using UIPageViewController.TransitionStyle.scroll?
Swiping left and right works inverts correctly for right-to-left, but my next button solution animates in the wrong direction (animates like it does for left-to-right).
My current solution to do so is to set pageControl.semanticContentAttribute = .forceLeftToRight, and to inverse the viewModels so that index 0 for right-to-left is the last index (viewModels.count - 1), as well as inverting the animation direction + viewControllerBefore/viewControllerAfter, e.g.:
viewControllerBefore becomes: nextIndex = isRightToLeft ? vc.index - 1 : vc.index + 1(and vice-versa for viewControllerAfter)
nextTapped's inDirection parameter for transitionFrom changes to isRightToLeft ? .reverse : .forward
Change loadFirstPage to set the startIndex to numberOfPages - 1 (aka viewModels.count - 1) rather than 0
Reverse the view models so index 0 is viewModels.count - 1 (in init):
if isRightToLeft {
viewModels = viewModels.map({ viewModel in
ViewModel(index: (numberOfPages - viewModel.index - 1), text: viewModel.text, color: viewModel.color)
}).reversed()
But I would like a solution that simply changes the animation direction out-of-the-box, like it seems to be possible when changing the spineLocation for UIPageViewController.TransitionStyle.pageCurl, like this question states.
Animations
Note that for the How it is by default the page comes from the right, but the page control indicator moves in the opposite (expected) direction.
How it is by default
Expected Functionality
Default Implementation:
import UIKit
struct ViewModel {
var index: Int
var text: String
var color: UIColor
}
class DummyViewController : UIViewController {
let label = UILabel()
let vm: ViewModel
let index: Int
init(vm: ViewModel) {
self.vm = vm
self.index = vm.index
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override func loadView() {
let view = UIView()
view.backgroundColor = vm.color
let label = UILabel()
label.frame = CGRect(x: 20, y: 200, width: 200, height: 20)
label.textColor = .black
label.text = vm.text
view.addSubview(label)
self.view = view
}
}
class ViewController : UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
let pageControl = UIPageControl()
let nextButton = UIButton()
var isRightToLeft: Bool {
traitCollection.layoutDirection == .rightToLeft
}
var currentIndex: Int = 0 {
didSet {
pageControl.currentPage = currentIndex
}
}
var numberOfPages: Int { return viewModels.count }
var viewModels: [ViewModel] = [
ViewModel(index: 0, text: "First", color: .red),
ViewModel(index: 1, text: "Second", color: .blue),
ViewModel(index: 2, text: "Third", color: .green),
]
required init?(coder: NSCoder) {
super.init(coder: coder)
pageViewController.dataSource = self
pageViewController.delegate = self
pageControl.currentPage = currentIndex
pageControl.numberOfPages = numberOfPages
nextButton.setTitle("Next", for: .normal)
nextButton.setTitleColor(.black, for: .normal)
}
override func viewDidLoad() {
addChild(pageViewController)
pageViewController.didMove(toParent: self)
view.addSubview(pageViewController.view)
view.addSubview(pageControl)
view.addSubview(nextButton)
view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
NSLayoutConstraint.activate([
pageViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
pageViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
pageViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageControl.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
nextButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
])
nextButton.addTarget(self, action: #selector(nextTapped), for: .primaryActionTriggered)
loadFirstPage()
}
func loadFirstPage() {
let startIndex: Int
startIndex = 0
guard let start = viewControllerAtIndex(startIndex) else {
return
}
pageViewController.setViewControllers([start], direction: .forward, animated: true, completion: nil)
}
func transitionFrom(index: Int, inDirection direction: UIPageViewController.NavigationDirection) {
let nextIndex = direction == .forward ? index + 1 : index - 1
guard let next = viewControllerAtIndex(nextIndex) else {
return
}
pageViewController.setViewControllers([next], direction: direction, animated: true, completion: { finished in
self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
})
}
func viewControllerAtIndex(_ index: Int) -> DummyViewController? {
guard index >= 0 && index < numberOfPages else { return nil }
let viewModel = viewModels[index]
return DummyViewController(vm: viewModel)
}
#objc
func nextTapped(_ sender: UIButton) {
guard currentIndex < numberOfPages - 1 else {
print("ending because \(currentIndex)")
return
}
let direction: UIPageViewController.NavigationDirection = .forward
transitionFrom(index: currentIndex, inDirection: direction)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? DummyViewController else { return nil }
let nextIndex: Int = vc.index - 1
return viewControllerAtIndex(nextIndex)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? DummyViewController else { return nil }
let nextIndex: Int = vc.index + 1
return viewControllerAtIndex(nextIndex)
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard let viewController = pageViewController.viewControllers?.first as? DummyViewController else { return }
currentIndex = viewController.index // Update currentIndex, which updates pageControl.currentPage
}
}
Manual reversing to support right-to-left (solution I'm trying to avoid).
Excluded ViewModel and DummyViewController as they're unchanged.
import UIKit
class ViewController : UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
let pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
let pageControl = UIPageControl()
let nextButton = UIButton()
var isRightToLeft: Bool {
traitCollection.layoutDirection == .rightToLeft
}
var currentIndex: Int = 0 {
didSet {
pageControl.currentPage = currentIndex
}
}
var numberOfPages: Int {
return viewModels.count
}
var viewModels: [ViewModel] = [
ViewModel(index: 0, text: "First", color: .red),
ViewModel(index: 1, text: "Second", color: .blue),
ViewModel(index: 2, text: "Third", color: .green),
]
required init?(coder: NSCoder) {
super.init(coder: coder)
pageViewController.dataSource = self
pageViewController.delegate = self
pageControl.currentPage = currentIndex
pageControl.numberOfPages = numberOfPages
pageControl.semanticContentAttribute = .forceLeftToRight
nextButton.setTitle("Next", for: .normal)
nextButton.setTitleColor(.black, for: .normal)
if isRightToLeft { // HERE
viewModels = viewModels.map({ viewModel in
ViewModel(index: (numberOfPages - viewModel.index - 1), text: viewModel.text, color: viewModel.color)
}).reversed()
}
}
override func viewDidLoad() {
addChild(pageViewController)
pageViewController.didMove(toParent: self)
view.addSubview(pageViewController.view)
view.addSubview(pageControl)
view.addSubview(nextButton)
view.subviews.forEach { $0.translatesAutoresizingMaskIntoConstraints = false }
NSLayoutConstraint.activate([
pageViewController.view.topAnchor.constraint(equalTo: view.topAnchor),
pageViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor),
pageViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor),
pageViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
pageControl.centerXAnchor.constraint(equalTo: view.centerXAnchor),
pageControl.centerYAnchor.constraint(equalTo: nextButton.centerYAnchor),
nextButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
nextButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor)
])
nextButton.addTarget(self, action: #selector(nextTapped), for: .primaryActionTriggered)
loadFirstPage()
}
func loadFirstPage() {
let startIndex: Int
if isRightToLeft { // HERE
startIndex = numberOfPages - 1
currentIndex = startIndex
} else {
startIndex = 0
}
guard let start = viewControllerAtIndex(startIndex) else {
return
}
pageViewController.setViewControllers([start], direction: .forward, animated: true, completion: nil)
}
func transitionFrom(index: Int, inDirection direction: UIPageViewController.NavigationDirection) { // UNCHANGED
let nextIndex = direction == .forward ? index + 1 : index - 1
guard let next = viewControllerAtIndex(nextIndex) else {
return
}
pageViewController.setViewControllers([next], direction: direction, animated: true, completion: { finished in
self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
})
}
func viewControllerAtIndex(_ index: Int) -> DummyViewController? {
guard index >= 0 && index < numberOfPages else { return nil }
let viewModel = viewModels[index]
return DummyViewController(vm: vm)
}
#objc
func nextTapped(_ sender: UIButton) {
guard (currentIndex < numberOfPages - 1 && !isRightToLeft) || (currentIndex > 0 && isRightToLeft) else { // HERE
return
}
let direction: UIPageViewController.NavigationDirection = isRightToLeft ? .reverse : .forward // HERE
transitionFrom(index: currentIndex, inDirection: direction)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? DummyViewController else {
return nil
}
let nextIndex: Int
if isRightToLeft { // HERE
nextIndex = vc.index + 1
} else {
nextIndex = vc.index - 1
}
return viewControllerAtIndex(nextIndex)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let vc = viewController as? DummyViewController else {
return nil
}
let nextIndex: Int
if isRightToLeft { // HERE
nextIndex = vc.index - 1
} else {
nextIndex = vc.index + 1
}
return viewControllerAtIndex(nextIndex)
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard let viewController = pageViewController.viewControllers?.first as? DummyViewController else {
return
}
currentIndex = viewController.index
}
}
Turns out this is much simpler than I realized - setting the direction simply changes the animation direction, it doesn't change what index/view controller is going to be presented.
As such, the solution is simply changing transitionFrom to:
func transitionTo(nextIndex: Int, inDirection direction: UIPageViewController.NavigationDirection) {
// Removed the `next` setting here in favor of getting it passed from `nextTapped`
guard let next = viewControllerAtIndex(nextIndex) else {
return
}
pageViewController.setViewControllers([next], direction: direction, animated: true, completion: { finished in
self.pageViewController(self.pageViewController, didFinishAnimating: finished, previousViewControllers: [], transitionCompleted: finished)
})
}
and changing nextTapped to:
#objc
func nextTapped(_ sender: UIButton) {
guard currentIndex < numberOfPages - 1 else { return }
let direction: UIPageViewController.NavigationDirection = isRightToLeft ? .reverse : .forward // This is key
transitionTo(nextIndex: currentIndex + 1, inDirection: direction)
}
Alternate, less intrusive solution
Alternatively, a less intrusive solution would be to subclass UIPageViewController and simply flip the animation direction if we're in a right-to-left locale.
private extension UIPageViewController.NavigationDirection {
var flipped: Self {
switch self {
case .forward:
return .reverse
case .reverse:
return .forward
#unknown default:
return .reverse
}
}
}
class LocalizedPageViewController: UIPageViewController {
override func setViewControllers(
_ viewControllers: [UIViewController]?,
direction: UIPageViewController.NavigationDirection,
animated: Bool,
completion: ((Bool) -> Void)? = nil
) {
let isRTL = view.effectiveUserInterfaceLayoutDirection == .rightToLeft
let direction = isRTL ? direction.flipped : direction
super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
}
}
Then I would simply need to change my initialization of the UIPageViewController to LocalizedPageViewController, and no other changes are needed!

Child View Controllers in Page View Controller Failing to Receive Delegate Calls

I am having an issue with my two child view controllers inside a parent PageViewController, where a delegate called by one of the children is not received by the other child.
My first child contains buttons, and when a button is pressed, a delegate is triggered in the other child to pause the timer. However, it fails to receive the call and the timer continues to run.
Here is my PageViewController:
class StartMaplessWorkoutPageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
lazy var workoutViewControllers: [UIViewController] = {
return [self.getNewViewController(viewController: "ButtonsViewController"), self.getNewViewController(viewController: "DisplayMaplessViewController")]
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.dataSource = self
// Saw this from another answer, doesn't do anything that helps (at the moment)
let buttonsViewController = storyboard?.instantiateViewController(withIdentifier: "ButtonsViewController") as! ButtonsViewController
let displayMaplessViewController = storyboard?.instantiateViewController(withIdentifier: "DisplayMaplessViewController") as! DisplayMaplessViewController
buttonsViewController.buttonsDelegate = displayMaplessViewController
if let firstViewController = workoutViewControllers.last {
setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
}
let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [StartWorkoutPageViewController.self])
pageControl.currentPageIndicatorTintColor = .orange
pageControl.pageIndicatorTintColor = .gray
}
func getNewViewController(viewController: String) -> UIViewController {
return (storyboard?.instantiateViewController(withIdentifier: viewController))!
}
// MARK: PageView DataSource
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return workoutViewControllers.last
}
guard workoutViewControllers.count > previousIndex else {
return nil
}
return workoutViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let workoutViewControllersCount = workoutViewControllers.count
guard workoutViewControllersCount != nextIndex else {
return workoutViewControllers.first
}
guard workoutViewControllersCount > nextIndex else {
return nil
}
return workoutViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return workoutViewControllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
guard let firstViewController = viewControllers?.first, let firstViewControllerIndex = workoutViewControllers.firstIndex(of: firstViewController) else {
return 0
}
return firstViewControllerIndex
}
}
My ChildViewController with Buttons:
protocol ButtonsViewDelegate: class {
func onButtonPressed(button: String)
}
class ButtonsViewController: UIViewController {
weak var buttonsDelegate: ButtonsViewDelegate?
var isPaused: Bool = false
#IBOutlet weak var startStopButton: UIButton!
#IBOutlet weak var optionsButton: UIButton!
#IBOutlet weak var endButton: UIButton!
#IBAction func startStopButton(_ sender: Any) {
if isPaused == true {
buttonsDelegate?.onButtonPressed(button: "Start")
isPaused = false
} else {
buttonsDelegate?.onButtonPressed(button: "Pause")
isPaused = true
}
}
#IBAction func endButton(_ sender: Any) {
let menu = UIAlertController(title: "End", message: "Are you sure you want to end?", preferredStyle: .actionSheet)
let end = UIAlertAction(title: "End", style: .default, handler: { handler in
self.buttonsDelegate?.onButtonPressed(button: "End")
})
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
menu.addAction(end)
menu.addAction(cancelAction)
self.present(menu, animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
My other ChildViewController, which should be receiving the calls of the ButtonsViewDelegate:
import UIKit
class DisplayMaplessViewController: UIViewController, ButtonsViewDelegate {
var timer = Timer()
var currentTime: TimeInterval = 0.0
var isCountdown: Bool = false
var isInterval: Bool = false
var currentRepeats: Int = 0
var currentActivity: Int = 0
var count: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
startIntervalTimer(withTime: 0)
}
// Currently not being called
func onButtonPressed(button: String) {
switch button {
case "Start":
restartIntervalTimer()
case "Pause":
pauseIntervalTimer()
case "End":
stop()
default:
break
}
}
func startIntervalTimer(withTime: Double) {
if withTime != 0 {
currentTime = withTime
if isInterval != true {
isCountdown = true
}
}
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(intervalTimerUpdate), userInfo: nil, repeats: true)
}
func pauseIntervalTimer() {
timer.invalidate()
}
func restartIntervalTimer() {
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(intervalTimerUpdate), userInfo: nil, repeats: true)
}
// Currently Not being called
func stop() {
timer.invalidate()
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .positional
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = [.pad]
let timeString = formatter.string(from: currentTime)
// save the data etc
print("Stop is called")
}
#objc func intervalTimerUpdate() {
currentTime += 1.0
print(currentTime)
}
}
Sorry that this is so long winded, been trying for quite a while and really annoyed that it doesn't work! Thanks!
I'll try to be clear, hopefully i'll be so as english is not my native language.
It seems to me that you are instantiating your ViewControllers to be presented in the getNewViewController() method and storing them in the workoutViewControllers array, but you are setting the delegate as a separate instance that you never set in your PageVC. You need to set the delegates using the same instances.
These two are two instances of two VC classes (also not sure if the identifier "DisplayViewController" is right, i expected "DisplayMaplessViewController", hard to tell without the storyboard):
let buttonsViewController = storyboard?.instantiateViewController(withIdentifier: "ButtonsViewController") as! ButtonsViewController
let displayMaplessViewController = storyboard?.instantiateViewController(withIdentifier: "DisplayViewController") as! DisplayMaplessViewController
buttonsViewController.buttonsDelegate = displayMaplessViewController
And these in the array two other instances, unrelated from the ones above, of the same two classes:
lazy var workoutViewControllers: [UIViewController] = {
return [self.getNewViewController(viewController: "ButtonsViewController"), self.getNewViewController(viewController: "DisplayMaplessViewController")]
}()
To better understand what i mean, i refactored from scratch and semplified your project (had to do it programmatically as i'm not used to storyboards).
It now consists of a PageController that displays a buttonsVC with a red button and a displayMaplessVC with a blue background.
Once you press the red button, the delegate method is called which causes the blue background to turn green.
Take a look at what i'm doing, as i'm appending the same instances of which i set the delegate:
instantiate a DisplayMaplessViewController object and ButtonsViewController object;
set buttonsVC.buttonsDelegate = displayMaplessVC;
append both ViewControllers to the array.
This is a way to get it done but for sure there are several other ways to achieve the same result, once you get the point and understand your mistake you can pick the one you like the most.
Just copy and paste it into a new project, build and run (you have to set the class of the starting ViewController in the Storyboard as StartMaplessWorkoutPageViewController):
import UIKit
class StartMaplessWorkoutPageViewController: UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
private var workoutViewControllers = [UIViewController]()
private let pageController: UIPageViewController = {
let pageController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
return pageController
}()
override func viewDidLoad() {
super.viewDidLoad()
pageController.delegate = self
pageController.dataSource = self
let buttonsVC = ButtonsViewController()
let displayMaplessVC = DisplayMaplessViewController()
buttonsVC.buttonsDelegate = displayMaplessVC
workoutViewControllers.append(buttonsVC)
workoutViewControllers.append(displayMaplessVC)
self.addChild(self.pageController)
self.view.addSubview(self.pageController.view)
self.pageController.setViewControllers([displayMaplessVC], direction: .forward, animated: true, completion: nil)
self.pageController.didMove(toParent: self)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
pageController.view.frame = view.bounds
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return workoutViewControllers.last
}
guard workoutViewControllers.count > previousIndex else {
return nil
}
return workoutViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = workoutViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let workoutViewControllersCount = workoutViewControllers.count
guard workoutViewControllersCount != nextIndex else {
return workoutViewControllers.first
}
guard workoutViewControllersCount > nextIndex else {
return nil
}
return workoutViewControllers[nextIndex]
}
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return workoutViewControllers.count
}
}
.
protocol ButtonsViewDelegate: class {
func onButtonPressed()
}
import UIKit
class ButtonsViewController: UIViewController {
weak var buttonsDelegate: ButtonsViewDelegate?
let button: UIButton = {
let button = UIButton()
button.backgroundColor = .red
button.addTarget(self, action: #selector(onButtonPressed), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(button)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
button.frame = CGRect(x: 50,
y: 50,
width: 100,
height: 100)
}
#objc private func onButtonPressed() {
buttonsDelegate?.onButtonPressed()
}
}
.
import UIKit
class DisplayMaplessViewController: UIViewController, ButtonsViewDelegate {
private let testView: UIView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(testView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
testView.frame = view.bounds
}
internal func onButtonPressed() {
testView.backgroundColor = .green
}
}

Got nil for delegate at ViewController included in UIPageViewController

I have a UIPageViewController and several ViewControllers in it. And I want to perform a function in UIPageViewController from a ViewController using delegate protocol. But get nil in the delegate. Can anybody determine what I'm doing wrong?
PageViewController.swift
import UIKit
class PageViewController: UIPageViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource, Storyboarded {
weak var coordinator: MainCoordinator?
lazy var orderedViewControllers: [UIViewController] = {
return [self.newInstanceVC(viewController: "FirstLoadViewController"), self.newInstanceVC(viewController: "FirstLoad2ViewController")]
}()
var pageControl = UIPageControl()
override func viewDidLoad() {
super.viewDidLoad()
let storyboardVC = newInstanceVC(viewController: "FirstLoad2ViewController") as? FirstLoad2ViewController
storyboardVC?.delegate = self
self.dataSource = self
if let firstViewController = orderedViewControllers.first {
setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
}
self.delegate = self
configurePageControl()
}
func configurePageControl() {
pageControl = UIPageControl(frame: CGRect(x: 0, y: UIScreen.main.bounds.maxY - 50, width: UIScreen.main.bounds.width, height: 50))
pageControl.numberOfPages = orderedViewControllers.count
pageControl.currentPage = 0
//pageControl.tintColor = .red
pageControl.pageIndicatorTintColor = .systemGray5
pageControl.currentPageIndicatorTintColor = .systemGray
self.view.addSubview(pageControl)
}
//return view controller by string identifier
func newInstanceVC(viewController: String) -> UIViewController {
return UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: viewController)
}
//MARK: - Setup page controllers
// view controller that appears on swiping to the -> (left)
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
//return view controller from the end if it is last index from the left side (infinit scroll)
//return orderedViewControllers.last
return nil
}
guard orderedViewControllers.count >= previousIndex else {
return nil
}
return orderedViewControllers[previousIndex]
}
// view controller that appears on swiping to the <- (right)
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = orderedViewControllers.firstIndex(of: viewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
guard orderedViewControllers.count != nextIndex else {
//return view controller from the begining if it is last index from the right side (infinit scroll)
//return orderedViewControllers.first
return nil
}
guard orderedViewControllers.count > nextIndex else {
return nil
}
return orderedViewControllers[nextIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
//switch the bullet of the page
let pageContentVievController = pageViewController.viewControllers![0]
self.pageControl.currentPage = orderedViewControllers.firstIndex(of: pageContentVievController)!
}
}
extension PageViewController: PageViewDelegate {
func handleButtonTap() {
coordinator?.goToMainFromPresentation()
}
}
OneOfTheViewControllers.swift
import UIKit
protocol PageViewDelegate: class {
func handleButtonTap()
}
class FirstLoad2ViewController: UIViewController, Storyboarded {
weak var coordinator: MainCoordinator?
weak var child: ChildCoordinator?
weak var delegate: PageViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func toMainScreenTap(_ sender: Any) {
delegate!.handleButtonTap() //got nil for delegate here
}
}
The problem is simple, it's hiding in your viewDidLoad() method inside PageViewController
...
override func viewDidLoad() {
super.viewDidLoad()
let storyboardVC = newInstanceVC(viewController: "FirstLoad2ViewController") as? FirstLoad2ViewController
storyboardVC?.delegate = self
...
}
/// (storyboardVC) --- will be destroyed when out of viewDidLoad() scope.
So, why you getting your storyboardVC?.delegate = self nil? Because you created storyboardVC property inside viewDidLoad() method and nothing is keeping reference to it. So, at the end viewDidLoad() method it just deinitialize as well as it's PageViewDelegate delegate

UIPageViewController - changing dots won't work

I'm using a UIPageViewController to swipe between two ViewControllers. I tried to display indicator dots, which worked (see addDots). But somehow the dots will only change on next swipe, but not when I swipe to the previous view.
Can anybody tell me where I've made a mistake?
Here's my code:
import UIKit
class LeasingTutorialViewController: UIPageViewController, UIPageViewControllerDataSource {
var pageControl = UIPageControl()
lazy var viewControllerList:[UIViewController] = {
let sb = UIStoryboard(name: "Main", bundle: nil)
let vc1 = sb.instantiateViewController(withIdentifier: "leasingPageOne")
let vc2 = sb.instantiateViewController(withIdentifier: "leasingPageTwo")
return [vc1, vc2]
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dataSource = self
addDots()
if let firstViewController = viewControllerList.first as? LeasingPageOneViewController {
self.setViewControllers([firstViewController], direction: .forward, animated: true, completion: nil)
}
}
func addDots() {
let color = UIColor(red: 24/255, green: 90/255, blue: 189/255, alpha: 1)
pageControl = UIPageControl(frame: CGRect(x: 0,y: UIScreen.main.bounds.maxY - 100,width: UIScreen.main.bounds.width,height: 50))
pageControl.numberOfPages = viewControllerList.count
pageControl.currentPage = 0
pageControl.tintColor = color
pageControl.pageIndicatorTintColor = UIColor.lightGray
pageControl.currentPageIndicatorTintColor = color
self.view.addSubview(pageControl)
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let vcIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let previousIndex = vcIndex - 1
guard previousIndex >= 0 else { return nil }
guard viewControllerList.count > previousIndex else { return nil }
self.pageControl.currentPage = previousIndex
return viewControllerList[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let vcIndex = viewControllerList.firstIndex(of: viewController) else { return nil }
let nextIndex = vcIndex + 1
guard viewControllerList.count != nextIndex else { return nil }
guard viewControllerList.count > nextIndex else { return nil }
self.pageControl.currentPage = nextIndex
return viewControllerList[nextIndex]
}
}

UIPageViewController programmatically swift 4

I wrote this code that checked several times I can not understand where I'm wrong.
I can not see my page..
but I see a black background with the 3 dots of pageControl.
I checked several times, I set the yellow background on the first page..
can you give me a hand?
I have 3-4 more files in which I declare the pages and their layout
Result
final class IntroViewController: UIPageViewController, UIPageViewControllerDelegate {
var introRouter: IntroRouter?
var pages = [UIViewController]()
let pageControl = UIPageControl()
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
// self.delegate = self
let initialPage = 1
let page1 = ViewController1()
let page2 = ViewController2()
let page3 = ViewController3()
let page4 = ViewController4()
self.pages.append(page1)
self.pages.append(page2)
self.pages.append(page3)
self.pages.append(page4)
setViewControllers([pages[initialPage]], direction: .forward, animated: true, completion: nil)
self.pageControl.frame = CGRect()
self.pageControl.currentPageIndicatorTintColor = UIColor.black
self.pageControl.pageIndicatorTintColor = UIColor.lightGray
self.pageControl.numberOfPages = self.pages.count
self.pageControl.currentPage = initialPage
self.view.addSubview(self.pageControl)
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
self.pageControl.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -5).isActive = true
self.pageControl.widthAnchor.constraint(equalTo: self.view.widthAnchor, constant: -20).isActive = true
self.pageControl.heightAnchor.constraint(equalToConstant: 20).isActive = true
self.pageControl.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
arrangeSubviews()
}
override init(transitionStyle style: UIPageViewControllerTransitionStyle, navigationOrientation: UIPageViewControllerNavigationOrientation, options: [String : Any]? = nil) {
super.init(transitionStyle: style, navigationOrientation: navigationOrientation, options: options)
}
func setRouter(introRouter: IntroRouter) {
self.introRouter = introRouter
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private extension IntroViewController {
func IntroViewController(_ IntroViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
if let viewControllerIndex = self.pages.index(of: viewController) {
if viewControllerIndex == 0 {
// wrap to last page in array
return self.pages.last
} else {
// go to previous page in array
return self.pages[viewControllerIndex - 1]
}
}
return nil
}
func IntroViewController(_ IntroViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
if let viewControllerIndex = self.pages.index(of: viewController) {
if viewControllerIndex < self.pages.count - 1 {
// go to next page in array
return self.pages[viewControllerIndex + 1]
} else {
// wrap to first page in array
return self.pages.first
}
}
return nil
}
func IntroViewController(_ IntroViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
// set the pageControl.currentPage to the index of the current viewController in pages
if let viewControllers = IntroViewController.viewControllers {
if let viewControllerIndex = self.pages.index(of: viewControllers[0]) {
self.pageControl.currentPage = viewControllerIndex
}
}
}
func setupUI() {
pageControl.do {
$0.numberOfPages = 4
$0.currentPage = 1
$0.pageIndicatorTintColor = .lightGray
$0.currentPageIndicatorTintColor = Theme.Colors.white
}
}
func arrangeSubviews() {
view.addSubview(pageControl)
}
}

Resources