我们大多数情况都会使用NavigationController和 TabbarController去管理自己的VC。他们都属于容器控制器(一个控制器包含其他一个或多个控制器时,前者为容器控制器 (Container View Controller),后者为子控制器 (Child View Controller))。
- 对于 UINavigationController 我们知道显示在导航控制器上的控制器,永远是栈顶控制器,其实就是压栈,先进后出的原则。退出到下一级,上一级的控制器就会被销毁。
- 而 TabbarController 则是会一次性初始化所有的子控制器,但是默认只会加载第一个VC,其他的只有在显示的时候才会调用loadView去加载对于的View。与UINavigationController不同的是他的子控制器加载后会存在内存中,下次直接显示,切换子控制器是不会销毁之前显示的VC的。
很明显,使用多视图控制器的优点:
1.低耦合,对页面中的逻辑更加分明。相应的View对应相应的VC。
2.当某个子View没有显示时,将不会被Load,减少了内存的使用。
3.当内存紧张时,可以释放当前没有显示的VC,优化内存释放机制。
如果我们自己要去实现一个多视图控制器该去怎么做呢?答案就是使用容器控制器。
1. 基本使用
1.1 添加子控制器
- (void)displayContentController:(UIViewController *)content {
[self addChildViewController:content];
//注意,容器控制器的 addChildViewController: 方法会调用子控制器的 willMoveToParentViewController: 方法,因此不需要写子控制器的 willMoveToParentViewController: 方法。
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];
}
1.2 移除子控制器
- (void)hideContentController:(UIViewController *)content {
[content willMoveToParentViewController:nil];
[content.view removeFromSuperview];
[content removeFromParentViewController];
//注意,子控制器的 removeFromParentViewController 方法会调用 didMoveToParentViewController: 方法,不用写 didMoveToParentViewController: 方法。
}
1.3 子控制器之间的切换
- (void)cycleFromViewController:(UIViewController *)oldVC
toViewController:(UIViewController *)newVC {
// Prepare the two view controllers for the change.
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Get the start frame of the new view controller and the end frame
// for the old view controller. Both rectangles are offscreen.
newVC.view.frame = [self newViewStartFrame];
CGRect endFrame = [self oldViewEndFrame];
// Queue up the transition animation.
[self transitionFromViewController:oldVC toViewController:newVC
duration:0.25 options:0
animations:^{
// Animate the views to their final positions.
newVC.view.frame = oldVC.view.frame;
oldVC.view.frame = endFrame;
}
completion:^(BOOL finished) {
// Remove the old view controller and send the final
// notification to the new view controller.
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
}];
}
1.4 通知子控制器的出现和消失
- (BOOL)shouldAutomaticallyForwardAppearanceMethods {
return NO;
}
如果返回NO,容器控制器就要在子控制器出现和消失时调用如下方法通知子控制器。
- (void)beginAppearanceTransition:(BOOL)isAppearing animated:(BOOL)animated{
NSLog(@"beginAppearanceTransition");
}
- (void)endAppearanceTransition{
NSLog(@"endAppearanceTransition");
}
重要提示:当你的VC是在UINavigationController或者其他容器控制器中时,必须返回YES,否则程序会奔溃;
If the new child view controller is already the child of a container view controller, it is removed from that container before being added. This method is only intended to be called by an implementation of a custom container view controller. If you override this method, you must call super in your implementation.
2. 实现一个可切换的多视图控制器
接下来我们来实现一个类似于头条和网易新闻的简单多视图控制器。
-
首先在创建的容器控制器中添加一个装载VC的数组属性:
@property (nonatomic, strong) NSArray <UIViewController *> *viewControllers; //装载viewController的集合
-
给底部添加一个UIScrollView,然后把添加的VC依次添加到容器控制器中:
- (void)setViewControllers:(NSArray<UIViewController *> *)viewControllers{ //必须含有元素 && viewControllers中元素必须为viewController if (!viewControllers.count) { return; } for (id vc in viewControllers) { NSAssert([vc isKindOfClass:[UIViewController class]], @"viewControllers必须为viewController或其子类"); } _viewControllers = viewControllers; [self displayViewControllers]; } // addChildViewController - (void)displayViewControllers{ NSInteger i = 0; for (UIViewController *vc in _viewControllers) { [self addChildViewController:vc]; //注意,容器控制器的 addChildViewController: 方法会调用子控制器的 willMoveToParentViewController: 方法,因此不需要写子控制器的 willMoveToParentViewController: 方法。 vc.view.frame = [self calculateContentFrame:i++]; [self.contentView addSubview:vc.view]; [vc didMoveToParentViewController:self]; } self.contentView.contentSize = CGSizeMake(self.view.bounds.size.width * i, self.contentView.bounds.size.height); self.selectedIndex = 0; }
-
为了更好的体验,判断偏移量,自动设置向左或向右滑动,实现UIScrollViewDelegate方法:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset { if (scrollView == self.contentView) { CGFloat x = targetContentOffset->x; CGFloat contentView_width = self.view.bounds.size.width; CGFloat contentViewMoveLength = x - _selectedIndex * contentView_width; if (contentViewMoveLength < - contentView_width * 0.5f) { // Move left --_selectedIndex; } else if (contentViewMoveLength > contentView_width * 0.5f) { // Move right ++_selectedIndex; } targetContentOffset->x = scrollView.contentOffset.x; // Stop [scrollView setContentOffset:CGPointMake(_selectedIndex * contentView_width, scrollView.contentOffset.y) animated:YES]; // Animate to destination with default velocity [self.headScroll updateTipView:self.selectedIndex]; } }
-
添加头部的titleView,我使用的是UIScrollView,当然tableView或者collectionView都行。根据传入的title数组初始化头部列表:
- (instancetype)initWithFrame:(CGRect)frame buttonArray:(NSMutableArray *)buttonArray{ self = [super initWithFrame:frame]; if (self) { _buttons = buttonArray; [self initWithButtons]; } return self; } - (void)initWithButtons{ CGFloat offsetX = 0; for (UIButton *btn in _buttons) { btn.frame = CGRectMake(offsetX, 0, btn.frame.size.width, btn.frame.size.height); offsetX += btn.bounds.size.width; [self addSubview:btn]; } }
-
最后添加一个底部小小的tipView,并且在滑动的时候更新他的坐标,我使用了UIView的弹簧动画:
usingSpringWithDamping:它的范围为 0.0f 到 1.0f ,数值越小「弹簧」的振动效果越明显。 initialSpringVelocity:初始的速度,数值越大一开始移动越快。值得注意的是,初始速度取值较高而时间较短时,也会出现反弹情况。
- (void)updateTipView:(NSInteger)index{ UIButton *btn = [self.buttons objectAtIndex:index]; // usingSpringWithDamping:它的范围为 0.0f 到 1.0f ,数值越小「弹簧」的振动效果越明显。 // initialSpringVelocity:初始的速度,数值越大一开始移动越快。值得注意的是,初始速度取值较高而时间较短时,也会出现反弹情况。 [UIView animateWithDuration:.4 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:2 options:UIViewAnimationOptionLayoutSubviews animations:^{ self.tipView.frame = CGRectMake(btn.frame.origin.x, self.tipView.frame.origin.y, self.tipView.frame.size.width, self.tipView.frame.size.height); } completion:^(BOOL finished) { }]; }
-
当然,当点击头部按钮时,记得更新对应的位置:
- (void)headTitleButtonClick:(UIButton *)btn{ NSUInteger index = [self.headScroll.buttons indexOfObject:btn]; self.selectedIndex = index; [self.headScroll updateTipView:self.selectedIndex]; [self.contentView setContentOffset:CGPointMake(self.selectedIndex * self.contentView.frame.size.width, self.contentView.frame.origin.y) animated:NO]; }
源码下载