49

これを行う簡単な方法がないのは奇妙なことです。次のシナリオを検討してください。

  1. 1ページのページビューコントローラがあります。
  2. 別のページ(合計2)を追加し、そのページまでスクロールします。
  3. 私が欲しいのは、ユーザーが最初のページにスクロールバックすると、2番目のページが削除されて割り当てが解除され、ユーザーはそのページにスワイプして戻ることができなくなることです。

遷移が完了した後、子View ControllerとしてViewControllerを削除しようとしましたが、それでも空のページにスクロールして戻ることができます(ページビューの「サイズ変更」は行われません)

私がやりたいことは可能ですか?

4

12 に答える 12

129

ここでの回答はすべて参考になりますが、問題を処理する別の方法があります。

UIPageViewController は、スクロール遷移スタイルで間違ったページに移動します

この問題への回答を最初に検索したとき、検索の言い回しが、リンクしたばかりの質問ではなく、この質問に巻き込まれたため、この別の質問にリンクする回答を投稿する義務があると感じました。見つけたので、少し詳しく説明します。

この問題は、ここのマットによってかなりよく説明されています。

これは実際には UIPageViewController のバグです。スクロール スタイル (UIPageViewControllerTransitionStyleScroll) でのみ発生し、animated:YES で setViewControllers:direction:animated:completion: を呼び出した後にのみ発生します。したがって、次の 2 つの回避策があります。

UIPageViewControllerTransitionStyleScroll を使用しないでください。

または、setViewControllers:direction:animated:completion: を呼び出す場合は、animated:NO のみを使用してください。

バグを明確に確認するには、setViewControllers:direction:animated:completion: を呼び出してから、(ユーザーとして) インターフェースで、手動で前のページに左 (戻る) に移動します。間違ったページに戻ります。前のページではなく、setViewControllers:direction:animated:completion: が呼び出されたときにいたページです。

バグの理由は、スクロール スタイルを使用する場合、UIPageViewController が何らかの内部キャッシュを行うためと思われます。したがって、setViewControllers:direction:animated:completion: の呼び出し後、内部キャッシュのクリアに失敗します。前のページが何であるかを知っていると思います。したがって、ユーザーが前のページに左方向に移動すると、UIPageViewController は dataSource メソッド pageViewController:viewControllerBeforeViewController: の呼び出しに失敗するか、間違った現在のビュー コントローラーで呼び出します。

これは適切な説明であり、この質問で指摘されている問題とはまったく異なりますが、非常に近いものです。setViewControllersユーザーが次にジェスチャでパンしたときに、UIPageViewController がそのデータ ソースを強制的に再クエリするかどうかについての行に注意してくださいanimated:NO。これは、「どこにあるか」または現在のビュー コントローラーの隣にあるビュー コントローラーがわからなくなるためです。 .

ただし、アニメーションで PageView をプログラムで移動する必要がある場合があったため、これはうまくいきませんでし

したがって、最初に考えたのはsetViewControllers、アニメーションを使用して呼び出し、完了ブロックで、現在表示されているビュー コントローラーを使用してメソッドを再度呼び出すことでしたが、アニメーションは使用しませんでした。したがって、ユーザーは問題なくパンできますが、メソッドを再度呼び出して、ページ ビューをリセットします。

残念ながら、試してみると、ページビューコントローラーから奇妙な「アサーションエラー」が発生し始めました。それらは次のようになります。

*** -[UIPageViewController queuingScrollView: ... でのアサーションの失敗

なぜこれが起こったのか正確にはわからなかったので、私は後戻りし、最終的にJai の答えを解決策として使用し始め、まったく新しい UIPageViewController を作成し、それを UINavigationController にプッシュしてから、古いものを取り出しました。ひどいですが、ほとんどの場合は機能します。次のように、UIPageViewController から時折アサーション エラーが発生することがあります。

*** -[UIPageViewController queuingScrollView:didEndManualScroll:toRevealView:direction:animated:didFinish:didComplete:] でのアサーションの失敗、/SourceCache/UIKit_Sim/UIKit-2380.17/UIPageViewController.m:1820 $1 = 154507824 可視ビューを管理するビュー コントローラーがありません >

そしてアプリが落ちる。なんで?さて、検索すると、上で言及したこの他の質問が見つかりました。特に、単純に呼び出して、同じビューコントローラーで呼び出しが完了したらすぐにUIPageViewControllerをリセットするという私の最初のアイデアを支持する受け入れられた答えが見つかりましたが、それは持っていました欠落している要素: そのコードをメイン キューに呼び戻します! コードは次のとおりです。setViewControllers: animated:YESsetViewControllers: animated:NO

__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
                });
            }
        }];

わお!これが実際に私にとって理にかなっている唯一の理由は、WWDC 2012 セッション 211、Building Concurrent User Interfaces on iOS (開発者アカウントでここから入手可能) を見たからです。UIKit オブジェクト (UIPageViewController など) が依存するデータ ソース オブジェクトを変更しようとして、それをセカンダリ キューで実行すると、厄介なクラッシュが発生する可能性があることを思い出しました。

特に文書化されているのを見たことがありませんが、実際にそうであると想定して読み進めなければならないのは、アニメーションの完了ブロックがメイン キューではなくセカンダリ キューで実行されるということです。setViewControllers animated:NOUIPageViewController が最初に完了ブロックを呼び出そうとしたときとsetViewControllers animated:YES、単に UINavigationController を使用して新しい UIPageViewController をプッシュしようとしたときの両方で、UIPageViewController がしゃがんでアサーションに失敗した理由)のブロックは、setViewControllers animated:YESすべてそのセカンダリ キューで発生しているためです。

アニメーション完了ブロックから来て、それをメイン キューに送り返すため、UIKit でストリームを横断しないため、そこにあるコードの一部が完全に機能するのはそのためです。素晴らしい。

とにかく、誰かがこの問題に出くわした場合に備えて、この旅を共有したかった.

編集:誰かが興味を持っている場合は、こちらの Swift バージョン。

于 2013-06-26T21:16:41.990 に答える
8

Matt Mc のすばらしい答えを結論付けて、次のメソッドを UIPageViewController のサブクラスに追加して、バグが存在しない場合に使用することを意図していた setViewControllers:direction:animated:completion: を使用できるようにすることができます。

- (void) setViewControllers:(NSArray*)viewControllers direction:(UIPageViewControllerNavigationDirection)direction animated:(BOOL)animated completion:(void (^)(BOOL))completion {

    if (!animated) {
        [super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
        return;
    }

    [super setViewControllers:viewControllers direction:direction animated:YES completion:^(BOOL finished){

        if (finished) {
            dispatch_async(dispatch_get_main_queue(), ^{
                [super setViewControllers:viewControllers direction:direction animated:NO completion:completion];
            });
        } else {
            if (completion != NULL) {
                completion(finished);
            }
        }
    }];
}

次に、このメソッドを実装するクラス/サブクラスで setViewControllers:direction:animated:completion: を呼び出すだけで、期待どおりに動作するはずです。

于 2014-08-28T12:51:13.260 に答える
5

maq そうですね。スクロール トランジションを使用している場合、UIPageViewController から子ビュー コントローラーを削除しても、ユーザーがページに移動した場合に、削除された「ページ」が画面に戻るのを防ぐことはできません。興味がある場合は、UIPageViewController から子ビュー コントローラーを削除する方法を次に示します。

// deleteVC is a child view controller of the UIPageViewController
[deleteVC willMoveToParentViewController:nil];
[deleteVC.view removeFromSuperview];
[deleteVC removeFromParentViewController]; 

ビュー コントローラーの deleteVC は、UIPageViewControllers の childViewControllers プロパティから削除されますが、ユーザーがそこに移動すると、引き続き画面に表示されます。

私より賢い誰かがエレガントな解決策を見つけるまで、回避策があります (これはハックです。UIPageViewController からページを本当に削除する必要があるかどうかを自問する必要があります)。

これらの手順は、一度に 1 ページのみが表示されることを前提としています。

ユーザーがページを削除することを示すボタンをタップした後、setViewControllers:direction:animated:completion: メソッドを使用して次または前のページに移動します。もちろん、ページのコンテンツをデータ モデルから削除する必要があります。

次に (これがハックです)、まったく新しい UIPageViewController を作成して構成し、フォアグラウンド (つまり、他の UIPageViewController の前) にロードします。新しい UIPageViewController が、以前に表示されていたのとまったく同じページの表示を開始することを確認してください。新しい UIPageViewController は、データ ソースから新しいビュー コントローラーを取得します。

最後に、バックグラウンドにある UIPageViewController をアンロードして破棄します。

とにかく、maq は本当に良い質問をしました。残念ながら、質問に賛成票を投じるのに十分な評判ポイントがありません。ああ、夢を見て……いつか名声が15になる。

于 2013-02-13T06:44:10.460 に答える
4

私自身の将来の参考のために、この回答をここに掲載します。

  1. ページを削除して次のページに進む
  2. の完了ブロックでsetViewControllers、変更されたデータ (アイテムが削除された) を使用して新しい UIPageViewController を作成/初期化し、アニメーション化せずにプッシュしたため、画面上で何も変化しません (私の UIPageViewController は UINavigationController 内に含まれています)。
  3. 新しい UIPageViewController をプッシュした後、viewControllersUINavigationController の配列のコピーを取得し、最後から 2 番目のビュー コントローラー (古い UIPageViewController) を削除します。
  4. ステップ 4 はありません。
于 2013-04-30T20:08:14.483 に答える
3

私はこれを自分で学習しているだけなので、一粒の塩を取りますが、私が理解していることから、ビューコントローラーを削除するのではなく、ページビューコントローラーのデータソースを変更する必要があります。pageviewcontroller に表示されるページ数は、viewcontroller ではなく、そのデータソースによって決まります。

于 2013-01-09T00:47:36.687 に答える
1

この問題は、viewController 間のスワイプ ジェスチャ アニメーション中に viewController を変更しようとすると発生します。この問題を解決するために、ページ ビュー コントローラーがいつ遷移するかを検出するための簡単なフラグを作成しました。

- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers {
    self.pageViewControllerTransitionInProgress = YES;
}

- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed {
        self.pageViewControllerTransitionInProgress = NO;
}

そして、View Controllerを変更しようとしているときに、進行中の移行があるかどうかを確認しています。

- (void)setCurrentPage:(NSString *)newCurrentPageId animated:(BOOL)animated {

    if (self.pageViewControllerTransitionInProgress) {
        return;
    }

    [self.pageContentViewController setViewControllers:@[pageDetails] direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:^(BOOL finished) {
    }

}
于 2015-06-26T21:41:18.463 に答える
-1

ユーザーがUIPageViewControllerから任意のページを「タップして削除」できるようにしたいという同様の状況がありました。少し遊んだ後、上記の解決策よりも簡単な解決策を見つけました。

  1. 「瀕死のページ」のページ インデックスをキャプチャします。
  2. 「瀕死のページ」が最後のページかどうかを確認します。
    • 最後のページの場合は左にスクロールする必要があるため、これは重要です (ページ「AB C」があり、C を削除すると、B にスクロールします)。
    • 最後のページでない場合は、右にスクロールします (「AB C」というページがあり、B を削除すると、C にスクロールします)。
  3. 「安全な」場所 (理想的には最後の場所) に一時的にジャンプします。animation: NO を使用して、これを瞬時に発生させます。

    UIViewController *jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
    [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:NO completion:nil];
    
  4. 選択したページをモデル/データソースから削除します。

  5. ここで、最後のページの場合:

    • 左側のページを選択するようにモデルを調整します。
    • 左側のビュー コントローラーを取得します。これは、手順 3 で取得したものと同じであることに注意してください。
    • ページが更新されるため、データソースからページを削除した後にこれを行うことが重要です。
    • それにジャンプします。今度はアニメーションで: はい。

      jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex-1];
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:nil];
      
  6. 最後のページではなかった場合:

    • モデルを調整して、右側のページを選択します。
    • 右側のビュー コントローラーを取得します。これは手順 3 で取得したものではないことに注意してください。私の場合、死にかけているページは既にモデルから削除されているため、それが死にかけたページインデックスにあることがわかります。
    • 繰り返しますが、ページが更新されるため、データソースからページを削除した後にこれを行うことが重要です。
    • それにジャンプします。今度はアニメーションで: はい。

      jumpToAnotherViewController = [self viewControllerAtIndex:dyingPageIndex;
      [self setViewControllers:@[jumpToAnotherViewController] direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:nil];
      
  7. それでおしまい!これは XCode 6.1.1 でうまく機能します。

以下の完全なコード。このコードは私の UIPageViewController にあり、削除するページからデリゲートを介して呼び出されます。私の場合、最初のページには残りのページとは異なるものが含まれているため、最初のページを削除することはできませんでした。もちろん、次のように置き換える必要があります。

  • YourUIViewController: 個々のページのクラス。
  • YourTotalNumberOfPagesFromModel: モデルの総ページ数
  • [YourModel deletePage:] モデルから瀕死のページを削除するコード

    - (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];
        }
    }
    
于 2015-02-16T05:34:43.883 に答える
-1

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
    }
}

そしてここで遅延関数

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)
}
于 2016-10-05T12:06:02.390 に答える