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
}
}
}
Related
I have two view controllers. MainViewController and SecondViewController (this one is embedded in a Navigation Controller).
MainViewController has a UIButton that will modally present SecondViewController, while SecondViewController has a UIButton that will dismiss itself.
Each of them have the following code:
var statusBarHidden = false {
didSet {
UIView.animate(withDuration: 0.5) { () -> Void in
self.setNeedsStatusBarAppearanceUpdate()
}
}
}
override var prefersStatusBarHidden: Bool {
return statusBarHidden
}
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
statusBarHidden = true
}
The slide animation of the status bar works great in the simulator but not on the actual device, what am i doing wrong ?
I'm using xCode 8.2.1 and Swift 3
What i ended up doing was this. I created a variable that links to the view of the status bar and added functions so i can do what i need.
extension UIApplication {
var statusBarView: UIView? {
return value(forKey: "statusBar") as? UIView
}
func changeStatusBar(alpha: CGFloat) {
statusBarView?.alpha = alpha
}
func hideStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 0
}
}
func showStatusBar() {
UIView.animate(withDuration: 0.3) {
self.statusBarView?.alpha = 1
}
}
}
A typical use would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let alpha = tableView.contentOffset.y / 100
UIApplication.shared.changeStatusBar(alpha: alpha)
}
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
}
}
I have a simple UIPageViewController which displays the default UIPageControl at the bottom of the pages. I wonder if it's possible to modify the position of the UIPageControl, e.g. to be on top of the screen instead of the bottom.
I've been looking around and only found old discussions that say I need to create my own UIPageControl.
Is this thing simpler with iOS8 and 9?
Thanks.
Yes, you can add custom page controller for that.
self.pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(0, self.view.frame.size.height - 50, self.view.frame.size.width, 50)]; // your position
[self.view addSubview: self.pageControl];
then remove
- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController
and
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
Then add another delegate method:
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray<UIViewController *> *)pendingViewControllers
{
PageContentViewController *pageContentView = (PageContentViewController*) pendingViewControllers[0];
self.pageControl.currentPage = pageContentView.pageIndex;
}
Just lookup PageControl in PageViewController subclass and set frame, location or whatever you want
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subView in view.subviews {
if subView is UIPageControl {
subView.frame.origin.y = self.view.frame.size.height - 164
}
}
}
Override the viewDidLayoutSubviews() of the pageviewcontroller and use this
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// get pageControl and scroll view from view's subviews
let pageControl = view.subviews.filter{ $0 is UIPageControl }.first! as! UIPageControl
let scrollView = view.subviews.filter{ $0 is UIScrollView }.first! as! UIScrollView
// remove all constraint from view that are tied to pagecontrol
let const = view.constraints.filter { $0.firstItem as? NSObject == pageControl || $0.secondItem as? NSObject == pageControl }
view.removeConstraints(const)
// customize pagecontroll
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.addConstraint(pageControl.heightAnchor.constraintEqualToConstant(35))
pageControl.backgroundColor = view.backgroundColor
// create constraints for pagecontrol
let leading = pageControl.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor)
let trailing = pageControl.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor)
let bottom = pageControl.bottomAnchor.constraintEqualToAnchor(scrollView.topAnchor, constant:8) // add to scrollview not view
// pagecontrol constraint to view
view.addConstraints([leading, trailing, bottom])
view.bounds.origin.y -= pageControl.bounds.maxY
}
The Shameerjan answer is very good, but it needs one more thing to work properly, and that is implementation of another delegate method:
func pageViewController(pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
// If user bailed our early from the gesture,
// we have to revert page control to previous position
if !completed {
let pageContentView = previousViewControllers[0] as! PageContentViewController;
self.pageControl.currentPage = pageContentView.pageIndex;
}
}
This is because if you don't, if you move the page control just so slightly, it will go back to previous position - but the page control will show different page.
Hope it helps!
Swift 4 version of #Jan's answer with fixed bug when the user cancels transition:
First, you create custom pageControl:
let pageControl = UIPageControl()
You add it to the view and position it as you want:
self.view.addSubview(self.pageControl)
self.pageControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
self.pageControl.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor, constant: 43),
self.pageControl.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -33)
])
Then you need to initialize the pageControl:
self.pageControl.numberOfPages = self.dataSource.controllers.count
Finally, you need to implement UIPageViewControllerDelegate, and its method pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:):
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
// this will get you currently presented view controller
guard let selectedVC = pageViewController.viewControllers?.first else { return }
// and its index in the dataSource's controllers (I'm using force unwrap, since in my case pageViewController contains only view controllers from my dataSource)
let selectedIndex = self.dataSource.controllers.index(of: selectedVC)!
// and we update the current page in pageControl
self.pageControl.currentPage = selectedIndex
}
Now in comparison with #Jan's answer, we update self.pageControl.currentPage using pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) (shown above), instead of pageViewController(_:willTransitionTo:). This overcomes the problem of cancelled transition - pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:) is called always when a transition was completed (it also better mimics the behavior of standard page control).
Finally, to remove the standard page control, be sure to remove implementation of presentationCount(for:) and presentationIndex(for:) methods of the UIPageViewControllerDataSource - if the methods are implemented, the standard page control will be presented.
So, you do NOT want to have this in your code:
func presentationCount(for pageViewController: UIPageViewController) -> Int {
return self.dataSource.controllers.count
}
func presentationIndex(for pageViewController: UIPageViewController) -> Int {
return 0
}
For swift:
self.pageController = UIPageControl(
frame: CGRect(
x: 0,
y: self.view.frame.size.height - 50,
width: self.view.frame.size.width,
height: 50
)
)
self.view.addSubview(pageController)
remember use pageController.numberOfPages and delegate the pageView
then remove
func presentationCountForPageViewController(
pageViewController: UIPageViewController
) -> Int
and
func presentationIndexForPageViewController(
pageViewController: UIPageViewController
) -> Int
Then add another delegate method:
func pageViewController(
pageViewController: UIPageViewController,
willTransitionToViewControllers pendingViewControllers:[UIViewController]){
if let itemController = pendingViewControllers[0] as? PageContentViewController {
self.pageController.currentPage = itemController.pageIndex
}
}
}
Yes, the Shameerjan answer is very good, but instead of adding another page control you can use default page indicator:
- (UIPageControl*)pageControl {
for (UIView* view in self.view.subviews) {
if ([view isKindOfClass:[UIPageControl class]]) {
//set new pageControl position
view.frame = CGRectMake( 100, 200, width, height);
return (id)view;
}
}
return nil;
}
and then extend the size of the UIPageViewController to cover up the bottom gap:
//somewhere in your code
self.view.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height+40);
here s a very effective way to change the position of the default PageControl for the PageViewController without having the need to create a new one ...
extension UIPageViewController {
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for subV in self.view.subviews {
if type(of: subV).description() == "UIPageControl" {
let pos = CGPoint(x: subV.frame.origin.x, y: subV.frame.origin.y - 75 * 2)
subV.frame = CGRect(origin: pos, size: subV.frame.size)
}
}
}
}
Here's my take. This version only changes de indicator when the animation is finished, i.e. when you get to the destination page.
/** Each page you add to the UIPageViewController holds its index */
class Page: UIViewController {
var pageIndex = 0
}
class MyPageController : UIPageViewController {
private let pc = UIPageControl()
private var pendingPageIndex = 0
// ... add pc to your view, and add Pages ...
/** Will transition, so keep the page where it goes */
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController])
{
let controller = pendingViewControllers[0] as! Page
pendingPageIndex = controller.pageIndex
}
/** Did finish the transition, so set the page where was going */
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool)
{
if (completed) {
pc.currentPage = pendingPageIndex
}
}
}
Swift 5 & iOS 13.5 (with Manual Layout)
The example below uses a custom UIPageControl, laid out at the bottom center position of the UIPageViewController. To use the code, replace MemberProfilePhotoViewController with the type of view controller you are using as pages.
Quick Notes
You must set the numberOfPages property on pageControl.
To size pageControl you can use pageControl.size(forNumberOfPages: count).
Make sure you delete presentationCount(...) and presentationIndex(...) to remove the default page control.
Using UIPageViewControllerDelegate's didFinishAnimating seems to be better timing for updating pageControl.currentPage.
The Code
import Foundation
import UIKit
class MemberProfilePhotosViewController: UIPageViewController {
private let profilePhotoURLs: [URL]
private let profilePhotoViewControllers: [MemberProfilePhotoViewController]
private var pageControl: UIPageControl?
// MARK: - Initialization
init(profilePhotoURLs: [URL]) {
self.profilePhotoURLs = profilePhotoURLs
profilePhotoViewControllers = profilePhotoURLs.map { (profilePhotoURL) -> MemberProfilePhotoViewController in
MemberProfilePhotoViewController(profilePhotoURL: profilePhotoURL)
}
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
// MARK: UIViewController
override func loadView() {
super.loadView()
pageControl = UIPageControl(frame: CGRect.zero)
pageControl!.numberOfPages = profilePhotoViewControllers.count
self.view.addSubview(pageControl!)
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
if let firstViewController = profilePhotoViewControllers.first {
setViewControllers([firstViewController],
direction: .forward,
animated: true,
completion: nil)
}
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let pageControl = pageControl {
let pageControlSize = pageControl.size(forNumberOfPages: profilePhotoViewControllers.count)
pageControl.frame = CGRect(
origin: CGPoint(x: view.frame.midX - pageControlSize.width / 2, y: view.frame.maxY - pageControlSize.height),
size: pageControlSize
)
}
}
// MARK: Private Helpers
private func indexOf(_ viewController: UIViewController) -> Int? {
return profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController)
}
}
extension MemberProfilePhotosViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
guard let selectedViewController = pageViewController.viewControllers?.first else { return }
if let indexOfSelectViewController = indexOf(selectedViewController) {
pageControl?.currentPage = indexOfSelectViewController
}
}
}
extension MemberProfilePhotosViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
return nil
}
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0 else {
return nil
}
guard profilePhotoViewControllers.count > previousIndex else {
return nil
}
return profilePhotoViewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let viewControllerIndex = profilePhotoViewControllers.firstIndex(of: viewController as! MemberProfilePhotoViewController) else {
return nil
}
let nextIndex = viewControllerIndex + 1
let profilePhotoViewControllersCount = profilePhotoViewControllers.count
guard profilePhotoViewControllersCount != nextIndex else {
return nil
}
guard profilePhotoViewControllersCount > nextIndex else {
return nil
}
return profilePhotoViewControllers[nextIndex]
}
}
This is good, and with more change
var pendingIndex = 0;
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
if completed {
pageControl.currentPage = pendingIndex
}
}
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
let itemController = pendingViewControllers.first as! IntroPageItemViewController
pendingIndex = itemController.itemIndex
}
Make sure pageControl is added as subview.
Then in
-(void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGRect frame = self.pageControl.frame;
frame.origin.x = self.view.frame.size.width/2 -
frame.size.width/2;
frame.origin.y = self.view.frame.size.height - 100 ;
self.pageControl.numberOfPages = self.count;
self.pageControl.currentPage = self.currentIndex;
self.pageControl.frame = frame;
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
for view in self.view.subviews{
if view is UIScrollView{
view.frame = UIScreen.main.bounds
}
else if view is UIPageControl {
view.backgroundColor = UIColor.clear
view.frame.origin.y = self.view.frame.size.height - 75
}
}
}
Just get the first view controller and find out the index in didFinishAnimating:
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
pageControl.currentPage = onboardingViewControllers.index(of: pageViewController.viewControllers!.first!)!
}
I'm following this tutorial: http://swiftiostutorials.com/ios-tutorial-using-uipageviewcontroller-create-content-slider-objective-cswift/ to create an app that shows multiple sliders.
Even though i've got this tutorial to work, This example only changes an image based on those that are stored in an array.
How can I get it to load ViewControllers instead of images
I have 4 ViewControllers:
ViewController1
ViewController2
ViewController3
ViewController4
I would like slide one to show ViewController1 and slide2 to load ViewController2 etc....
Here is my main ViewController:
import UIKit
class ViewController: UIViewController, UIPageViewControllerDataSource {
// MARK: - Variables
private var pageViewController: UIPageViewController?
// Initialize it right away here
private let contentImages = ["nature_pic_1.png",
"nature_pic_2.png",
"nature_pic_3.png",
"nature_pic_4.png"];
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
createPageViewController()
setupPageControl()
}
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("PageController") as UIPageViewController
pageController.dataSource = self
if contentImages.count > 0 {
let firstController = getItemController(0)!
let startingViewControllers: NSArray = [firstController]
pageController.setViewControllers(startingViewControllers, direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
private func setupPageControl() {
let appearance = UIPageControl.appearance()
appearance.pageIndicatorTintColor = UIColor.grayColor()
appearance.currentPageIndicatorTintColor = UIColor.whiteColor()
appearance.backgroundColor = UIColor.darkGrayColor()
}
// MARK: - UIPageViewControllerDataSource
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
let itemController = viewController as PageItemController
if itemController.itemIndex > 0 {
return getItemController(itemController.itemIndex-1)
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
let itemController = viewController as PageItemController
if itemController.itemIndex+1 < contentImages.count {
return getItemController(itemController.itemIndex+1)
}
return nil
}
private func getItemController(itemIndex: Int) -> PageItemController? {
if itemIndex < contentImages.count {
let pageItemController = self.storyboard!.instantiateViewControllerWithIdentifier("ItemController") as PageItemController
pageItemController.itemIndex = itemIndex
pageItemController.imageName = contentImages[itemIndex]
return pageItemController
}
return nil
}
// MARK: - Page Indicator
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return contentImages.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
and here is my PageItemController:
import UIKit
class PageItemController: UIViewController {
// MARK: - Variables
var itemIndex: Int = 0
var imageName: String = "" {
didSet {
if let imageView = contentImageView {
imageView.image = UIImage(named: imageName)
}
}
}
#IBOutlet var contentImageView: UIImageView?
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
contentImageView!.image = UIImage(named: imageName)
}
}
I'm new to Swift/iOS Development and really trying to get into it by developing. Thank you in advance for your answers :)
EDIT: To Make Question Clear
How do I make it so that there is an array of view controllers that correspond to the slide left/right of the UIPageViewController?
So when I swipe left on ViewController1 - the UIViewController2 is loaded and reverse for swipe right.
Assuming you have view controllers 1-4 defined in the same storyboard as your UIPageViewController, and you have their Storyboard IDs set as ViewController0, ViewController1, and et cetera, then create a method to populate your view controller array and call it in your viewDidLoad() before calling createPageViewController()
override func viewDidLoad() {
super.viewDidLoad()
populateControllersArray()
createPageViewController()
setupPageControl()
}
Implement the method like so:
var controllers = [PageItemController]()
func populateControllersArray() {
for i in 0...3 {
let controller = storyboard!.instantiateViewControllerWithIdentifier("ViewController\(i)") as PageItemController
controller.itemIndex = i
controllers.append(controller)
}
}
And define your createPageViewController() as the following
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("PageController") as UIPageViewController
pageController.dataSource = self
if !controllers.isEmpty {
pageController.setViewControllers([controllers[0]], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
then in your two delegate before and after methods:
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let controller = viewController as? PageItemController {
if controller.itemIndex > 0 {
return controllers[controller.itemIndex - 1]
}
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if let controller = viewController as? PageItemController {
if controller.itemIndex < controllers.count - 1 {
return controllers[controller.itemIndex + 1]
}
}
return nil
}
And in the count method
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return controllers.count
}
In fact, you can populate controllers with any view controllers you want to display, just set their class as PageItemController in storyboard (in order to have index property).
Or you can set each view controller as it's own class, and use runtime property getting and setting.
Use controller.valueForKey("itemIndex") as Int in the before and after method instead of controller.itemIndex
Use controller.setValue(i, forKey: "itemIndex") instead of controller.itemIndex = i in populateControllersArray().
Just ensure that each controller class has the Int property itemIndex, or your application will crash.
To bring it all together in your code, do the following:
import UIKit
import UIKit
class ViewController: UIViewController, UIPageViewControllerDataSource {
// MARK: - Variables
private var pageViewController: UIPageViewController?
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
populateControllersArray()
createPageViewController()
setupPageControl()
}
var controllers = [PageItemController]()
func populateControllersArray() {
for i in 0...3 {
let controller = storyboard!.instantiateViewControllerWithIdentifier("ViewController\(i)") as PageItemController
controller.itemIndex = i
controllers.append(controller)
}
}
private func createPageViewController() {
let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("PageController") as UIPageViewController
pageController.dataSource = self
if !controllers.isEmpty {
pageController.setViewControllers([controllers[0]], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
}
pageViewController = pageController
addChildViewController(pageViewController!)
self.view.addSubview(pageViewController!.view)
pageViewController!.didMoveToParentViewController(self)
}
private func setupPageControl() {
let appearance = UIPageControl.appearance()
appearance.pageIndicatorTintColor = UIColor.grayColor()
appearance.currentPageIndicatorTintColor = UIColor.whiteColor()
appearance.backgroundColor = UIColor.darkGrayColor()
}
// MARK: - UIPageViewControllerDataSource
func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
if let controller = viewController as? PageItemController {
if controller.itemIndex > 0 {
return controllers[controller.itemIndex - 1]
}
}
return nil
}
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
if let controller = viewController as? PageItemController {
if controller.itemIndex < controllers.count - 1 {
return controllers[controller.itemIndex + 1]
}
}
return nil
}
// MARK: - Page Indicator
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int {
return controllers.count
}
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int {
return 0
}
You do load ViewControllers for every page. every image that you show is inside it's own ViewController. That is done through:
private func getItemController(itemIndex: Int) -> PageItemController?
if every page of yours uses the same layout, there is nothing left to do here, except designing this ViewController in the Interface Builder.
If, however, every page uses a different layout and shows different data, you would first prototype/design those ViewControllers in Interface Builder.
then you would create classes for every ViewController and extend them from PageItemController. You only keep the index variable in PageItemController and move the rest of your logic to the subclasses.
import UIKit
class PageItemController: UIViewController {
// MARK: - Variables
var itemIndex: Int = 0
}
for example a viewController that holds an image
import UIKit
class PageImageViewController: PageItemController {
// MARK: - Outlets
#IBOutlet var contentImageView: UIImageView?
// MARK: - Variables
var imageName: String = "" {
didSet {
if let imageView = contentImageView {
imageView.image = UIImage(named: imageName)
}
}
}
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
contentImageView!.image = UIImage(named: imageName)
}
}
finally you just change your getItemController function to return the correct ViewController for the specified index. here you either pass data to the ViewController or you just return it.
private func getItemController(itemIndex: Int) -> UIViewController? {
var vc: PageItemController? = nil
switch itemIndex {
case 0:
// show an ImageViewController and pass data if needed
vc = self.storyboard!.instantiateViewControllerWithIdentifier("ImageController") as PageImageViewController
vc.itemIndex = itemIndex
vc.imageName = "any_image_file"
case 1:
// show any other ViewController and pass data if needed
vc = self.storyboard!.instantiateViewControllerWithIdentifier("ANY_OTHERController") as ANY_OTHERController
vc.PRESENTABLE_DATA = ANY_PRESENTABLE_DATA_SOURCE
vc.itemIndex = itemIndex
case 2:
// show any other ViewController and pass data if needed
vc = self.storyboard!.instantiateViewControllerWithIdentifier("ANY_OTHERController") as ANY_OTHERController
vc.PRESENTABLE_DATA = ANY_PRESENTABLE_DATA_SOURCE
vc.itemIndex = itemIndex
}
return vc
}
Here is a great repo for this:
https://github.com/goktugyil/EZSwipeController
private func setupPageViewController() {
pageViewController = UIPageViewController(transitionStyle: UIPageViewControllerTransitionStyle.Scroll, navigationOrientation: UIPageViewControllerNavigationOrientation.Horizontal, options: nil)
pageViewController.dataSource = self
pageViewController.delegate = self
pageViewController.setViewControllers([stackPageVC[stackStartLocation]], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: nil)
pageViewController.view.frame = CGRect(x: 0, y: Constants.StatusBarHeight, width: Constants.ScreenWidth, height: Constants.ScreenHeightWithoutStatusBar)
pageViewController.view.backgroundColor = UIColor.clearColor()
addChildViewController(pageViewController)
view.addSubview(pageViewController.view)
pageViewController.didMoveToParentViewController(self)
}
I got Eric Ferreira's code above to work (Xcode 6.4). I wanted to use a Page View Controller to display two completely unique view controllers with labels that displays different data from a Realm database.
I got it to work in a test project where I used 2 storyboards, each containing a single label that is set by its own class containing the IBOutlet to the label and code setting the label text -- this adequately simulates the way I am displaying my Realm database data. Each storyboard class inherits the PageItemController so as to have the "itemIndex" variable available to it. The PageItemController in turns inherits UIViewController completing the inheritance chain.
I hope this helps someone that is seeking to use completely unique storyboards.
I am using a UIPageViewController, and I need to get the scroll position of the ViewController as the users swipe so I can partially fade some assets while the view is transitioning to the next UIViewController.
The delegate and datasource methods of UIPageViewController don't seem to provide any access to this, and internally I'm assuming that the UIPageViewController must be using a scroll view somewhere, but it doesn't seem to directly subclass it so I'm not able to call
func scrollViewDidScroll(scrollView: UIScrollView) {
}
I've seen some other posts suggestion to grab a reference to the pageViewController!.view.subviews and then the first index is a scrollView, but this seems very hacky. I'm wondering if there is a more standard way to handle this.
You can search for the UIScrollView inside your UIPageViewController. To do that, you will have to implement the UIScrollViewDelegate.
After that you can get your scrollView:
for v in pageViewController.view.subviews{
if v.isKindOfClass(UIScrollView){
(v as UIScrollView).delegate = self
}
}
After that, you are able to use all the UIScrollViewDelegate-methods and so you can override the scrollViewDidScroll method where you can get the scrollPosition:
func scrollViewDidScroll(scrollView: UIScrollView) {
//your Code
}
Or if you want a one-liner:
let scrollView = view.subviews.filter { $0 is UIScrollView }.first as! UIScrollView
scrollView.delegate = self
UIPageViewController scroll doesn't work like normal scrollview and you can't get scrollView.contentOffset like other scrollViews.
so here is a trick to get what's going on when user scrolls :
first you have to find scrollview and set delegate to current viewController like other answers said.
class YourViewController : UIPageViewController {
var startOffset = CGFloat(0) //define this
override func viewDidLoad() {
super.viewDidLoad()
//from other answers
for v in view.subviews{
if v is UIScrollView {
(v as! UIScrollView).delegate = self
}
}
}
.
.
.
}
extension YourViewController : UIScrollViewDelegate{
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
startOffset = scrollView.contentOffset.x
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
var direction = 0 //scroll stopped
if startOffset < scrollView.contentOffset.x {
direction = 1 //going right
}else if startOffset > scrollView.contentOffset.x {
direction = -1 //going left
}
let positionFromStartOfCurrentPage = abs(startOffset - scrollView.contentOffset.x)
let percent = positionFromStartOfCurrentPage / self.view.frame.width
//you can decide what to do with scroll
}
}
Similar to Christian's answer but a bit more Swift-like (and not unnecessarily continuing to loop through view.subviews):
for view in self.view.subviews {
if let view = view as? UIScrollView {
view.delegate = self
break
}
}
As of iOS 13, the UIPageViewController seems to reset the scrollview's contentOffset once it transitions to another view controller. Here is a working solution:
Find the child scrollView and set its delegate to self, as other answers suggested
Keep track of the current page index of the pageViewController:
var currentPageIndex = 0
// The pageViewController's viewControllers
let orderredViewControllers: [UIViewController] = [controller1, controller2, ...]
pageViewController.delegate = self
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
guard completed, let currentViewController = pageViewController.viewControllers?.first else { return }
currentPageIndex = orderredViewControllers.firstIndex(of: currentViewController)!
}
Get the progress that ranges from 0 to 1
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffsetX = scrollView.contentOffset.x
let width = scrollView.frame.size.width
let offset = CGFloat(currentPageIndex) / CGFloat(orderredViewControllers.count - 1)
let progress = (contentOffsetX - width) / width + offset
}
var pageViewController: PageViewController? {
didSet {
pageViewController?.dataSource = self
pageViewController?.delegate = self
scrollView?.delegate = self
}
}
lazy var scrollView: UIScrollView? = {
for subview in pageViewController?.view?.subviews ?? [] {
if let scrollView = subview as? UIScrollView {
return scrollView
}
}
return nil
}()
extension BaseFeedViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.x
let bounds = scrollView.bounds.width
let page = CGFloat(self.currentPage)
let count = CGFloat(viewControllers.count)
let percentage = (offset - bounds + page * bounds) / (count * bounds - bounds)
print(abs(percentage))
}
}
To make the code as readable and separated as possible, I would define an extension on UIPageViewController:
extension UIPageViewController {
var scrollView: UIScrollView? {
view.subviews.first(where: { $0 is UIScrollView }) as? UIScrollView
}
}
It's quite easy to set yourself as the delegate for scroll view events, as so:
pageViewController.scrollView?.delegate = self