iOS7新特性 ViewController轉場切換(三) 自定義視圖控制器容器的切換—非交互式 – iPhone手機開發技術文章 iPhone軟體開發教學課程

@繼續前面的內容,這一章,主要介紹自定義ViewController容器上視圖VC的切換.先來看看系統給我們提供的容器控制器 UINavigationController和UITabBarController 都有一個NSArray類型的屬性viewControllers,很明顯,存儲的就是需要切換的視圖VC.同理,我們定義一個ContainerViewController,是UIViewController的直接子類,用來作為容器依托,額,其他屬性定義詳見代碼吧,這裡不多說瞭.(PS:原先我進行多個自定義視圖VC切換的方法,是放置一個UIScrollView,然後把所有childViewController的View的frame的X坐標,依此按320遞增,大傢可以自行想象下,這樣不好的地方,我感覺就是所有的VC一經加載就全部實體化瞭,而且不會因為被切換變成暫不顯示而釋放掉)

@偷懶下,用storyboard創建的5個childVC

 

// ContainerViewController
@interface FTContainerViewController () 

@property (strong, nonatomic) FTPhotoSenderViewController       *photoSenderViewController;
@property (strong, nonatomic) FTVideoSenderViewController       *videoSenderViewController;
@property (strong, nonatomic) FTFileSenderViewController        *fileSenderViewController;
@property (strong, nonatomic) FTContactSenderViewController     *contactSenderViewController;
@property (strong, nonatomic) FTClipboardSenderViewController   *clipboardSenderViewController;
@property (strong, nonatomic) UIViewController                  *selectedViewController;        // 當前選擇的VC
@property (strong, nonatomic) NSArray                           *viewControllers;               // childVC數組
@property (assign, nonatomic) NSInteger                         currentControllerIndex;         // 當前選擇的VC的數組下標號


@end

@implementation FTContainerViewController

#pragma mark - ViewLifecycle Methods

- (void)viewDidLoad
{
    [super viewDidLoad];
    // childVC
    self.photoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@FTPhotoSenderViewController];
    self.videoSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@FTVideoSenderViewController];
    self.fileSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@FTFileSenderViewController];
    self.contactSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@FTContactSenderViewController];
    self.clipboardSenderViewController = [self.storyboard instantiateViewControllerWithIdentifier:@FTClipboardSenderViewController];
    // 存儲childVC的數組
    self.viewControllers = @[_photoSenderViewController,_videoSenderViewController,_fileSenderViewController,_contactSenderViewController,_clipboardSenderViewController];
    // 缺省為下標為0的VC
    self.selectedViewController = self.selectedViewController ?: self.viewControllers[0];
    self.currentControllerIndex = 0;
}

@依舊,實現UIViewControllerAnimatedTransitioning協議的Animator類,不過裡面換個動畫效果,利用iOS7新增的彈簧動畫效果:

 

 

#import FTMthTransitionAnimator.h

@implementation FTMthTransitionAnimator

static CGFloat const kChildViewPadding = 16;
static CGFloat const kDamping = 0.5;    // damping參數代表彈性阻尼,隨著阻尼值越來越接近0.0,動畫的彈性效果會越來越明顯,而如果設置阻尼值為1.0,則視圖動畫不會有彈性效果
static CGFloat const kInitialSpringVelocity = 0.5;  // 初始化彈簧速率

- (NSTimeInterval)transitionDuration:(id)transitionContext
{
    return 1.0;
}

- (void)animateTransition:(id)transitionContext
{
    /**
     *  - viewControllerForKey:我們可以通過他訪問過渡的兩個 ViewController。
     *  - containerView:兩個 ViewController 的 containerView。
     *  - initialFrameForViewController 和 finalFrameForViewController 是過渡開始和結束時每個 ViewController 的 frame。
     */
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    [[transitionContext containerView] addSubview:toViewController.view];
    toViewController.view.alpha = 0;
    BOOL goingRight = ([transitionContext initialFrameForViewController:toViewController].origin.x < [transitionContext finalFrameForViewController:toViewController].origin.x);
    CGFloat transDistance = [transitionContext containerView].bounds.size.width + kChildViewPadding;
    CGAffineTransform transform = CGAffineTransformMakeTranslation(goingRight ? transDistance : -transDistance, 0);
    // CGAffineTransformInvert 反轉
    toViewController.view.transform = CGAffineTransformInvert(transform);
//    toViewController.view.transform = CGAffineTransformTranslate(toViewController.view.transform, (goingRight ? transDistance : -transDistance), 0);
    
    /**
     *   ----------彈簧動畫.....-------
     *  使用由彈簧的運動描述的時序曲線` animations` 。當` dampingRatio`為1時,動畫將平穩減速到其最終的模型值不會振蕩。阻尼比小於1來完全停止前將振蕩越來越多。可以使用彈簧的初始速度,以指定的速度在模擬彈簧的端部的物體被移動它附著之前。這是一個單元坐標系,其中1是指行駛總距離的動畫在第二。所以,如果你改變一個物體的位置由200PT在這個動畫,以及你想要的動畫表現得好像物體在動,在100PT /秒的動畫開始之前,你會通過0.5 。你通常會想通過0的速度。
     */
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:kDamping initialSpringVelocity:kInitialSpringVelocity options:0x00 animations:^{
        fromViewController.view.transform = transform;
        fromViewController.view.alpha = 0;
        // CGAffineTransformIdentity  重置,初始化
        toViewController.view.transform = CGAffineTransformIdentity;
        toViewController.view.alpha = 1;
    } completion:^(BOOL finished) {
        fromViewController.view.transform = CGAffineTransformIdentity;
        // 聲明過渡結束-->記住,一定別忘瞭在過渡結束時調用 completeTransition: 這個方法。
        [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
    }];
}

@end

@接下來的代碼,就是實現自定義容器切換的關鍵瞭.通常情況下,當我們使用系統內建的類時,系統框架為我們創建瞭轉場上下文對象,並把它傳遞給動畫控制器。但是在我們這種情況下,我們需要自定義轉場動畫,所以我們需要承擔系統框架的責任,自己去創建這個轉場上下文對象。

 

 

@interface FTMthTransitionContext : NSObject 

- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight;
@property (nonatomic, copy) void (^completionBlock)(BOOL didComplete);
@property (nonatomic, assign, getter=isAnimated) BOOL animated;
@property (nonatomic, assign, getter=isInteractive) BOOL interactive; // 是否交互式

@property (nonatomic, strong) NSDictionary *privateViewControllers;
@property (nonatomic, assign) CGRect privateDisappearingFromRect;
@property (nonatomic, assign) CGRect privateAppearingFromRect;
@property (nonatomic, assign) CGRect privateDisappearingToRect;
@property (nonatomic, assign) CGRect privateAppearingToRect;
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, assign) UIModalPresentationStyle presentationStyle;

@end

@implementation FTMthTransitionContext

- (instancetype)initWithFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController goingRight:(BOOL)goingRight {
    
	if ((self = [super init])) {
		self.presentationStyle = UIModalPresentationCustom;
		self.containerView = fromViewController.view.superview;
		self.privateViewControllers = @{
                                        UITransitionContextFromViewControllerKey:fromViewController,
                                        UITransitionContextToViewControllerKey:toViewController,
                                        };
		
		// Set the view frame properties which make sense in our specialized ContainerViewController context. Views appear from and disappear to the sides, corresponding to where the icon buttons are positioned. So tapping a button to the right of the currently selected, makes the view disappear to the left and the new view appear from the right. The animator object can choose to use this to determine whether the transition should be going left to right, or right to left, for example.
		CGFloat travelDistance = (goingRight ? -self.containerView.bounds.size.width : self.containerView.bounds.size.width);
		self.privateDisappearingFromRect = self.privateAppearingToRect = self.containerView.bounds;
		self.privateDisappearingToRect = CGRectOffset (self.containerView.bounds, travelDistance, 0);
		self.privateAppearingFromRect = CGRectOffset (self.containerView.bounds, -travelDistance, 0);
	}
	
	return self;
}

- (CGRect)initialFrameForViewController:(UIViewController *)viewController {
	if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) {
		return self.privateDisappearingFromRect;
	} else {
		return self.privateAppearingFromRect;
	}
}

- (CGRect)finalFrameForViewController:(UIViewController *)viewController {
	if (viewController == [self viewControllerForKey:UITransitionContextFromViewControllerKey]) {
		return self.privateDisappearingToRect;
	} else {
		return self.privateAppearingToRect;
	}
}

- (UIViewController *)viewControllerForKey:(NSString *)key {
	return self.privateViewControllers[key];
}

- (void)completeTransition:(BOOL)didComplete {
	if (self.completionBlock) {
		self.completionBlock (didComplete);
	}
}

// 非交互式,直接返回NO,因為不允許交互當然也就無法操作進度取消
- (BOOL)transitionWasCancelled { return NO; }

// 非交互式,直接不進行操作,隻有進行交互,下面3個協議方法才有意義,可參照系統給我們定義好的交互控制器
// @interface UIPercentDrivenInteractiveTransition : NSObject 
- (void)updateInteractiveTransition:(CGFloat)percentComplete {}
- (void)finishInteractiveTransition {}
- (void)cancelInteractiveTransition {}

@end

@OK,準備工作都做好瞭,為瞭仿照UIScrollView的滑動切換,但又因為現在展示的是非交互式,我們定義一個swip(輕掃)手勢.

 

 

    UISwipeGestureRecognizer *leftGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)];
    [leftGesture setDirection:UISwipeGestureRecognizerDirectionLeft];
    [self.view addGestureRecognizer:leftGesture];
    
    UISwipeGestureRecognizer *rightGesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swapController:)];
    [rightGesture setDirection:UISwipeGestureRecognizerDirectionRight];
    [self.view addGestureRecognizer:rightGesture];
// 響應手勢的方法
- (void)swapViewControllers:(UISwipeGestureRecognizer *)swipeGestureRecognizer
{
    if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
        if (_currentControllerIndex < 4) {
            _currentControllerIndex++;
        }
        NSLog(@_currentControllerIndex = %ld,(long)_currentControllerIndex);
        UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex];
         NSLog(@%s__%d__|%@,__FUNCTION__,__LINE__,@右邊);
         self.selectedViewController = selectedViewController;
    } else if (swipeGestureRecognizer.direction == UISwipeGestureRecognizerDirectionRight){
         NSLog(@%s__%d__|%@,__FUNCTION__,__LINE__,@左邊);
        if (_currentControllerIndex > 0) {
            _currentControllerIndex--;
        }
        UIViewController *selectedViewController = self.viewControllers[_currentControllerIndex];
        self.selectedViewController = selectedViewController;
    }
}

// 重寫selectedViewController的setter
- (void)setSelectedViewController:(UIViewController *)selectedViewController
{
	NSParameterAssert (selectedViewController);
	[self _transitionToChildViewController:selectedViewController];
	_selectedViewController = selectedViewController;
}

// 切換操作(自定義的,聯想我在前面文章網易標簽欄切換中,系統給的transitionFromViewController,是一個道理)
- (void)_transitionToChildViewController:(UIViewController *)toViewController
{
    UIViewController *fromViewController = self.childViewControllers.count > 0 ? self.childViewControllers[0] : nil;
    if (toViewController == fromViewController) {
        return;
    }
    UIView *toView = toViewController.view;
	[toView setTranslatesAutoresizingMaskIntoConstraints:YES];
	toView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
	toView.frame = self.view.bounds;
	
    // 自定義容器的切換,addChildViewController是關鍵,它保證瞭你想要顯示的VC能夠加載到容器中
    // 而所謂的動畫和上下文,隻是為瞭轉場的動畫效果
    // 因此,就算用UIScrollView切換,也不能缺少addChildViewController,切記!切記!
	[fromViewController willMoveToParentViewController:nil];
	[self addChildViewController:toViewController];
	
	if (!fromViewController) {
		[self.view addSubview:toViewController.view];
		[toViewController didMoveToParentViewController:self];
		return;
	}
    
    // Animator
    FTMthTransitionAnimator *transitionAnimator = [[FTMthTransitionAnimator alloc] init];
    NSUInteger fromIndex = [self.viewControllers indexOfObject:fromViewController];
	NSUInteger toIndex = [self.viewControllers indexOfObject:toViewController];
    
    // Context
    FTMthTransitionContext *transitionContext = [[FTMthTransitionContext alloc] initWithFromViewController:fromViewController toViewController:toViewController goingRight:(toIndex > fromIndex)];
    transitionContext.animated = YES;
	transitionContext.interactive = NO;
	transitionContext.completionBlock = ^(BOOL didComplete) {
        // 因為是非交互式,所以fromVC可以直接直接remove出its parent's children controllers array
		[fromViewController.view removeFromSuperview];
		[fromViewController removeFromParentViewController];
		[toViewController didMoveToParentViewController:self];
		if ([transitionAnimator respondsToSelector:@selector (animationEnded:)]) {
			[transitionAnimator animationEnded:didComplete];
		}
	};
    // 轉場動畫需要以轉場上下文為依托,因為我們是自定義的Context,所以要手動設置
	[transitionAnimator animateTransition:transitionContext];
}

大功告成.

 

上面展示的就是一個基本的自定義容器的非交互式的轉場切換.那交互式的呢?從上面我定義手勢定義為swip而不是pan也可以看出,非交互轉場,並不能完全實現UIScrollView那種分頁式的效果,按照類似百分比的形式來進行fromVC和toVC的切換,因為我們缺少交互控制器.在自定義的容器中,系統是沒有提供返回交互控制器的協議給我們的,查瞭蠻多資料,也沒找到給出明確的方法,我認為,要跟實現轉場上下文一樣,仿照系統方法,自定義的去實現交互式的協議方法.我們就要去思考,系統是如何搭建起這個環境的.

容後續給出響應的Demo,目前研究中…….

 

發佈留言