UIPageViewController
を特定のページにジャンプさせる方法に関するいくつかの質問を見つけましたが、ジャンプの追加の問題に気付きました。
IOSアプリ(ページカレンダーに似ています)の詳細には触れずに、ここで私が経験していることを説明します。 UIPageViewController
を宣言し、現在のView Controllerを設定して、データソースを実装します。
// end of the init method
pageViewController = [[UIPageViewController alloc]
initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
options:nil];
pageViewController.dataSource = self;
[self jumpToDay:0];
}
//...
- (void)jumpToDay:(NSInteger)day {
UIViewController *controller = [self dequeuePreviousDayViewControllerWithDaysBack:day];
[pageViewController setViewControllers:@[controller]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES
completion:nil];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days + 1];
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController {
NSInteger days = ((THDayViewController *)viewController).daysAgo;
return [self dequeuePreviousDayViewControllerWithDaysBack:days - 1];
}
- (UIViewController *)dequeuePreviousDayViewControllerWithDaysBack:(NSInteger)days {
return [[THPreviousDayViewController alloc] initWithDaysAgo:days];
}
編集注:デキュー方法の簡易コードを追加しました。この冒とく的な実装を使用しても、ページの順序にはまったく同じ問題があります。
初期化はすべて正常に機能します。増分ページングもすべて正常に機能します。問題は、jumpToDay
を再度呼び出すと、順序が乱れることです。
ユーザーが-5日目にいて1日目にジャンプすると、左にスクロールすると適切な0日目の代わりに5日目が再び表示されます。これは、UIPageViewController
が近くのページですが、キャッシュの更新を強制するメソッドへの参照が見つかりません。
何か案は?
Programming iOS6 、Matt Neuburgがこの正確な問題を文書化しており、彼のソリューションは現在受け入れられている答えよりも少し気分が良いことがわかりました。このソリューションは、うまく機能しますが、その前後に画像をアニメーション化し、そのページを目的のページに不快に置き換えるというマイナスの副作用があります。それは奇妙なユーザーエクスペリエンスであると感じ、Mattのソリューションがそれを処理します。
__weak UIPageViewController* pvcw = pvc;
[pvc setViewControllers:@[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:YES completion:^(BOOL finished) {
UIPageViewController* pvcs = pvcw;
if (!pvcs) return;
dispatch_async(dispatch_get_main_queue(), ^{
[pvcs setViewControllers:@[page]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO completion:nil];
});
}];
そのため、ページに「ジャンプ」できるようにする必要があるあなたと同じ問題に遭遇し、ページを振り返ったときに「順序が乱れた」ことがわかりました。私が知る限り、Page View Controllerは間違いなくView Controllerをキャッシュしているため、ページに「ジャンプ」するときは、順方向または逆方向を指定する必要があります。次に、新しいView Controllerが前のView Controllerの「隣接」であると想定しているため、ジェスチャバックすると自動的に前のView Controllerを提示します。これは、UIPageViewControllerTransitionStyleScroll
ではなくUIPageViewControllerTransitionStylePageCurl
を使用している場合にのみ発生することがわかりました。ページカールスタイルでは、ページに「ジャンプ」してからジェスチャバックすると、pageViewController:viewController(Before/After)ViewController:
メッセージがデータソースに配信され、正しいネイバービューコントローラーを提供できるため、明らかに同じキャッシュを行いません。
解決策:ページへの「ジャンプ」を実行する場合、最初にページの隣のページにジャンプできます(animated:NO
)ジャンプし、そのジャンプの完了ブロックで目的のページにジャンプします。これにより、キャッシュが更新され、ジェスチャバックすると正しい近隣ページが表示されます。欠点は、2つのView Controllerを作成する必要があることです。あなたがジャンプしているものと戻って身振りした後に表示されるべきもの。
UIPageViewController
用に作成したカテゴリのコードは次のとおりです。
@implementation UIPageViewController (Additions)
- (void)setViewControllers:(NSArray *)viewControllers direction:(UIPageViewControllerNavigationDirection)direction invalidateCache:(BOOL)invalidateCache animated:(BOOL)animated completion:(void (^)(BOOL finished))completion {
NSArray *vcs = viewControllers;
__weak UIPageViewController *mySelf = self;
if (invalidateCache && self.transitionStyle == UIPageViewControllerTransitionStyleScroll) {
UIViewController *neighborViewController = (direction == UIPageViewControllerNavigationDirectionForward
? [self.dataSource pageViewController:self viewControllerBeforeViewController:viewControllers[0]]
: [self.dataSource pageViewController:self viewControllerAfterViewController:viewControllers[0]]);
[self setViewControllers:@[neighborViewController] direction:direction animated:NO completion:^(BOOL finished) {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}];
}
else {
[mySelf setViewControllers:vcs direction:direction animated:animated completion:completion];
}
}
@end
これをテストするためにできることは、新しい「ページベースのアプリケーション」を作成し、特定の暦月に「ジャンプ」してから戻る「ジャンプ」ボタンを追加することです。遷移スタイルをスクロールに設定してください。
この機能を使用します(私は常に横向き、2ページモードです)
-(void) flipToPage:(NSString * )index {
int x = [index intValue];
LeafletPageContentViewController *theCurrentViewController = [self.pageViewController.viewControllers objectAtIndex:0];
NSUInteger retreivedIndex = [self indexOfViewController:theCurrentViewController];
LeafletPageContentViewController *firstViewController = [self viewControllerAtIndex:x];
LeafletPageContentViewController *secondViewController = [self viewControllerAtIndex:x+1 ];
NSArray *viewControllers = nil;
viewControllers = [NSArray arrayWithObjects:firstViewController, secondViewController, nil];
if (retreivedIndex < x){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:YES completion:NULL];
} else {
if (retreivedIndex > x ){
[self.pageViewController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionReverse animated:YES completion:NULL];
}
}
}
UIPageViewController
のサブクラスに使用されるmy Swiftソリューション
ViewControllerの配列をviewControllerArray
に、現在のページインデックスをupdateCurrentPageIndex
に格納するとします。
private func slideToPage(index: Int, completion: (() -> Void)?) {
let tempIndex = currentPageIndex
if currentPageIndex < index {
for var i = tempIndex+1; i <= index; i++ {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Forward, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if (complete) {
self?.updateCurrentPageIndex(i-1)
completion?()
}
})
}
}
else if currentPageIndex > index {
for var i = tempIndex - 1; i >= index; i-- {
self.setViewControllers([viewControllerArray[i]], direction: UIPageViewControllerNavigationDirection.Reverse, animated: true, completion: {[weak self] (complete: Bool) -> Void in
if complete {
self?.updateCurrentPageIndex(i+1)
completion?()
}
})
}
}
}
Djibouti33の回答の迅速なバージョン:
weak var pvcw = pageViewController
pageViewController!.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: true) { _ in
if let pvcs = pvcw {
dispatch_async(dispatch_get_main_queue(), {
pvcs.setViewControllers([page], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
})
}
}
これはiOS 10には当てはまらず、承認済みの回答ソリューションを使用する必要がなくなったことに注意することが重要です。いつものように続けてください。
この問題は確認できましたが、UIPageViewControllerTransitionStyleScroll
ではなくUIPageViewControllerTransitionStylePageCurl
を使用した場合にのみ発生します。
回避策:ループを作成し、目的のページに到達するまで、ページターンごとにUIPageViewController setViewControllers
を呼び出します。
これにより、UIPageViewControllerの内部データソースインデックスの同期が維持されます。
これは唯一の解決策です
-(void)buyAction
{
isFromBuy = YES;
APPChildViewController *initialViewController = [self viewControllerAtIndex:4];
viewControllers = [NSArray arrayWithObject:initialViewController];
[self.pageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
-(NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
if (isFromBuy) {
isFromBuy = NO;
return 5;
}
return 0;
}
以下に、最新のSwift 3+バージョンの @ djibouti33による回答 をクリーンアップした構文で示します。
weak var weakPageVc = pageVc
pageVc.setViewControllers([page], direction: .forward, animated: true) { finished in
guard let pageVc = weakPageVc else {
return
}
DispatchQueue.main.async {
pageVc.setViewControllers([page], direction: .forward, animated: false)
}
}
あなたのページが初期化後に更新されるように設計されている場合は、別のアプローチがありました:
マニュアルページが選択されると、フラグを更新します
- (void)scrollToPage:(NSInteger)page animated:(BOOL)animated
{
if (page != self.currentPage) {
[self setViewControllers:@[[self viewControllerForPage:page]]
direction:(page > self.currentPage ?
UIPageViewControllerNavigationDirectionForward :
UIPageViewControllerNavigationDirectionReverse)
animated:animated
completion:nil];
self.currentPage = page;
self.forceReloadNextPage = YES; // to override view controller automatic page cache
}
}
- (ScheduleViewController *)viewControllerForPage:(NSInteger)page
{
CustomViewController * scheduleViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"CustomViewController"];
scheduleViewController.view.tag = page; // keep track of pages using view.tag property
scheduleViewController.data = [self dataForPage:page];
if (self.currentViewController)
scheduleViewController.calendarLayoutHourHeight = self.currentViewController.calendarLayoutHourHeight;
return scheduleViewController;
}
次に、次のページに正しいデータを強制的に再読み込みさせます。
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
{
CustomViewController * nextViewController = [pendingViewControllers lastObject];
// When manual scrolling occurs, the next page is loaded from UIPageViewController cache
// and must be refreshed
if (self.forceReloadNextPage) {
// calculate the direction of the scroll to know if to load next or previous page
NSUInteger page = self.currentPage + 1;
if (self.currentPage > nextViewController.view.tag) page = self.currentPage - 1;
nextViewController.data = [self dataForPage:page];
self.forceReloadNextPage = NO;
}
}
私がやらなかったように、新しいページにアニメートする必要がない場合は、ストーリーボードの「Value Changed」と呼ばれる次のコードが役に立ちました。 View Controller間で変更する代わりに、現在のView Controllerに関連付けられているデータを変更します。
- (IBAction)pageControlCurrentPageDidChange:(id)sender
{
self.currentIndex = self.pageControl.currentPage;
MYViewController *currentPageViewController = (MYViewController *)self.pageViewController.viewControllers.firstObject;
currentPageViewController.pageData = [self.pageDataSource dataForPage:self.currentIndex];
[currentPageViewController updateDisplay];
}
currentIndexがあるので、ページ間をスワイプするとpageControlのcurrentPageを更新できます。
pageDataSource dataForPage:ページによって表示されるデータオブジェクトの配列を返します。
let orderedViewControllers = [UIViewController(),UIViewController(), UIViewController()]
let pageViewController = UIPageViewController()
let pageControl = UIPageControl()
func jump(to: Int, completion: @escaping (_ vc: UIViewController?) -> Void){
guard orderedViewControllers.count > to else{
//index of bounds
return
}
let toVC = orderedViewControllers[to]
var direction: UIPageViewController.NavigationDirection = .forward
if pageControl.currentPage < to {
direction = .forward;
} else {
direction = .reverse;
}
pageViewController.setViewControllers([toVC], direction: direction, animated: true) { _ in
DispatchQueue.main.async {
self.pageViewController.setViewControllers([toVC], direction: direction, animated: false){ _ in
self.pageControl.currentPage = to
completion(toVC)
}
}
}
}
使用法:
self.jump(to: 5) { (vc) in
// you can do anything for new vc.
}
私はこの問題に長い間苦労していました。私にとっては、ストーリーボードからUIPageViewController(PageControllerと呼びます)をロードし、その上にUIViewController 'ContentVC'を追加しました。
ContentVCにコンテンツ領域にロードされるデータを処理させ、PageControllerにスライディング/ goto/PageIndicatorの更新を処理させます。 ContentVCにはivar CurrentPageIndexがあり、その値をPageControllerに送信して、PageControllerがどのページにあるかを認識します。 PageControllerを含む.mファイルには、これら2つのメソッドがあります。
0に設定したため、PageVCがリロードされるたびに、望まない最初のページ[self viewControllerAtIndex:0]に移動します。
- (void)setPageForward
{
ContentVC *FirstVC = [self viewControllerAtIndex:[CurrentPageIndex integerValue]];
NSArray *viewControllers = @[FirstVC];
[PageController setViewControllers:viewControllers direction:UIPageViewControllerNavigationDirectionForward animated:NO completion:nil];
}
この2番目のメソッドは、PageViewControllerのDataSourceメソッドです。 presentationIndexForPageViewControllerは、強調表示されたドットを右側のページ(必要なページ)に設定します。ここで0を返す場合、ページインジケーターは最初のページを示す最初のドットを強調表示することに注意してください。
- (NSInteger)presentationIndexForPageViewController:(UIPageViewController *)pageViewController
{
return [CurrentPageIndex integerValue];
}