4

アニメーション API とビュー コントローラーのコンテインメントによりUIView、現在の Cocoa Touch スタックは、ビュー コントローラー間の自動遷移に非常に適しています。

私が書くのが難しいと思うのは、View Controller 間のインタラクティブなトランジションです。例として、プッシュ アニメーションを使用してあるビューを別のビューに置き換えたいだけのUINavigationController場合、コンテインメント API を使用または使用して、自分でトランジションを記述できます。しかし、トランジションをインタラクティブでタッチ制御にしたいのはよくあることです。ユーザーが現在のビューをドラッグし始めると、横から入ってきたビューが表示され、パンニング タッチ ジェスチャによってトランジションが制御されます。ユーザーは、少しパンして次のビューを「のぞき見る」だけで、現在のビューを表示したままパンして戻せます。ジェスチャが特定のしきい値を下回った場合、遷移はキャンセルされ、それ以外の場合は完了します。

(これが十分に明確でない場合、私は iBooks のページめくりのようなものについて話していますが、異なるビュー コントローラー間で、そのようなインタラクティブな遷移に一般化されています。)

私はそのようなトランジションの書き方を知っていますが、現在のビュー コントローラはトランジションについてあまりにも多くのことを知らなければなりません - それはあまりにも多くのコードを占有します。2 つの異なるインタラクティブなトランジションが簡単に発生する可能性があることは言うまでもありません。

インタラクティブな遷移コードを抽象化して一般化し、別のクラスまたはコードの塊に移動するパターンはありますか? たぶん図書館?

4

3 に答える 3

2

これが私がたどり着いたAPIです。これには 3 つのコンポーネントがあります。別のビュー コントローラーへの遷移を作成する通常のビュー コントローラー、カスタム コンテナー ビュー コントローラー、および遷移クラスです。遷移クラスは次のようになります。

@interface TZInteractiveTransition : NSObject

@property(strong) UIView *fromView;
@property(strong) UIView *toView;

// Usually 0–1 where 0 = just fromView visible and 1 = just toView visible
@property(assign, nonatomic) CGFloat phase;
// YES when the transition is taken far enough to perform the controller switch
@property(assign, readonly, getter = isCommitted) BOOL committed;

- (void) prepareToRun;
- (void) cleanup;

@end

この抽象クラスから、プッシュ、回転などの具体的なトランジションを派生させます。ほとんどの作業はコンテナー コントローラーで行われます (少し単純化されています)。

@interface TZTransitionController : UIViewController

@property(strong, readonly) TZInteractiveTransition *transition;

- (void) startPushingViewController: (TZViewController*) controller withTransition: (TZInteractiveTransition*) transition;
- (void) startPoppingViewControllerWithTransition: (TZInteractiveTransition*) transition;

// This method finishes the transition either to phase = 1 (if committed),
// or to 0 (if cancelled). I use my own helper animation class to step
// through the phase values with a nice easing curve.
- (void) endTransitionWithCompletion: (dispatch_block_t) completion;

@end

もう少し明確にするために、これが移行の開始方法です。

- (void) startPushingViewController: (TZViewController*) controller withTransition: (TZInteractiveTransition*) transition
{
    NSParameterAssert(controller != nil);
    NSParameterAssert([controller parentViewController] == nil);

    // 1. Add the new controller as a child using the containment API.
    // 2. Add the new controller’s view to [self view].
    // 3. Setup the transition:    
    [self setTransition:transition];
    [_transition setFromView:[_currentViewController view]];
    [_transition setToView:[controller view]];
    [_transition prepareToRun];
    [_transition setPhase:0];
}

これTZViewControllerは、遷移コントローラーへのポインターを保持する単純なUIViewControllerサブクラスです (プロパティと非常によく似ていnavigationControllerます)。に似たカスタム ジェスチャ レコグナイザーを使用しUIPanGestureRecognizerて遷移を駆動します。ビュー コントローラーのジェスチャ コールバック コードは次のようになります。

- (void) handleForwardPanGesture: (TZPanGestureRecognizer*) gesture
{
    TZTransitionController *transitionController = [self transitionController];
    switch ([gesture state]) {
        case UIGestureRecognizerStateBegan:
            [transitionController
                startPushingViewController:/* build next view controller */
                withTransition:[TZCarouselTransition fromRight]];
            break;
        case UIGestureRecognizerStateChanged: {
            CGPoint translation = [gesture translationInView:[self view]];
            CGFloat phase = fabsf(translation.x)/CGRectGetWidth([[self view] bounds]);
            [[transitionController transition] setPhase:phase];
            break;
        }
        case UIGestureRecognizerStateEnded: {
            [transitionController endTransitionWithCompletion:NULL];
            break;
        }
        default:
            break;
    }
}

結果に満足しています。かなり単純で、ハックを使用せず、新しいトランジションで簡単に拡張でき、View Controller のコードは適度に短くシンプルです。私の唯一の不満は、カスタム コンテナー コントローラーを使用する必要があることです。そのため、それが標準のコンテナーとモーダル コントローラーでどのように機能するかわかりません。

于 2013-03-27T15:11:18.853 に答える
2

これを行うためのライブラリはわかりませんが、UIViewController のカテゴリを使用するか、遷移コードを含むビュー コントローラーの基本クラスを作成して、遷移コードを抽象化しました。厄介なトランジション コードはすべて基本クラスに保持し、コントローラーではジェスチャ レコグナイザーを追加し、そのアクション メソッドから基本クラス メソッドを呼び出すだけです。

-(IBAction)dragInController:(UIPanGestureRecognizer *)sender {
    [self dragController:[self.storyboard instantiateViewControllerWithIdentifier:@"GenericVC"] sender:sender];
}

編集後:

これが私の試みの1つです。これは、上記のコードを使用してビューをドラッグするために、別のコントローラーが継承する必要があるコントローラーである DragIntoBaseVC のコードです。これはドラッグ イン (右からのみ) のみを処理し、ドラッグ アウトは処理しません (まだ作業中であり、方向に関してこれをより一般的にする方法)。このコードの多くは、回転を処理するためにそこにあります。どの向きでも機能し (逆さまを除く)、iPhone と iPad の両方で機能します。フレームを設定するのではなく、レイアウトの制約をアニメーション化することでアニメーションを作成しています。これは、Apple が向かっている方法のように思われるからです (古い支柱とスプリングのシステムは将来的に減価償却されるのではないかと思います)。

#import "DragIntoBaseVC.h"

@interface DragIntoBaseVC ()
@property (strong,nonatomic) NSLayoutConstraint *leftCon;
@property (strong,nonatomic) UIViewController *incomingVC;
@property (nonatomic) NSInteger w;
@end

@implementation DragIntoBaseVC

static int first = 1;


-(void)dragController:(UIViewController *) incomingVC sender:(UIPanGestureRecognizer *) sender {
    if (first) {
        self.incomingVC = incomingVC;
        UIView *inView = incomingVC.view;
        [inView setTranslatesAutoresizingMaskIntoConstraints:NO];
        inView.transform = self.view.transform;
        [self.view.window addSubview:inView];
        self.w = self.view.bounds.size.width;
        NSLayoutConstraint *con2;

        switch ([UIDevice currentDevice].orientation) {
            case 0:
            case 1:
                self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeLeft relatedBy:0 toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1 constant:self.w];
                con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeTop relatedBy:0 toItem:inView attribute:NSLayoutAttributeTop multiplier:1 constant:0];
                break;
            case 3:
                self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeBottom relatedBy:0 toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1 constant:self.w];
                con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeLeft relatedBy:0 toItem:inView attribute:NSLayoutAttributeLeft multiplier:1 constant:0];
                break;
            case 4:
                self.leftCon = [NSLayoutConstraint constraintWithItem:inView attribute:NSLayoutAttributeTop relatedBy:0 toItem:self.view attribute:NSLayoutAttributeTop multiplier:1 constant:-self.w];
                con2 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeRight relatedBy:0 toItem:inView attribute:NSLayoutAttributeRight multiplier:1 constant:0];
                break;
            default:
                break;
        }
        
        NSLayoutConstraint *con3 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeWidth relatedBy:0 toItem:inView attribute:NSLayoutAttributeWidth multiplier:1 constant:0];
        NSLayoutConstraint *con4 = [NSLayoutConstraint constraintWithItem:self.view attribute:NSLayoutAttributeHeight relatedBy:0 toItem:inView attribute:NSLayoutAttributeHeight multiplier:1 constant:0];
        
        NSArray *constraints = @[self.leftCon,con2,con3,con4];
        [self.view.window addConstraints:constraints];
        first = 0;
    }
    
    CGPoint translate = [sender translationInView:self.view];
    if ([UIDevice currentDevice].orientation == 0 || [UIDevice currentDevice].orientation == 1 || [UIDevice currentDevice].orientation == 3) { // for portrait or landscapeRight
        if (sender.state == UIGestureRecognizerStateBegan || sender.state == UIGestureRecognizerStateChanged) {
            self.leftCon.constant += translate.x;
            [sender setTranslation:CGPointZero inView:self.view];
            
        }else if (sender.state == UIGestureRecognizerStateEnded){
            if (self.leftCon.constant < self.w/2) {
                [self.view removeGestureRecognizer:sender];
                [self finishTransition];
            }else{
                [self abortTransition:1];
            }
        }

    }else{ // for landscapeLeft
        if (sender.state == UIGestureRecognizerStateBegan || sender.state == UIGestureRecognizerStateChanged) {
            self.leftCon.constant -= translate.x;
            [sender setTranslation:CGPointZero inView:self.view];
            
        }else if (sender.state == UIGestureRecognizerStateEnded){
            if (-self.leftCon.constant < self.w/2) {
                [self.view removeGestureRecognizer:sender];
                [self finishTransition];
            }else{
                [self abortTransition:-1];
            }
        }
    }
}



-(void)finishTransition {
    self.leftCon.constant = 0;
    [UIView animateWithDuration:.3 animations:^{
        [self.view.window layoutSubviews];
    } completion:^(BOOL finished) {
        self.view.window.rootViewController = self.incomingVC;
    }];
}



-(void)abortTransition:(int) sign {
    self.leftCon.constant = self.w * sign;
    [UIView animateWithDuration:.3 animations:^{
        [self.view.window layoutSubviews];
    } completion:^(BOOL finished) {
        [self.incomingVC.view removeFromSuperview]; // this line and the next reset the system back to the inital state.
        first = 1;
    }];
}
于 2013-03-26T07:03:36.247 に答える
2

これに答えようとしているのは少し奇妙に感じます... 私は質問を理解していないようです。あなたは間違いなく私よりもよく知っているからです。

封じ込め API を使用して遷移を自分で作成しましたが、結果に不満はありませんか? これまでのところ、非常に効果的であることがわかりました。ビュー コンテンツのないカスタム コンテナー ビュー コントローラーを作成しました (子ビューを全画面表示に設定します)。これを私の として設定しましたrootViewController

私のコンテインメント ビュー コントローラーには、あらかじめ用意された一連のトランジション ( で指定enum) が付属しており、各トランジションには、トランジションを制御するための事前定義されたジェスチャがあります。左右のスライドには 2 本指のパン、3 本指のピンチ/ズームは、画面の中央への拡大/縮小などに使用します。設定する方法があります:

- (void)addTransitionTo:(UIViewController *)viewController withTransitionType:(TransitionType)type;

次に、ビュー コントローラーのスワップ アウトをセットアップするメソッドを呼び出します。

[self.parentViewController addTransitionTo:nextViewController withTransitionType:TransitionTypeSlideLeft];
[self.parentViewController addTransitionTo:previousViewController withTransitionType:TransitionTypeSlideRight];
[self.parentViewController addTransitionTo:infoViewController withTransitionType:TransitionTypeSlideZoom];

親コンテナは、トランジション タイプに適切なトランジション ジェスチャを追加し、View Controller 間のインタラクティブな動きを管理します。パンしているときに途中で放すと、画面の大部分を覆っていた方に跳ね返ります。完全な遷移が完了すると、コンテナ ビュー コントローラーは古いビュー コントローラーとそれに伴うすべての遷移を削除します。次のメソッドを使用して、いつでも遷移を削除することもできます。

- (void)removeTransitionForType:(TransitionType)type;

インタラクティブなトランジションは素晴らしいですが、非インタラクティブなトランジションも必要な場合があります。そのためには別のタイプを使用します。なぜなら、インタラクティブにするのに適切なジェスチャー (クロスフェードなど) がわからないため、静的なトランジションしかないトランジションがいくつかあるからです。

- (void)transitionTo:(UIViewController *) withStaticTransitionType:(StaticTransitionType)type;

私はもともと、スライド デッキのようなアプリのコンテナーを作成していましたが、それ以来、方向転換していくつかのアプリで再利用しています。再利用するためにライブラリに引き出したことはまだありませんが、おそらく時間の問題です。

于 2013-03-26T19:23:07.513 に答える