ビュー間でスワイプ/スライドアニメーションを実装するにはどうすればよいですか? 質問する

ビュー間でスワイプ/スライドアニメーションを実装するにはどうすればよいですか? 質問する

iOS プログラムでスワイプしたいビューがいくつかあります。現在、クロス ディゾルブ アニメーション付きのモーダル スタイルを使用して、ビュー間をスワイプしています。ただし、ホーム画面などで見られるようなスワイプ/スライド アニメーションが必要です。このような遷移をコーディングする方法がわかりません。また、アニメーション スタイルはモーダル遷移スタイルとして利用できません。コードの例を教えてもらえますか? モーダル モデルなどである必要はありません。これが最も簡単だと思っただけです。

ベストアンサー1

iOS 7以降、2つのビューコントローラ間の遷移をアニメーション化したい場合は、WWDC 2013のビデオで説明されているように、カスタムトランジションを使用します。ビューコントローラを使用したカスタム遷移たとえば、新しいビュー コントローラの表示をカスタマイズするには、次のようにします。

  1. 宛先ビュー コントローラは、プレゼンテーション アニメーションの および をself.modalPresentationStyle指定します。transitioningDelegate

    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            self.modalPresentationStyle = UIModalPresentationCustom;
            self.transitioningDelegate = self;
        }
        return self;
    }
    
  2. このデリゲート (この例では、ビュー コントローラ自体) は以下に準拠しUIViewControllerTransitioningDelegate、実装します。

    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                       presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return [[PresentAnimator alloc] init];
    }
    
    // in iOS 8 and later, you'd also specify a presentation controller
    
    - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
        return [[PresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    }
    
  3. 目的のアニメーションを実行するアニメーターを実装します。

    @interface PresentAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @implementation PresentAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.5;
    }
    
    // do whatever animation you want below
    
    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
        UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
        [[transitionContext containerView] addSubview:toViewController.view];
        CGFloat width = fromViewController.view.frame.size.width;
        CGRect originalFrame = fromViewController.view.frame;
        CGRect rightFrame = originalFrame; rightFrame.origin.x += width;
        CGRect leftFrame  = originalFrame; leftFrame.origin.x  -= width / 2.0;
        toViewController.view.frame = rightFrame;
    
        toViewController.view.layer.shadowColor = [[UIColor blackColor] CGColor];
        toViewController.view.layer.shadowRadius = 10.0;
        toViewController.view.layer.shadowOpacity = 0.5;
    
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.frame = leftFrame;
            toViewController.view.frame = originalFrame;
            toViewController.view.layer.shadowOpacity = 0.5;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
    @end
    
  4. また、ビュー階層のクリーンアップを行うプレゼンテーション コントローラーも実装します。この場合、プレゼンテーション ビューを完全にオーバーレイしているので、遷移が完了したら階層から削除できます。

    @interface PresentationController: UIPresentationController
    @end
    
    @implementation PresentationController
    
    - (BOOL)shouldRemovePresentersView {
        return true;
    }
    
    @end
    
  5. オプションとして、このジェスチャをインタラクティブにしたい場合は、次の操作も実行します。

    • インタラクション コントローラ (通常はUIPercentDrivenInteractiveTransition) を作成します。

    • UIViewControllerAnimatedTransitioningも実装します。interactionControllerForPresentationこれは明らかに前述のインタラクション コントローラーを返します。

    • ジェスチャー(または何か)で更新するinteractionController

これはすべて前述のビューコントローラを使用したカスタム遷移

ナビゲーションコントローラのプッシュ/ポップのカスタマイズの例については、ナビゲーション コントローラーのカスタム遷移アニメーション


以下に、カスタムトランジション以前の私の元の回答のコピーを示します。


@sooper さんの回答は正しく、CATransition を使用すると、必要な効果が得られます。

ただし、背景が白でない場合は、遷移の最後に奇妙なフェードインとkCATransitionPushフェードアウトCATransitionが発生し、気が散る可能性があります (特に、画像間を移動するときに、わずかにちらつくような効果が発生します)。この問題に悩まされている場合は、次の単純な遷移が非常に適切であることがわかりました。「次のビュー」を画面の右側に少し外れるように準備し、現在のビューが画面の左側に外れるようにアニメーション化すると同時に、次のビューが現在のビューがあった場所に移動するアニメーション化を行うことができます。私の例では、単一のビュー コントローラー内でサブビューをメイン ビューに出入りするようにアニメーション化していますが、おそらくその考え方は理解できるでしょう。

float width = self.view.frame.size.width;
float height = self.view.frame.size.height;

// my nextView hasn't been added to the main view yet, so set the frame to be off-screen

[nextView setFrame:CGRectMake(width, 0.0, width, height)];

// then add it to the main view

[self.view addSubview:nextView];

// now animate moving the current view off to the left while the next view is moved into place

[UIView animateWithDuration:0.33f 
                      delay:0.0f 
                    options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     [nextView setFrame:currView.frame];
                     [currView setFrame:CGRectMake(-width, 0.0, width, height)];
                 }
                 completion:^(BOOL finished){
                     // do whatever post processing you want (such as resetting what is "current" and what is "next")
                 }];

明らかに、コントロールの設定方法に応じてこれを微調整する必要がありますが、これにより、フェードなどの発生がなく、非常にシンプルな遷移が得られ、スムーズな遷移が実現されます。

注意: まず、この例も、例もCATransition、(あなたが話していた) SpringBoard ホーム画面アニメーションとはまったく異なります。これは連続的です (つまり、スワイプの途中であれば、停止して戻るなどできます)。これらの遷移は、一度開始すると、自動的に実行されます。リアルタイムのインタラクションが必要な場合は、それも実行できますが、異なります。

アップデート:

ユーザーの指を追跡する継続的なジェスチャを使用する場合は、UIPanGestureRecognizerではなくを使用できます。その場合、よりも の方が良いと思います。 を変更して座標をユーザーのジェスチャに合わせて変更し、上記のコードを変更して、ユーザーが指を離したときにアニメーションを完了するようにしました。かなりうまく機能します。 では簡単にはできないと思います。UISwipeGestureRecognizeranimateWithDurationCATransitionhandlePanGestureframeCATransition

たとえば、コントローラーのメイン ビューにジェスチャ ハンドラーを作成できます。

[self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]];

ハンドラーは次のようになります。

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    // transform the three views by the amount of the x translation

    CGPoint translate = [gesture translationInView:gesture.view];
    translate.y = 0.0; // I'm just doing horizontal scrolling

    prevView.frame = [self frameForPreviousViewWithTranslate:translate];
    currView.frame = [self frameForCurrentViewWithTranslate:translate];
    nextView.frame = [self frameForNextViewWithTranslate:translate];

    // if we're done with gesture, animate frames to new locations

    if (gesture.state == UIGestureRecognizerStateCancelled ||
        gesture.state == UIGestureRecognizerStateEnded ||
        gesture.state == UIGestureRecognizerStateFailed)
    {
        // figure out if we've moved (or flicked) more than 50% the way across

        CGPoint velocity = [gesture velocityInView:gesture.view];
        if (translate.x > 0.0 && (translate.x + velocity.x * 0.25) > (gesture.view.bounds.size.width / 2.0) && prevView)
        {
            // moving right (and/or flicked right)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the right

                                 // this redefines "next" to be the old "current",
                                 // "current" to be the old "previous", and recycles
                                 // the old "next" to be the new "previous" (you'd presumably.
                                 // want to update the content for the new "previous" to reflect whatever should be there

                                 UIView *tempView = nextView;
                                 nextView = currView;
                                 currView = prevView;
                                 prevView = tempView;
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }];
        }
        else if (translate.x < 0.0 && (translate.x + velocity.x * 0.25) < -(gesture.view.frame.size.width / 2.0) && nextView)
        {
            // moving left (and/or flicked left)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 nextView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the left

                                 // this redefines "previous" to be the old "current",
                                 // "current" to be the old "next", and recycles
                                 // the old "previous" to be the new "next". (You'd presumably.
                                 // want to update the content for the new "next" to reflect whatever should be there

                                 UIView *tempView = prevView;
                                 prevView = currView;
                                 currView = nextView;
                                 nextView = tempView;
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }];
        }
        else
        {
            // return to original location

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:NULL];
        }
    }
}

これは、frameおそらく目的の UX に合わせて定義する次の単純なメソッドを使用します。

- (CGRect)frameForPreviousViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(-self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForCurrentViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForNextViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

特定の実装は間違いなく異なりますが、これでアイデアが説明できたと思います。

これらすべてを説明して(この古い回答を補足し、明確にして)、この手法はもう使用していないことを指摘しておく必要があります。現在、私は通常、UIScrollView(「ページング」をオンにした状態)または (iOS 6 の場合)を使用しますUIPageViewController。これにより、この種のジェスチャ ハンドラーを記述する手間が省けます(スクロール バー、バウンスなどの追加機能も利用できます)。実装では、必要なサブビューを遅延ロードしていることを確認するために、イベントUIScrollViewに応答するだけです。scrollViewDidScroll

おすすめ記事