@继续前面的内容,这一章,主要介绍自定义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,目前研究中.......**
** @转载请注明:iOS@迷糊小书童**