View not removed from its superview in iOS - ios

I have a UIView and a collectionView. If there is an internet connection I want to hide the collectionView and show the UIView, if not otherwise.
class MyClass{
#IBOutlet weak var collectionView: UICollectionView!
var myView : CustomView?
....
func internetStatusChanegd(){
if(isOnline){
collectionView.isHidden = true
if let viewNib = UIView.loadFromNibNamed("CustomView", bundle: Bundle.main) as? CustomView {
myView = viewNib
myView!.frame = self.view.bounds
self.view.addSubview(myView!)
}
}else{
if let customView = myView{
customView.removeFromSuperview()
}
collectionView.isHidden = false
}
}
}
removeFromSuperview() Is called but the view is not removed from the view. Do you have an idea about the problem?

While adding a sub view in my view give a tag to that view.
Iterate the for loop for subviews in view.
While removing just check if it's the view with same tag then call-
self.removeFromSuperview()

Please remove already available view before you are adding new view.
func internetStatusChanegd() {
if(isOnline) {
collectionView.isHidden = true
for subView in (self.view.subviews)! {
if (subView.tag == 100) {
subView.removeFromSuperview() //this will remove already available object form self.view
}
}
if let viewNib = UIView.loadFromNibNamed("CustomView", bundle: Bundle.main) as? CustomView {
myView = viewNib
myView!.frame = self.view.bounds
myView.tag = 100 //add tag when you create object
self.view.addSubview(myView!)
}
}else{
if let customView = myView{
customView.removeFromSuperview()
}
collectionView.isHidden = false}
}
}

func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if isOnline{
collectionView.backgroundView = myView //your custom view whatever you want to show here like : button..
return 0
}
collectionView.backgroundView = nil
return array.count
}
func internetStatusChanegd(){ collectionView.reloadData() } //it'll handle automatically that view .
Try this

Remove like this
for subview in self.view.subviews{
if subview is CustomView
{
subview.removeFromSuperview()
}
}

Related

Child view controller's view ignores Auto Layout constraints

I would like to add child view controller to UINavigationController's view. I have hierarchy like this
NavigationController -> View(drawer) -> View(contentContainer) -> child view controller should be pinned to the contentContainer
But for some reason it ignores the constraints and I get weird results. Please see the screenshot, drawer is green, contentContainer is yellow and the child controller is placed almost outside of the screen and has frame with 0 height.
The critical code is inside the drawerContentController didSet and setupView method. Please note that I'm using SnapKit for constraints but the same problem was happening by setting the NSLayoutConstraints traditionally
class NavigationController: UINavigationController {
private var drawerHeightConstraint: Constraint!
fileprivate lazy var drawer = UIView()
private lazy var contentContainer = UIView()
var drawerContentController: UIViewController? {
didSet {
guard let new = drawerContentController else {
oldValue?.removeFromParent(animated: true)
return
}
if let old = oldValue {
old.removeFromParent(animated: true) { _ in
self.add(child: new, superview: self.contentContainer, animated: true, completion: nil)
}
} else {
add(child: new, superview: contentContainer, animated: true, completion: nil)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupView()
}
private func setupView() {
view.addSubview(drawer)
drawer.backgroundColor = .green
drawer.snp.makeConstraints { make in
make.leading.bottom.trailing.equalToSuperview()
drawerHeightConstraint = make.height.equalTo(360).constraint
}
let blur = UIBlurEffect(style: .default)
let visualView = UIVisualEffectView(effect: blur)
drawer.addSubview(visualView)
visualView.pinToSuperView()
contentContainer.backgroundColor = .yellow
drawer.addSubview(contentContainer)
contentContainer.snp.makeConstraints { make in
make.edges.equalTo(drawer.safeAreaLayoutGuide)
}
}
func add(child controller: UIViewController, superview: UIView? = nil, animated: Bool, completion: ((Bool) -> Void)? = nil) {
controller.willMove(toParent: self)
addChild(controller)
controller.view.alpha = 0
(superview ?? view).addSubview(controller.view)
controller.view.pinToSuperView()
UIView.animate(withDuration: animated ? 0.3 : 0, animations: {
controller.view.alpha = 1
}) { finished in
controller.didMove(toParent: self)
completion?(finished)
}
}
}
extension UIView {
func pinToSuperView() {
snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
When I add a simple UIView to the contentContainer it works as expected
var drawerContentController: UIViewController? {
didSet {
let v = UIView()
v.backgroundColor = .purple
contentContainer.addSubview(v)
v.pinToSuperView()
}
}
Okay, I "solved" the issue.
It seems that UINavigationController does not like having child view controllers added to it. As seen on the screenshots, when child controller is added the entire view hierarchy of navigation controller is broken. On the second screenshot, the table view controller (white view) is displayed correctly, but in the first case it is not even in the view hierarchy!
So the moral is, try to not add child view controllers directly to UINavigationController, in the end it is not a normal UIViewController and doesn't even have a normal UIView. I have added another UIViewController to act as a container for UINavigationController and added the child controller to it. It now works as expected.

How to get all the textfields from a view in swift

I have a view which has more than 15 UITextFields. I have to set bottomBorder(extension) for all the UITextFields. I can set it one by one for all the UITextFields and its working too. I want to set the bottom border for all the UITextFields at once. Here is the code I am trying but it seems like that for loop is not executing. I have even tried it in viewDidLayoutSubViews but for loop not executing there too.
override func viewDidLoad()
{
super.viewDidLoad()
/** setting bottom border of textfield**/
for case let textField as UITextField in self.view.subviews {
textField.setBottomBorder()
}
}
Swift: This function will return all text-fields in a view. No matter if field exists in any subview. ;-)
func getAllTextFields(fromView view: UIView)-> [UITextField] {
return view.subviews.flatMap { (view) -> [UITextField] in
if view is UITextField {
return [(view as! UITextField)]
} else {
return getAllTextFields(fromView: view)
}
}.flatMap({$0})
}
Usage:
getAllTextFields(fromView : self.view).forEach{($0.text = "Hey dude!")}
Generic Way:
func getAllSubviews<T: UIView>(fromView view: UIView)-> [T] {
return view.subviews.map { (view) -> [T] in
if let view = view as? T {
return [view]
} else {
return getAllSubviews(fromView: view)
}
}.flatMap({$0})
}
Usage:
let textFields: [UITextField] = getAllSubviews(fromView: self.view)
I made it working, but still need the explanation why the code in question is not working
I got it from somewhere on the forum, not exactle able to credit the answer.
/** extract all the textfield from view **/
func getTextfield(view: UIView) -> [UITextField] {
var results = [UITextField]()
for subview in view.subviews as [UIView] {
if let textField = subview as? UITextField {
results += [textField]
} else {
results += getTextfield(view: subview)
}
}
return results
Call the above function in viewDidLoad or viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
/** setting bottom border to the textfield **/
let allTextField = getTextfield(view: self.view)
for txtField in allTextField
{
txtField.setBottomBorder()
}
}
extension:
extension UIView {
func viewOfType<T:UIView>(type:T.Type, process: (_ view:T) -> Void)
{
if let view = self as? T
{
process(view)
}
else {
for subView in subviews
{
subView.viewOfType(type:type, process:process)
}
}
}
}
Usage:
view.viewOfType(type:UITextField.self) {
view in
view.text = "123"
}
try this
for aSubView: Any in self.view.subviews {
if (aSubView is UITextField) {
var textField = (aSubView as! UITextField)
textField. setBottomBorder()
}
}
or try this
for view in self.view.subviews {
if (view is UITextField) {
var textField = view as! UITextField
textField. setBottomBorder()
}
}
Try this :)
for view in self.view.subviews as! [UIView] {
if let textField = view as? UITextField {
textField.setBottomBorder()
}
}
This worked for me.
var textFieldsArray = [UITextField]()
for view in self.view.subviews {
if view is UITextField {
textFieldsArray.append(view as! UITextField)
}
}
textFieldsArray.forEach { $0.setBottomBorder() }
If you want to get the result of the function applied in a new array, use map() instead.
func getTextFields() {
for textField in view.subviews where view is UITextField {
(textField as? UITextField).setBottomBorder()
}
}
Swift 5
A Very simple answer you can understand easyly
: - You can handle all kind of Objects like UILable, UITextfields, UIButtons, UIView, UIImages . any kind of objecs etc.
for subviews in self.view.subviews {
if subviews is UITextField
{
//MARK: - if the sub view is UITextField you can handle here
funtextfieldsetting(textfield: subviews as! UITextField)
}
if subviews is UIButton
{
//MARK: - if the sub view is UIButton you can handle here
funbuttonsetting(button: subviews as! UIButton)
}
if subviews is UILabel
{
//MARK: - if the sub view is UILabel you can handle here
//Any thing you can do it with label or textfield etc
}
}

Removing the hairline under Navigation Bar

The effect that I want to achieve is:
And the current state of my app is:
This is the set up of my view controller. I put a tool bar underneath the navigation bar. Then, I set the tool bar's delegate to the navigation bar. I've read several posts about this. One solution that was provided was:
navigationController?.navigationBar.shadowImage = UIImage();
navigationController?.navigationBar.setBackgroundImage(UIImage(), forBarMetrics: .Default)
However, this causes the navigation bar to become white and loses the effect. So I got the following code from this post (UISegmentedControl below UINavigationbar in iOS 7):
#IBOutlet weak var toolbar: UIToolbar!
var hairLine: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
doneButton.enabled = false
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if childView is UIImageView && childView.bounds.size.width == self.navigationController!.navigationBar.frame.size.width {
hairLine = childView
print(hairLine.frame)
}
}
}
}
func removeHairLine(appearing: Bool) {
var hairLineFrame = hairLine.frame
if appearing {
hairLineFrame.origin.y += toolbar.bounds.size.height
} else {
hairLineFrame.origin.y -= toolbar.bounds.size.height
}
hairLine.frame = hairLineFrame
print(hairLine.frame)
}
override func viewWillAppear(animated: Bool) {
removeHairLine(true)
}
override func viewWillDisappear(animated: Bool) {
removeHairLine(true)
}
However, this code removes the hairline before the view is completely loaded but when the view is loaded, it appears again. Any solutions?
I found solution on this site but don't remember where exactly.
Objective-C:
#interface YourViewController () {
UIImageView *navBarHairlineImageView;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
navBarHairlineImageView = [self findHairlineImageViewUnder:self.navigationController.navigationBar];
navBarHairlineImageView.hidden = YES;
}
- (UIImageView *)findHairlineImageViewUnder:(UIView *)view {
if ([view isKindOfClass:UIImageView.class] && view.bounds.size.height <= 1.0) {
return (UIImageView *)view;
}
for (UIView *subview in view.subviews) {
UIImageView *imageView = [self findHairlineImageViewUnder:subview];
if (imageView) {
return imageView;
}
}
return nil;
}
Swift:
class YourViewController: UIViewController {
var navBarLine: UIImageView?
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
navBarLine = findHairlineImageViewUnderView(self.navigationController?.navigationBar)
navBarLine?.hidden = true
}
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
if view.isKindOfClass(UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subview in view.subviews {
if let imgView = findHairlineImageViewUnderView(subview) {
return imgView
}
}
return nil
}
}
I use this lines of code
UINavigationBar.appearance().shadowImage = UIImage()
UINavigationBar.appearance().setBackgroundImage(UIImage(named: "background"), for: .default)
Try this
for parent in self.navigationController!.navigationBar.subviews {
for childView in parent.subviews {
if(childView is UIImageView) {
childView.removeFromSuperview()
}
}
}
I hope this help you.
You could use this
self.navigationController?.navigationBar.subviews[0].subviews.filter({$0 is UIImageView})[0].removeFromSuperview()
I didn't find any good Swift 3 solution so I am adding this one, based on Ivan Bruel answer. His solution is protocol oriented, allows to hide hairline in any view controller with just one line of code and without subclassing.
Add this code to your views model:
protocol HideableHairlineViewController {
func hideHairline()
func showHairline()
}
extension HideableHairlineViewController where Self: UIViewController {
func hideHairline() {
findHairline()?.isHidden = true
}
func showHairline() {
findHairline()?.isHidden = false
}
private func findHairline() -> UIImageView? {
return navigationController?.navigationBar.subviews
.flatMap { $0.subviews }
.flatMap { $0 as? UIImageView }
.filter { $0.bounds.size.width == self.navigationController?.navigationBar.bounds.size.width }
.filter { $0.bounds.size.height <= 2 }
.first
}
}
Then make sure view controller which doesn't need hairline conforms to HideableHairlineViewController protocol and call hideHairline().
Swift 4 version of alexandr answer
Step 1: Create property of type UIImageView?
private var navigationBarHairLine: UIImageView?
Step 2: Create findHairlineImageViewUnderView function
This function filters through the view's subviews to find the view with the height of less than or equal to 1pt.
func findHairlineImageViewUnderView(view: UIView?) -> UIImageView? {
guard let view = view else { return nil }
if view.isKind(of: UIImageView.classForCoder()) && view.bounds.height <= 1 {
return view as? UIImageView
}
for subView in view.subviews {
if let imageView = findHairlineImageViewUnderView(view: subView) {
return imageView
}
}
return nil
}
Step 3: Call the created function in ViewWillAppear and pass in the navigationBar. It will return the hairline view which you then set as hidden.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationBarHairLine = findHairlineImageViewUnderView(view: navigationController?.navigationBar)
navigationBarHairLine?.isHidden = true
}
You can subclass UINavigationBar and set the following in initializer (Swift 5):
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
E.g.:
class CustomNavigationBar: UINavigationBar {
override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupViews()
}
private func setupViews() {
shadowImage = UIImage()
setBackgroundImage(UIImage(), for: .default) // needed for iOS 10
}
}

Disable UIPageViewController bouncing - Swift [duplicate]

This question already has answers here:
Disable UIPageViewController bounce
(16 answers)
Closed 5 years ago.
I've been making a sample app with scrolling between UIViewControllers but the point is i want to disable the bouncing effect at the end of scrolling and when going back to the first UIViewController.
here is my code :
class PageViewController: UIPageViewController,UIPageViewControllerDataSource, UIPageViewControllerDelegate{
var index = 0
var identifiers: NSArray = ["FirstNavigationController", "SecondNavigationController"]
override func viewDidLoad() {
self.dataSource = self
self.delegate = self
let startingViewController = self.viewControllerAtIndex(self.index)
let viewControllers: NSArray = [startingViewController]
self.setViewControllers(viewControllers as [AnyObject], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
func viewControllerAtIndex(index: Int) -> UINavigationController! {
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
//first view controller = firstViewControllers navigation controller
if index == 0 {
return storyBoard.instantiateViewControllerWithIdentifier("FirstNavigationController") as! UINavigationController
}
//second view controller = secondViewController's navigation controller
if index == 1 {
return storyBoard.instantiateViewControllerWithIdentifier("SecondNavigationController") as! UINavigationController
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
//if the index is the end of the array, return nil since we dont want a view controller after the last one
if index == identifiers.count - 1 {
return nil
}
//increment the index to get the viewController after the current index
self.index = self.index + 1
return self.viewControllerAtIndex(self.index)
}
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let identifier = viewController.restorationIdentifier
let index = self.identifiers.indexOfObject(identifier!)
//if the index is 0, return nil since we dont want a view controller before the first one
if index == 0 {
return nil
}
//decrement the index to get the viewController before the current one
self.index = self.index - 1
return self.viewControllerAtIndex(self.index)
}
}
Take a look here :
I don't know if you can. UIPageController is not very customizable.
Personally, when I want to scroll between UIViewController, I prefer using a simple UIViewController, which will be a container, with an UIScrollView in it. Then I add programmatically all the controllers in the contentSize of the UIScrollView. You have to add all the controllers as child of the container.
UPDATE iOS 9
func setupDetailViewControllers() {
var previousController: UIViewController?
for controller in self.controllers {
addChildViewController(controller)
addControllerInContentView(controller, previousController: previousController)
controller.didMoveToParentViewController(self)
previousController = controller
}
}
func addControllerInContentView(controller: UIViewController, previousController: UIViewController?) {
contentView.addSubview(controller.view)
controller.view.translatesAutoresizingMaskIntoConstraints = false
// top
controller.view.topAnchor.constraintEqualToAnchor(contentView.topAnchor).active = true
// bottom
controller.view.bottomAnchor.constraintEqualToAnchor(contentView.bottomAnchor).active = true
// trailing
trailingContentViewConstraint?.active = false
trailingContentViewConstraint = controller.view.trailingAnchor.constraintEqualToAnchor(contentView.trailingAnchor)
trailingContentViewConstraint?.active = true
// leading
let leadingAnchor = previousController?.view.trailingAnchor ?? contentView.leadingAnchor
controller.view.leadingAnchor.constraintEqualToAnchor(leadingAnchor).active = true
// width
controller.view.widthAnchor.constraintEqualToAnchor(scrollView.widthAnchor).active = true
}
PREVIOUS ANSWER
Like so :
scrollView is an IBOutlet with contraints to each edge of the ContainerViewController
class ContainerViewController: UIViewController {
#IBOutlet var scrollView: UIScrollView!
override func viewDidLoad() {
super.viewDidLoad()
scrollView.bounces = false
scrollView.pagingEnabled = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewDidLayoutSubviews() {
initScrollView()
}
func initScrollView(){
let viewController1 = storyboard?.instantiateViewControllerWithIdentifier("ViewController1") as! ViewController1
viewController1.willMoveToParentViewController(self)
viewController1.view.frame = scrollView.bounds
let viewController2 = storyboard?.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
viewController2.willMoveToParentViewController(self)
viewController2.view.frame.size = scrollView.frame.size
viewController2.view.frame.origin = CGPoint(x: view.frame.width, y: 0)
scrollView.contentSize = CGSize(width: 2 * scrollView.frame.width, height: scrollView.frame.height)
scrollView.addSubview(viewController2.view)
self.addChildViewController(viewController2)
viewController2.didMoveToParentViewController(self)
scrollView.addSubview(viewController1.view)
self.addChildViewController(viewController1)
viewController1.didMoveToParentViewController(self)
}}

How do I Disable the swipe gesture of UIPageViewController?

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
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 :)
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;
}
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
}
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
}
}
}
pageViewController.view.isUserInteractionEnabled = false
This will disable all interaction with the pages. If you need to user to be able to interact with the content - this is not the solution for you.
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
}
}
Similar to #user3568340 answer
Swift 4
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")
}
}
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
}
}
}

Resources