多控制器管理自定义

我们大多数情况都会使用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. 实现一个可切换的多视图控制器

接下来我们来实现一个类似于头条和网易新闻的简单多视图控制器。

  1. 首先在创建的容器控制器中添加一个装载VC的数组属性:

    @property (nonatomic, strong) NSArray <UIViewController *> *viewControllers;    //装载viewController的集合
    

  1. 给底部添加一个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;
    }
    
  2. 为了更好的体验,判断偏移量,自动设置向左或向右滑动,实现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];
    
        }
    }
    

  3. 添加头部的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];
        }
    }
    
  4. 最后添加一个底部小小的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) {
            
        }];
    }
    
  5. 当然,当点击头部按钮时,记得更新对应的位置:

    - (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];
    }
    

源码下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • #import "AppDelegate.h" #import "CRITabBarController.h" @...
    博行天下阅读 173评论 0 0
  • 实现自定义分段控件 相关属性声明 封装初始化类方法。调用初始化方法传入参数:需要设定的整个控件的frame 以及 ...
    WeiHing阅读 6,622评论 0 8
  • *7月8日上午 N:Block :跟一个函数块差不多,会对里面所有的内容的引用计数+1,想要解决就用__block...
    炙冰阅读 2,467评论 1 14
  • //设置尺寸为屏幕尺寸的时候self.window = [[UIWindow alloc] initWithFra...
    LuckTime阅读 787评论 0 0
  • 月下对影独酌 心中几番潇涩 晚风轻抚月色 被寂寞 误执着 一半倾城半倾国 桃花十里桃花落 一丝凄凉过 双手浸雨无人...
    墨绮阅读 450评论 5 6