In my case parent UIViewController
contains UIPageViewController
which contains UINavigationController
which contains UIViewController
. I need to add a swipe gesture to the last view controller, but swipes are handled as if they belong to page view controller. I tried to do this both programmatically and via xib but with no result.
So as I understand I can't achieve my goal until UIPageViewController
handles its gestures. How to solve this issue?
The documented way to prevent the UIPageViewController
from scrolling is to not assign the dataSource
property. If you assign the data source it will move into 'gesture-based' navigation mode which is what you're trying to prevent.
Without a data source you manually provide view controllers when you want to with setViewControllers:direction:animated:completion
method and it will move between view controllers on demand.
The above can be deduced from Apple's documentation of UIPageViewController (Overview, second paragraph):
To support gesture-based navigation, you must provide your view controllers using a data source object.
for (UIScrollView *view in self.pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
view.scrollEnabled = NO;
}
}
I translate answer of user2159978 to Swift 5.1
func removeSwipeGesture(){
for view in self.pageViewController!.view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = false
}
}
}
Implementing @lee's (@user2159978's) solution as an extension:
extension UIPageViewController {
var isPagingEnabled: Bool {
get {
var isEnabled: Bool = true
for view in view.subviews {
if let subView = view as? UIScrollView {
isEnabled = subView.isScrollEnabled
}
}
return isEnabled
}
set {
for view in view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = newValue
}
}
}
}
}
Usage: (in UIPageViewController
)
self.isPagingEnabled = false
Edit : this answer works for page curl style only. Jessedc's answer is far better: works regardless of the style and relies on documented behavior .
UIPageViewController
exposes its array of gesture recognizers, which you could use to disable them:
// myPageViewController is your UIPageViewController instance
for (UIGestureRecognizer *recognizer in myPageViewController.gestureRecognizers) {
recognizer.enabled = NO;
}
I've been fighting this for a while now and thought I should post my solution, following on from Jessedc's answer; removing the PageViewController's datasource.
I added this to my PgeViewController class (linked to my page view controller in the storyboard, inherits both UIPageViewController
and UIPageViewControllerDataSource
):
static func enable(enable: Bool){
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let pageViewController = appDelegate.window!.rootViewController as! PgeViewController
if (enable){
pageViewController.dataSource = pageViewController
}else{
pageViewController.dataSource = nil
}
}
This can then be called when each sub view appears (in this case to disable it);
override func viewDidAppear(animated: Bool) {
PgeViewController.enable(false)
}
I hope this helps someone out, its not as clean as I would like it but doesn't feel too hacky etc.
EDIT: If someone wants to translate this into Objective-C please do :)
A useful extension of UIPageViewController
to enable and disable swipe.
extension UIPageViewController {
func enableSwipeGesture() {
for view in self.view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = true
}
}
}
func disableSwipeGesture() {
for view in self.view.subviews {
if let subView = view as? UIScrollView {
subView.isScrollEnabled = false
}
}
}
}
If you want your UIPageViewController
to maintain it's ability to swipe, while allowing your content controls to use their features (Swipe to delete, etc), just turn off canCancelContentTouches
in the UIPageViewController
.
Put this in your UIPageViewController
's viewDidLoad
func. (Swift)
if let myView = view?.subviews.first as? UIScrollView {
myView.canCancelContentTouches = false
}
The UIPageViewController
has an auto-generated subview that handles the gestures. We can prevent these subviews from cancelling content gestures.
From...
Swipe to delete on a tableView that is inside a pageViewController
Swifty way for @lee answer
extension UIPageViewController {
var isPagingEnabled: Bool {
get {
return scrollView?.isScrollEnabled ?? false
}
set {
scrollView?.isScrollEnabled = newValue
}
}
var scrollView: UIScrollView? {
return view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
}
}
I solved it like this (Swift 4.1)
if let scrollView = self.view.subviews.filter({$0.isKind(of: UIScrollView.self)}).first as? UIScrollView {
scrollView.isScrollEnabled = false
}
pageViewController.view.isUserInteractionEnabled = false
Here is my solution in swift
extension UIPageViewController {
var isScrollEnabled: Bool {
set {
(self.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView)?.isScrollEnabled = newValue
}
get {
return (self.view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView)?.isScrollEnabled ?? true
}
}
}
Similar to @user3568340 answer
private var _enabled = true
public var enabled:Bool {
set {
if _enabled != newValue {
_enabled = newValue
if _enabled {
dataSource = self
}
else{
dataSource = nil
}
}
}
get {
return _enabled
}
}
Translating @user2159978's response to C#:
foreach (var view in pageViewController.View.Subviews){
var subView = view as UIScrollView;
if (subView != null){
subView.ScrollEnabled = enabled;
}
}
Thanks to @user2159978's answer.
I make it a little more understandable.
- (void)disableScroll{
for (UIView *view in self.pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
UIScrollView * aView = (UIScrollView *)view;
aView.scrollEnabled = NO;
}
}
}
(Swift 4) You can remove gestureRecognizers of your pageViewController:
pageViewController.view.gestureRecognizers?.forEach({ (gesture) in
pageViewController.view.removeGestureRecognizer(gesture)
})
If you prefer in extension:
extension UIViewController{
func removeGestureRecognizers(){
view.gestureRecognizers?.forEach({ (gesture) in
view.removeGestureRecognizer(gesture)
})
}
}
and pageViewController.removeGestureRecognizers
Declare it like this:
private var scrollView: UIScrollView? {
return pageViewController.view.subviews.compactMap { $0 as? UIScrollView }.first
}
Then use it like this:
scrollView?.isScrollEnabled = true //false
The answers I found look very confusing or incomplete to me so here is a complete and configurable solution:
Step 1:
Give each of your PVC elements the responsibility to tell whether left and right scrolling are enabled or not.
protocol PageViewControllerElement: class {
var isLeftScrollEnabled: Bool { get }
var isRightScrollEnabled: Bool { get }
}
extension PageViewControllerElement {
// scroll is enabled in both directions by default
var isLeftScrollEnabled: Bool {
get {
return true
}
}
var isRightScrollEnabled: Bool {
get {
return true
}
}
}
Each of your PVC view controllers should implement the above protocol.
Step 2:
In your PVC controllers, disable the scroll if needed:
extension SomeViewController: PageViewControllerElement {
var isRightScrollEnabled: Bool {
get {
return false
}
}
}
class SomeViewController: UIViewController {
// ...
}
Step 3:
Add the effective scroll lock methods to your PVC:
class PVC: UIPageViewController, UIPageViewDelegate {
private var isLeftScrollEnabled = true
private var isRightScrollEnabled = true
// ...
override func viewDidLoad() {
super.viewDidLoad()
// ...
self.delegate = self
self.scrollView?.delegate = self
}
}
extension PVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("contentOffset = \(scrollView.contentOffset.x)")
if !self.isLeftScrollEnabled {
disableLeftScroll(scrollView)
}
if !self.isRightScrollEnabled {
disableRightScroll(scrollView)
}
}
private func disableLeftScroll(_ scrollView: UIScrollView) {
let screenWidth = UIScreen.main.bounds.width
if scrollView.contentOffset.x < screenWidth {
scrollView.setContentOffset(CGPoint(x: screenWidth, y: 0), animated: false)
}
}
private func disableRightScroll(_ scrollView: UIScrollView) {
let screenWidth = UIScreen.main.bounds.width
if scrollView.contentOffset.x > screenWidth {
scrollView.setContentOffset(CGPoint(x: screenWidth, y: 0), animated: false)
}
}
}
extension UIPageViewController {
var scrollView: UIScrollView? {
return view.subviews.filter { $0 is UIScrollView }.first as? UIScrollView
}
}
Step 4:
Update scroll related attributes when reaching a new screen (if you transition to some screen manually don't forget to call the enableScroll method):
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
let pageContentViewController = pageViewController.viewControllers![0]
// ...
self.enableScroll(for: pageContentViewController)
}
private func enableScroll(for viewController: UIViewController) {
guard let viewController = viewController as? PageViewControllerElement else {
self.isLeftScrollEnabled = true
self.isRightScrollEnabled = true
return
}
self.isLeftScrollEnabled = viewController.isLeftScrollEnabled
self.isRightScrollEnabled = viewController.isRightScrollEnabled
if !self.isLeftScrollEnabled {
print("Left Scroll Disabled")
}
if !self.isRightScrollEnabled {
print("Right Scroll Disabled")
}
}
There's a much simpler approach than most answers here suggest, which is to return nil
in the viewControllerBefore
and viewControllerAfter
dataSource callbacks.
This disables the scrolling gesture on iOS 11+ devices, while keeping the possibility to use the dataSource
(for things such as the presentationIndex
/ presentationCount
used for the page indicator)
It also disables navigation via. the pageControl
(the dots in the bottom) for iOS 11-13. On iOS 14, the bottom dots navigation can be disabled using a UIAppearance proxy.
extension MyPageViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
return nil
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
return nil
}
}
Having no datasource is the right answer as said above.
Here is my handy/short Swift 5 solution though.
It builds on the fact that my subclass of UIPageViewController also contains its dataSource.
extension MyPagerVC: UIPageViewControllerDataSource {
var isPagingEnabled:Bool {
get { dataSource == nil ? false : true }
set { dataSource = newValue ? self : nil }
}
}
class MyPagerVC:UIPageViewController {
override func viewDidLoad() {
isPagingEnabled = true
// or simply set your dataSource
dataSource = self
}
}
More efficient way with a return, call this method on viewdidload (Swift 5):
private func removeSwipeGesture() {
self.pageViewController?.view.subviews.forEach({ view in
if let subView = view as? UIScrollView {
subView.isScrollEnabled = false
return
}
})
}
You can implement the UIPageViewControllerDataSource
protocol and return nil
for the previousViewController and nextViewController methods. This will prevent the UIPageViewController
from being able to swipe to the next or previous page.
fileprivate func canSwipeToNextViewController() -> Bool {
guard
currentIndex < controllers.count,
let controller = controllers[currentIndex] as? OnboardingBaseViewController,
controller.canSwipeToNextScreen
else {
return false
}
return true
}
}
// MARK: - UIPageViewControllerDataSource
extension ViewController: UIPageViewControllerDataSource {
func presentationCount(for pageViewController: UIPageViewController) -> Int {
controllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
currentIndex
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController
) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController) {
if index > 0 {
currentIndex -= 1
return controllers[index - 1]
} else {
// Return nil to prevent swiping to the previous page
return nil
}
}
return nil
}
func pageViewController(
_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController
) -> UIViewController? {
if let index = controllers.firstIndex(of: viewController) {
if index < controllers.count - 1,
canSwipeToNextViewController() {
currentIndex += 1
return controllers[index + 1]
} else {
// Return nil to prevent swiping to the next page
return nil
}
}
return nil
}
}
Remember to set the dataSource
property of the UIPageViewController
to the view controller that implements the UIPageViewControllerDataSource
protocol.
I hope that helps.
Enumerating the subviews to find the scrollView of a UIPageViewController
didn't work for me, as I can't find any scrollView in my page controller subclass. So what I thought of doing is to disable the gesture recognizers, but careful enough to not disable the necessary ones.
So I came up with this:
if let panGesture = self.gestureRecognizers.filter({$0.isKind(of: UIPanGestureRecognizer.self)}).first
panGesture.isEnabled = false
}
Put that inside the viewDidLoad()
and you're all set!
override func viewDidLayoutSubviews() {
for View in self.view.subviews{
if View.isKind(of: UIScrollView.self){
let ScrollV = View as! UIScrollView
ScrollV.isScrollEnabled = false
}
}
}
Add this in your pageviewcontroller class. 100% working
just add this control property at your UIPageViewController subclass:
var isScrollEnabled = true {
didSet {
for case let scrollView as UIScrollView in view.subviews {
scrollView.isScrollEnabled = isScrollEnabled
}
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.