45

I'm in the process of making a tutorial, and I'm trying to emulate the style of Path's tutorial like so:

http://www.appcoda.com/wp-content/uploads/2013/06/UIPageViewController-Tutorial-Screen.jpg enter image description here

My issue is that if set the delegate method as so:

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController {
    // The number of items reflected in the page indicator.
    return 5;
}

Then I get this stupid black bar under the dots:

https://i.stack.imgur.com/pUEdh.png enter image description here

Is there a way to make this bar translucent in a way thats similar to setting a UINavigationBar to translucent?

Brian
  • 14,610
  • 7
  • 35
  • 43
mverderese
  • 5,314
  • 6
  • 27
  • 36

12 Answers12

47

It is very easy to make it work. You just only have to make the pageviewcontroller taller, and place a PageControl into the XIB file. The trick is put the PageControl in the foreground (and all the other common controls) at the beginning, and update the content of the PageControl with the PageViewController. Here is the code:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.

    self.pageController = [[UIPageViewController alloc] initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal options:nil];

    self.pageController.dataSource = self;
    // We need to cover all the control by making the frame taller (+ 37)
    [[self.pageController view] setFrame:CGRectMake(0, 0, [[self view] bounds].size.width, [[self view] bounds].size.height + 37)];

    TutorialPageViewController *initialViewController = [self viewControllerAtIndex:0];

    NSArray *viewControllers = [NSArray arrayWithObject:initialViewController];

    [self.pageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];

    [self addChildViewController:self.pageController];
    [[self view] addSubview:[self.pageController view]];
    [self.pageController didMoveToParentViewController:self];

    // Bring the common controls to the foreground (they were hidden since the frame is taller)
    [self.view bringSubviewToFront:self.pcDots];
    [self.view bringSubviewToFront:self.btnSkip];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {

    NSUInteger index = [(TutorialPageViewController *)viewController index];

    [self.pcDots setCurrentPage:index];

    if (index == 0) {
        return nil;
    }

    index--;

    return [self viewControllerAtIndex:index];
}

- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {

    NSUInteger index = [(TutorialPageViewController *)viewController index];

    [self.pcDots setCurrentPage:index];

    index++;

    if (index == 3) {
        return nil;
    }

    return [self viewControllerAtIndex:index];
}

- (TutorialPageViewController *)viewControllerAtIndex:(NSUInteger)index {

    TutorialPageViewController *childViewController = [[TutorialPageViewController alloc] initWithNibName:@"TutorialPageViewController" bundle:nil];
    childViewController.index = index;

    return childViewController;
}

- (NSInteger)presentationCountForPageViewController:(UIPageViewController *)pageViewController {
    // The number of items reflected in the page indicator.
    NSInteger tutorialSteps = 3;
    [self.pcDots setNumberOfPages:tutorialSteps];

    return tutorialSteps;
}

- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController {
    // The selected item reflected in the page indicator.
    return 0;
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Alex R. R.
  • 2,992
  • 1
  • 18
  • 13
  • 1
    This doesn't work very well. Just go to either your first or last page, scroll in the direction you can't go, and watch the index get thrown off. – AdamPro13 Aug 12 '14 at 02:24
  • 3
    @AdamPro13 I agree, this didn't work for me as well. I got it working by implementing the `UIPageViewControllerDelegate` and setting the current page inside of `pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:`. Not sure why but I found I had to set the delegate before I set the dataSource on my UIPageViewController. – Michael M. Myers Sep 01 '14 at 19:18
  • @MichaelMyers Can you post your working code? Stuck on trying to get the delegate working with this tutorial. – CorreyS Sep 23 '14 at 04:19
  • 3
    Works well. Instead of changing the frame to hide the default pageControl, u just have to NOT implement `presentationCountForPageViewController` and `presentationIndexForPageViewController`. Just set `setNumberOfPages` in `viewDidLoad`. – allaire Jun 26 '15 at 02:46
  • Thank you for the tips! I also came across this project over github https://github.com/larryaasen/LAWalkthrough. You can also learn from it too (similar approach). – haxpor Sep 03 '15 at 10:12
  • 1
    How are you getting access to pcDots?? I am trying to use the default PageControl that comes with PageViewController and I can't seem to get access to the dots to bring the subview forward – Sandy D. Oct 19 '16 at 17:37
  • @SandyD. have you seend the -37 in frameheight.. thing is the dots of pageviewcontroller is still there.. the pcDots is PageControl another dots that you wired up to your class .. you will update its index whenever swipe triggered. like in the answer – CaffeineShots Nov 19 '16 at 14:08
21

The same effect can be achieved simply by subclassing UIPageViewController and overriding viewDidLayoutSubviews as follows:

-(void)viewDidLayoutSubviews {
    UIView* v = self.view;
    NSArray* subviews = v.subviews;
    // Confirm that the view has the exact expected structure.
    // If you add any custom subviews, you will want to remove this check.
    if( [subviews count] == 2 ) {
        UIScrollView* sv = nil;
        UIPageControl* pc = nil;
        for( UIView* t in subviews ) {
            if( [t isKindOfClass:[UIScrollView class]] ) {
                sv = (UIScrollView*)t;
            } else if( [t isKindOfClass:[UIPageControl class]] ) {
                pc = (UIPageControl*)t;
            }
        }
        if( sv != nil && pc != nil ) {
            // expand scroll view to fit entire view
            sv.frame = v.bounds;
            // put page control in front
            [v bringSubviewToFront:pc];
        }
    }
    [super viewDidLayoutSubviews];
}

Then there is no need to maintain a separate UIPageControl and such.

zeroimpl
  • 2,746
  • 22
  • 19
9

Swift 3 snippet

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let scrollView = view.subviews.filter({ $0 is UIScrollView }).first,
        let pageControl = view.subviews.filter({ $0 is UIPageControl }).first {
        scrollView.frame = view.bounds
        view.bringSubview(toFront:pageControl)
    }
}
6

Here's a conversion of Zerotool's solution into Swift 2.1, though there's probably a more elegant way to write it:

override func viewDidLayoutSubviews() {
    var scrollView: UIScrollView?
    var pageControl: UIPageControl?

    // If you add any custom subviews, you will want to remove this check.
    if (self.view.subviews.count == 2) {
        for view in self.view.subviews {
            if (view.isKindOfClass(UIScrollView)) {
                scrollView = view as? UIScrollView
            } else if (view.isKindOfClass(UIPageControl)) {
                pageControl = view as? UIPageControl
            }
        }
    }

    if let scrollView = scrollView {
        if let pageControl = pageControl {
            scrollView.frame = self.view.bounds
            self.view.bringSubviewToFront(pageControl)
        }
    }

    super.viewDidLayoutSubviews()
}
markedwardmurray
  • 523
  • 4
  • 10
4

I don't think you can change the behavior of UIPageViewController, so it seems likely that the Path app uses its own view controller. You can do the same: create your own container view controller that uses a UIPageControl to indicate the current page.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • @MikeV It never hurts to file a bug report with Apple if you want functionality that the framework lacks. – Caleb Jul 22 '13 at 05:56
  • Good point I'll probably do that. In the meantime looks like I'm rewriting my code to account for this – mverderese Jul 22 '13 at 05:59
  • I'm surprised. I'm using a UiPageControl on my basic utility app and it was transparent by default. – Rob van der Veer Jul 22 '13 at 06:18
  • @RobvanderVeer Problem isn't really the transparency of the control, it's the size of the views managed by UIPageViewController and the placement of the UIPageControl on the black background. – Caleb Jul 22 '13 at 06:21
3

You can simply adjust the alpha of the UIPageViewController's UIPageControl.

First, you should retrieve it from the UIPageViewController like so:

- (UIPageControl *)getPageControlForPageViewController:(UIPageViewController *)pageViewController {
      for (UIView *subview in self.pageViewController.view.subviews) {
        if ([subview isKindOfClass:[UIPageControl class]]) {
          return (UIPageControl *) subview;
        }
      }

      return nil;
    }

Next, make use of the function. I've made a property on my ViewController called childPageControl. Give it the UIPageViewController's UIPageControl:

self.childPageControl = [self getPageControlForPageViewController:self.pageViewController];

Next, you can adjust the alpha to give a translucent effect:

self.childPageControl.alpha = .5;

You're very limited in what you can do to affect the UIPageViewController's UIPageControl, but you can at least achieve this with little effort.

Carter Hudson
  • 1,176
  • 1
  • 11
  • 23
2

Small hack I found today..

Please see the code below.

enter image description here

self.pageController.dataSource = self;
    CGRect rect = [self.view bounds];
    rect.size.height+=37;
    [[self.pageController view] setFrame:rect];
        NSArray *subviews = self.pageController.view.subviews;
        UIPageControl *thisControl = nil;
        for (int i=0; i<[subviews count]; i++) {
            if ([[subviews objectAtIndex:i] isKindOfClass:[UIPageControl class]]) {
                thisControl = (UIPageControl *)[subviews objectAtIndex:i];
            }
        }

        UIView *tempview = [[UIView alloc] initWithFrame:CGRectMake(0, -30, 320, 40)];
        [tempview addSubview:thisControl];
        thisControl.pageIndicatorTintColor = [UIColor lightGrayColor];
        thisControl.currentPageIndicatorTintColor = [UIColor greenColor];
        [self.view addSubview:tempview];
mandeep-dhiman
  • 2,505
  • 2
  • 21
  • 31
2

this code is in Swift Add following in your UIPageViewController

override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

    for view in self.view.subviews {
        if view.isKindOfClass(UIScrollView) {
            view.frame = UIScreen.mainScreen().bounds
        } else if view.isKindOfClass(UIPageControl) {
            view.backgroundColor = UIColor.clearColor()
        }
    }
}
Gaurav Dave
  • 6,838
  • 9
  • 25
  • 39
Mohsin Qureshi
  • 1,203
  • 2
  • 16
  • 26
1

I wanted to do a similar effect in the app I was working on - I used a UIPageViewController with a separate UIPageControl.

This lets you place the UIPageControl anywhere you'd like in the view, including over the top of the UIPageViewController, and you keep its active page dot up to date via the UIPageViewController delegate method:

- (void)pageViewController:(UIPageViewController *)pageViewController
        didFinishAnimating:(BOOL)finished
   previousViewControllers:(NSArray<UIViewController *> *)previousViewControllers
       transitionCompleted:(BOOL)completed {
    if (completed) {
        self.pageControl.currentPage = [self.pageViewControllers indexOfObject:pageViewController.viewControllers.firstObject];
    }
}

No need to traverse the subview hierarchy trying to find the internal UIPageViewController page control, nor having to resize the contents of the internal scrollview.

Hope this helps.

crafterm
  • 1,831
  • 19
  • 17
1

I solve using this code:

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.


self.namesImage = @[@"page1.png", @"page2.png", @"page3.png", @"page4.png"];

self.pageViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"PageViewController"];
self.pageViewController.dataSource = self;


TutorialContentViewController *startingViewController = [self viewControllerAtIndex:0];
NSArray *viewControllers = @[startingViewController];

[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];


[self addChildViewController:self.pageViewController];
[self.view addSubview:self.pageViewController.view];
[self.pageViewController didMoveToParentViewController:self];

[[UIPageControl appearance] setPageIndicatorTintColor:[UIColor grayColor]];
[[UIPageControl appearance] setCurrentPageIndicatorTintColor:[UIColor whiteColor]];
[[UIPageControl appearance] setBackgroundColor: [[UIColor blackColor] colorWithAlphaComponent:0.1f]];
[[UIPageControl appearance] setOpaque:YES];

}
MMSousa
  • 411
  • 3
  • 12
1

Swift 5.2

you can use this code for your requirment

 override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()

    if let myScrollView = view.subviews.filter({ $0 is UIScrollView }).first,
        let myPageControl = view.subviews.filter({ $0 is UIPageControl }).first {
        myScrollView.frame = view.bounds
        view.bringSubviewToFront(myPageControl)
    }
}
Hasnain ahmad
  • 301
  • 1
  • 10
  • 33
0

I found an other workarround that fits me better.

I reuse the code given by zerotool to get the UIPageControl (var called pageControl) and the UIScrollView (var called pageView) used by the UIPageViewController.

Once that done in the viewDidLoad, I just prevent clip subview of pageView and let the content spread more to be beneath the UIPageControl.

The pageControl is beneath the pageView so we have to manually make it come in front.

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    if(
       [[[self view] subviews] count] == 2
       )
    {
        UIScrollView* pageView = nil;
        UIPageControl* pageControl = nil;

        UIView* selfView = self.view;
        NSArray* subviews = selfView.subviews;

        for( NSInteger i = 0 ; i < subviews.count && ( pageView == nil || pageControl == nil ) ; i++ )
        {
            UIView* t = subviews[i];
            if( [t isKindOfClass:[UIScrollView class]] )
            {
                pageView = (UIScrollView*)t;
            }
            else if( [t isKindOfClass:[UIPageControl class]] )
            {
                pageControl = (UIPageControl*)t;
            }
        }

        if( pageView != nil && pageControl != nil )
        {
            [pageView setClipsToBounds:NO];
            [selfView bringSubviewToFront:pageControl];
        }
    }
}

Once I get my pageView covering the space occupied by the pageControl but under the pageControl, I just have to adjust the nib file use for each viewController displayed as page :

  • base view should not clip
  • the first and only subview :
    • should have constraint to set bottom to -37 (or more if you need but 37 is the size of the pageControl) from bottom of superview
    • should clip content
Community
  • 1
  • 1
MessuKilkain
  • 76
  • 1
  • 1
  • 11