Removing a view controller from UIPageViewController

前端 未结 12 2457
梦谈多话
梦谈多话 2020-11-28 19:22

It\'s odd that there\'s no straightforward way to do this. Consider the following scenario:

  1. You have a page view controller with 1 page.
  2. Add another p
相关标签:
12条回答
  • 2020-11-28 19:44

    While the answers here are all informative, there is an alternate way of handling the problem, given here:

    UIPageViewController navigates to wrong page with Scroll transition style

    When I first searched for an answer to this problem, the way I was wording my search wound me up at this question, and not the one I've just linked to, so I felt obligated to post an answer linking to this other question, now that I've found it, and also elaborating a little bit.

    The problem is described pretty well by matt here:

    This is actually a bug in UIPageViewController. It occurs only with the scroll style (UIPageViewControllerTransitionStyleScroll) and only after calling setViewControllers:direction:animated:completion: with animated:YES. Thus there are two workarounds:

    Don't use UIPageViewControllerTransitionStyleScroll.

    Or, if you call setViewControllers:direction:animated:completion:, use only animated:NO.

    To see the bug clearly, call setViewControllers:direction:animated:completion: and then, in the interface (as user), navigate left (back) to the preceding page manually. You will navigate back to the wrong page: not the preceding page at all, but the page you were on when setViewControllers:direction:animated:completion: was called.

    The reason for the bug appears to be that, when using the scroll style, UIPageViewController does some sort of internal caching. Thus, after the call to setViewControllers:direction:animated:completion:, it fails to clear its internal cache. It thinks it knows what the preceding page is. Thus, when the user navigates leftward to the preceding page, UIPageViewController fails to call the dataSource method pageViewController:viewControllerBeforeViewController:, or calls it with the wrong current view controller.

    This is a good description, not quite the problem noted in this question but very close. Note the line about if you do setViewControllers with animated:NO you will force the UIPageViewController to re-query its data source next time the user pans with a gesture, as it no longer "knows where it is" or what view controllers are next to its current view controller.

    However, this didn't work for me because there were times when I need to programmatically move the PageView around with an animation.

    So, my first thought was to call setViewControllers with an animation, and then in the completion block call the method again with whatever view controller was now showing, but with no animation. So the user can pan, fine, but then we call the method again to get the page view to reset.

    Unfortunately when I tried that I started getting strange "assertion errors" from the page view controller. They look something like this:

    *** Assertion failure in -[UIPageViewController queuingScrollView: ...

    Not knowing exactly why this was happening, I backtracked and eventually started using Jai's answer as a solution, creating an entirely new UIPageViewController, pushing it onto a UINavigationController, then popping out the old one. Gross, but it works--mostly. I have been finding I'm still getting occasional Assertion Failures from the UIPageViewController, like this one:

    *** Assertion failure in -[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:], /SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $1 = 154507824 No view controller managing visible view >

    And the app crashes. Why? Well, searching, I found this other question that I mentioned up top, and particularly the accepted answer which advocates my original idea, of simply calling setViewControllers: animated:YES and then as soon as it completes calling setViewControllers: animated:NO with the same view controllers to reset the UIPageViewController, but it had the missing element: calling that code back on the main queue! Here's the code:

    __weak YourSelfClass *blocksafeSelf = self;     
    [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:^(BOOL finished){
                if(finished)
                {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [blocksafeSelf.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:NULL];// bug fix for uipageview controller
                    });
                }
            }];
    

    Wow! The only reason this actually made sense to me is because I have watched the the WWDC 2012 Session 211, Building Concurrent User Interfaces on iOS (available here with a dev account). I recall now that attempting to modify data source objects that UIKit objects (like UIPageViewController) depend on, and doing it on a secondary queue, can cause some nasty crashes.

    What I have never seen particularly documented, but must now assume to be the case and read up on, is that the completion block for an animation is performed on a secondary queue, not the main one. So the reason why UIPageViewController was squawking and giving assertion failures, both when I originally attempted to call setViewControllers animated:NO in the completion block of setViewControllers animated:YES and also now that I am simply using a UINavigationController to push on a new UIPageViewController (but doing it, again, in the completion block of setViewControllers animated:YES) is because it's all happening on that secondary queue.

    That's why that piece of code up there works perfectly, because you come from the animation completion block and send it back over to the main queue so you don't cross the streams with UIKit. Brilliant.

    Anyway, wanted to share this journey, in case anyone runs across this problem.

    EDIT: Swift version here, if anyone's interested.

    0 讨论(0)
  • 2020-11-28 19:45

    I'll put this answer here just for my own future reference and if it helps anyone - what I ended up doing was:

    1. Delete the page and advance to the next page
    2. In the completion block of setViewControllers, I created/init'ed a new UIPageViewController with the modified data (item removed), and pushed it without animating, so nothing changes on-screen (my UIPageViewController is contained within a UINavigationController)
    3. After pushing the new UIPageViewController, get a copy of the viewControllers array of the UINavigationController, remove the second-to-last view controller (which is the old UIPageViewController)
    4. There is no step 4 - done!
    0 讨论(0)
  • 2020-11-28 19:46

    I had a similar situation where I wanted the user to be able to "tap and delete" any page from the UIPageViewController. After playing a bit with it, I found a simpler solution than the ones described above:

    1. Capture the page index of the "dying page".
    2. Check to see if the "dying page" is the last page.
      • This is important because if it is the last page, we need to scroll left (if we have pages "A B C" and delete C, we will scroll to B).
      • If it is not the last page, we will scroll right (if we have pages "A B C" and delete B, we will scroll to C).
    3. Make a temporary jump to a "safe" place (ideally the final one). Use animated: NO to have this happen instantaneously.

      UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
      
    4. Delete the selected page from the model/datasource.

    5. Now, if it is the last page:

      • Adjust your model to select the page on the left.
      • Get the viewcontroller on the left, note that THIS IS THE SAME ONE than the one you retrieved in step 3.
      • It is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
      • Jump to it, this time with animated: YES.

        jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
        [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
        
    6. In the case where it was NOT the last page:

      • Adjust your model to select the page on the right.
      • Get the viewcontroller on the right, note that this is not the one you retrieved in step 3. In my case, you will see that it is the one at dyingPageIndex, because the dying page has already been removed from the model.
      • Again, it is important to do this AFTER you have deleted the page from the datasource because it will refresh it.
      • Jump to it, this time with animated: YES.

        jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex;
        [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
        
    7. That's it! This works well in XCode 6.1.1.

    Full code below. This code is in my UIPageViewController and is called through a delegate from the page to be deleted. In my case, deleting the first page was not allowed as it contains different things from the rest of the pages. Of course, you need to substitute:

    • YourUIViewController: with the class of your individual pages.
    • YourTotalNumberOfPagesFromModel: with the total number of pages in your model
    • [YourModel deletePage:] with the code to delete the dying page from your model

      - (void)deleteAViewController:(id)sender {
          YourUIViewController *dyingGroup = (YourUIViewController *)sender;
          NSUInteger dyingPageIndex = dyingGroup.pageIndex;
          // Check to see if we are in the last page as it's a special case.
          BOOL isTheLastPage = (dyingPageIndex >= YourTotalNumberOfPagesFromModel.count);
          // Make a temporary jump back - make sure to use animated:NO to have it jump instantly
          UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
          [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
          // Now delete the selected group from the model, setting the target
          [YourModel deletePage:dyingPageIndex];
          if (isTheLastPage) {
              // Now jump to the definitive controller. In this case, it's the same one, we're just reloading it to refresh the data source.
              // This time we're using animated:YES
              jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
              [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
          } else {
              // Now jump to the definitive controller. This reloads the data source. This time we're using animated:YES
              jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex];
              [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
          }
      }
      
    0 讨论(0)
  • 2020-11-28 19:51

    For improve this. You should detect whether pageView is scrolling or not before setViewControllers.

    var isScrolling = false
    
    func viewDidLoad() {
    ...
    
      for v in view.subviews{
        if v.isKindOfClass(UIScrollView) {
          (v as! UIScrollView).delegate = self
        }
      }
    }
    
    func scrollViewWillBeginDragging(scrollView: UIScrollView){
        isScrolling = true
    }
    
    func scrollViewDidEndDecelerating(scrollView: UIScrollView){
        isScrolling = false
    }
    
    func jumpToVC{
        if isScrolling {  //you should not jump out when scrolling
            return
        }
        setViewControllers([vc], direction:direction, animated:true, completion:{[unowned self] (succ) -> Void in
            if succ {
                dispatch_async(dispatch_get_main_queue(), { () -> Void in
                    self.setViewControllers([vc], direction:direction, animated:false, completion:nil)
                })
            }
        })
    }
    
    0 讨论(0)
  • 2020-11-28 19:53

    It did it the following in Swift 3.0

    fileprivate var isAnimated:Bool = false
    
    override func setViewControllers(_ viewControllers: [UIViewController]?, direction: UIPageViewControllerNavigationDirection, animated: Bool, completion: ((Bool) -> Void)? = nil) {
    
            if self.isAnimated {
                delay(0.5, closure: { 
                    self.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
                })
            }else {
                    super.setViewControllers(viewControllers, direction: direction, animated: animated, completion: completion)
            }
    
        }
    
    
    extension SliderViewController:UIPageViewControllerDelegate {
    
        func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
            self.isAnimated = true
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
            self.isAnimated = finished
        }
    }
    

    And here the delay function

    func delay(_ delay:Double, closure:@escaping ()->()) {
        DispatchQueue.main.asyncAfter(
            deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
    }
    
    0 讨论(0)
  • 2020-11-28 19:54
    NSMutableArray *mArray = [[NSMutableArray alloc] initWithArray:self.userArray];
    [mArray removeObject:userToBlock];
    self.userArray = mArray;
    
    UIViewController *startingViewController = [self viewControllerAtIndex:atIndex-1];
    NSArray *viewControllers = @[startingViewController];
    [self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
    

    I didn't quite have time to read through the above comments but this worked for me. Basically I remove the data (in my case a user) and then move to the page before it. Works like a charm. Hope this helps those looking for a quick fix.

    0 讨论(0)
提交回复
热议问题