24

I searched before posting this question, but couldn't find anything.

I'm having a huge issue. I have a scrolling type UIPageViewController as the basis of my app, with 3 view controllers.

One of the view controller's (listTableView) has a table view and a search display controller.

The problem is, I cannot scroll to the top of the table view when tapping on the status bar like a normal table view. I believe the UIPageViewController is interfering with this, but I have no idea how to go about fixing it, but I know that I need to for my app to not feel broken.

I appreciate any help offered.

I know someone will ask for code even though it's irrelvant in this case but here it is for creating the UIPageViewController:

#import "MainViewController.h"

@interface MainViewController ()

@property (nonatomic, strong) UIPageViewController *pageViewController;
@property (nonatomic, strong) NSArray *contentViewControllers;

@end

@implementation MainViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

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

    UIViewController *settings = [self.storyboard instantiateViewControllerWithIdentifier:@"Settings"];

    UIViewController *listTableView = [self.storyboard instantiateViewControllerWithIdentifier:@"List"];

    UIViewController *first = [self.storyboard instantiateViewControllerWithIdentifier:@"First"];

    self.contentViewControllers = [NSArray arrayWithObjects:settings,listTableView,first,nil];

    [self.pageViewController setViewControllers:@[first] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];

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

    self.view.backgroundColor = [UIColor colorWithRed:(248.0/255.0) green:(248.0/255.0) blue:(248.0/255.0) alpha:1.0];
}

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

    NSUInteger index = [self.contentViewControllers indexOfObject:viewController];

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

    return self.contentViewControllers[index - 1];
}

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

    NSUInteger index = [self.contentViewControllers indexOfObject:viewController];

    if (index >= self.contentViewControllers.count - 1) {
        return nil;
    }

    return self.contentViewControllers[index + 1];
}
klcjr89
  • 5,862
  • 10
  • 58
  • 91
  • Have you tried adding the to your MainViewController interface? – Nitin Alabur Jan 12 '14 at 00:05
  • Yes I have, but that doesn't work. The problem is you simply cannot get access to the _UIQueuingScrollView that the UIPageViewController unfortunately contains and ruins everything. – klcjr89 Jan 12 '14 at 00:08
  • I have followed your description of your app, and re-implemented it with your code. It behaves correctly, I cannot replicate your issue. The problem must lie elsewhere in the app - can you provide more information? Perhaps some of the code from your "list" viewController. – foundry Jan 12 '14 at 00:42
  • Could you show declaration of the MainViewController interface, please? (MainViewController.h) – dmirkitanov Jan 12 '14 at 00:49

5 Answers5

31

Solution

After chat, we discovered something interesting. The page view controller keeps other view controllers' views inside the view hierarchy, so they also capture the scrollsToTop behavior and interfere. This means that you need to disable scrollsToTop for each scrollable view inside viewWillDisappear: of the disappearing view controllers (and enable again on viewWillAppear:).


Original Investigation

The quick and easy way: The scrollview is the only subview of the UIPageViewController's view:

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

Inspecting in debugger:

(lldb) po [self.pageViewController.view recursiveDescription]
<_UIPageViewControllerContentView: 0x8d7c390; frame = (0 0; 320 480); clipsToBounds = YES; opaque = NO; autoresize = W+H; layer = <CALayer: 0x8d7c4a0>>
   | <_UIQueuingScrollView: 0xa912800; frame = (0 0; 320 480); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x8d7cc90>; layer = <CALayer: 0x8d7c7e0>; contentOffset: {320, 0}>
   |    | <UIView: 0x8d7da00; frame = (0 0; 320 480); layer = <CALayer: 0x8d7da60>>
   |    | <UIView: 0x8d7dab0; frame = (320 0; 320 480); layer = <CALayer: 0x8d7db10>>
   |    | <UIView: 0x8d7db40; frame = (640 0; 320 480); layer = <CALayer: 0x8d7dba0>>

You could use several methods to reach that scrollview. Easiest is to iterate self.pageViewController.view.subviews and find the one that is a subclass of UIScrollView. Since it is the only subview, your loop will end after one iteration.

Is this solution optimal? No. Is it error prone? In theory, sure. Is it likely to change? Not likely, as the view hierarchy is pretty logical. It at least gives you a quick fix, instead of having to deal with changing the entire app structure due to a small oversight by Apple (providing the user with access to the scrollview).

You should open a feature request with Apple over at https://bugreport.apple.com.

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • Hey Leo, I tried looping through the UIPageViewController's scrollviews, and I can indeed disable scrolling, but setting the scrolls to top to NO still renders my child controllers unable to scroll to the top. – klcjr89 Jan 12 '14 at 00:47
  • @troop231 Try running the method proposed here to see who is interfering with the scrolling: http://stackoverflow.com/questions/8527801/how-to-find-out-which-other-uiscrollview-interferes-with-scrollstotop – Léo Natan Jan 12 '14 at 00:52
  • I just ran that method and it listed out all the scroll views. How can I show you them? Also in addition: I need to eventually disable the panning of the x axis to the right on the pageViewController only on one of my content view controllers after I resolve this, should I just abandon UIPageViewController now? – klcjr89 Jan 12 '14 at 00:58
  • I found something quite interesting, I found a UICollectionView as a subview for some reason? And by disabling it, my table view now scrolls to the top now!! Yay! – klcjr89 Jan 12 '14 at 01:24
  • @troop231 Where is that collection view coming from? Is it from one of your view controller? – Léo Natan Jan 12 '14 at 01:25
  • it is indeed coming from the first content view controller. But, I'm still having issues, If I go to another view controller (Settings) then go back to the listTableView, the scrolls to top doesn't work anymore. Should I ditch this pageViewController? Like I said, I also need to disable panning later on programatically. – klcjr89 Jan 12 '14 at 01:33
  • @troop231 If it is causing more issues, then you should. I am not convinced the problem here is the page view controller, though. – Léo Natan Jan 12 '14 at 01:34
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/45036/discussion-between-leo-natan-and-troop231) – Léo Natan Jan 12 '14 at 01:36
  • Insane! but there is no more elegant solution ? – Coldsteel48 Jan 12 '14 at 03:00
  • 1
    @Roma-MT No, because Apple does not expose their scrollview. You should file a feature enhancement request so they add it in future versions of UIKit. – Léo Natan Jan 12 '14 at 03:04
  • Don't feel well doing it but if it is the only way so ok. thanks for the answer helped me as well. – Coldsteel48 Jan 12 '14 at 03:10
  • Folks, there's a much cleaner and safer solution - see ernesto che's answer below. – Jordan H Apr 19 '14 at 02:29
  • @LeoNatan: You should somehow emphasize the supplemental second part of your answer because it's the clean way to solve the problem. Maybe put it first or highlight it with a heading. That way it's easier to find the relevant information first. Great answer by the way! – Mischa Jul 27 '16 at 15:53
  • 1
    @Mischa Edited. See if it is better. Feel free to edit further to make it better. – Léo Natan Jul 27 '16 at 15:55
  • That was fast. Awesome! – Mischa Jul 27 '16 at 15:56
3

The iOS 7 docs on -scrollToTop say

On iPhone, the scroll-to-top gesture has no effect if there is more than one scroll view on-screen that has scrollsToTop set to YES.

I'm using a UIPageViewController in a similar fashion as you; one of my page controllers is a UITableViewController. So I get dinged by the two-scrollviews-onscreen-no-scrolltotop policy.

Kyle's idea to implement your own scrollToTop behavior via an invisible button is probably your most reliable bet in this situation.

Bill Garrison
  • 2,039
  • 15
  • 18
  • I actually ditched the page view controller and used a UIScrollView instead, which works better than the page view controller and provides more options. – klcjr89 Feb 19 '14 at 19:17
1

In my case I had a UITableView in the UIPageViewController's children view controllers. Pulling from Leo Natan's suggestions, adding this to my child view controller did the job:

- (void)viewWillAppear:(BOOL)animated 
{
    [super viewWillAppear:animated];
    self.tableView.scrollsToTop = YES; 
}

- (void)viewWillDisappear:(BOOL)animated 
{
    [super viewWillDisappear:animated];
    self.tableView.scrollsToTop = NO; 
}

As mentioned, the competing UIScrollView's in the children view controllers will interfere with each other's scrollsToTop delegate method. This code makes sure that only one of them has scrollsToTop enabled at a time.

Kyle Clegg
  • 38,547
  • 26
  • 130
  • 141
1

I used this:

for v in view.subviews {
    if v.isKindOfClass(UIScrollView) {
        (v as! UIScrollView).scrollsToTop = false
    }
}

But if there are still other scrollviews that can scroll to top, yours won't work. Use this script to find out your other scrollsviews: https://stackoverflow.com/a/33443757/2589276 , then disable their scroll to top.

Then in the viewcontroller where you want to scroll to top to work, add these:

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    tableView.scrollsToTop = true
}

override func viewWillDisappear(animated: Bool) {
    super.viewWillDisappear(animated)
    tableView.scrollsToTop = false
}
Community
  • 1
  • 1
Esqarrouth
  • 38,543
  • 21
  • 161
  • 168
0

I think the best solution would be using the following two methods of the UIPageViewControllerDelegate protocol (quick and dirty version, considering there is only one view controller on screen):

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
    ((UITableViewController *)pendingViewControllers[0].tableView).scrollsToTop = YES;
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
    ((UITableViewController *)previousViewControllers[0].tableView).scrollsToTop = NO;
}

Edit: better use the approach outlined in the last paragraph of the accepted answer

Community
  • 1
  • 1
ernesto
  • 1,771
  • 2
  • 18
  • 18
  • 1
    Kind of, yeah, with the disclaimer "Is this solution optimal? No. Is it error prone? In theory, sure." So I thought I'd share another approach. Nevermind – ernesto Feb 27 '14 at 19:06
  • This should be the accepted answer. It's the cleanest solution and not at all fragile or hackish. Note that you will need to split those lines into two - assign it to a table view controller before accessing its tableView because otherwise it's still id which doesn't have a tableView property. Also, you may wish to do some introspection before assigning it. Also note that this works for UICollectionViewController as well - change tableView to collectionView. Brilliant, thanks! – Jordan H Apr 19 '14 at 02:23
  • If you look in the chat in the other answer, you will see that we finally found out what it was and fixed it. This is a dangerous way, and does not cover all possible transitions from one view controller to another. Better to do it in the view controllers themselves, in `viewWillAppear` and `viewWillDisappear`. – Léo Natan Apr 19 '14 at 03:09
  • 1
    @LeoNatan is right, [his accepted approach](http://stackoverflow.com/a/21069767/270420) (setting it to YES on `viewWillAppear` and to NO on `viewWillDisappear`) is better. – ernesto Apr 22 '14 at 15:28