一句代码实现带有头视图的pageController效果

demo地址

先看效果:

头视图跟随下移.gif
下拉头视图放大.gif

现在很多这样的需求,拿到需求的时候是不是不知所措呢?是不是在想着,那么难的控制器效果,iOS官方为何不专门出一个控件呢? 然后就去网上找一堆三方,看的一阵蒙蔽,再然后就是头大!!!!!

本篇文章教你快速如何实现,并可以封装后一句代码实现本效果,从此再也不用担心产品提这些需求了。(不知道我这是不是救了你们产品经理一命)


原理剖析

当看不明白时可以直接跳到代码实现部分
底层容器视图,可以左右滑动,那么可以采用UIScrollView和UICollectionView。

UIScrollView实现
  • 底部采用UIScrollView,然后每页采用tableView(或者collectionView,scrollView,webView等),加到scrollView上
  • 每页的tableView设置空的headerView
  • 视觉上的headerView是添加到self.view上的,然后根据scollView.contentOffset.y的偏移更改headerView的frame
  • segment放在橙色部分,添加到headerView上

优点: 每页的tableView可以分离到不同的UIViewController中,然后通过

 [self.scrollView addSubview:childVC.view];
 [self addChildViewController:childVC];

添加到scrollView,便于每个tableView的代码管理。

缺点:scrollView的subViews不复用,subViews较多的时候占用内存较大

  1. UICollectionView
  • 底部采用UICollectionView,然后Cell中实现tableView(或者collectionView,scrollView,webView等)
  • 每个cell中的tableView设置空的headerView
  • 视觉上的headerView是添加到self.view上的,然后根据collectionView.contentOffset.y的偏移更改headerView的frame
  • segment放在橙色部分,添加到headerView上
    优点:cell复用,省内存
    缺点:封装的话使用着没有UIScrollView的封装方便,代码也比UIScrollView多

实现

这里以UIScrollView为容器实现

//代码中用到的的宏定义
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define HEAD_HEIGHT 240 //headerView的高度
//需要的视图
@property (nonatomic , strong) UIScrollView *hScrollView;
@property (nonatomic , strong) UITableView *tableView1;
@property (nonatomic , strong) UITableView *tableView2;
@property (nonatomic , strong) UIImageView *headView;

这里忽略各个view的实现部分,因为都是常规的视图创建,需要的就是实现滚动的代理,更改headerView的frame,让headerView看起来像是跟着scrollview滚动的

*
 scrollView滑动时调用
 */
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.hScrollView) {
        //如果是底层scrollView的滑动则不用更改headerView跟随滑动
        return;
    }
    
    //如果是其他scrollView的滑动则需要更改headerView跟随滑动
    CGFloat contentY = scrollView.contentOffset.y;
    
    
    // 偏移量contentY有三种情况:
    // 1. 头视图完全显示,视图下拉,即:contentY < 0,此时可做处理:headerView跟随下移或者headerView放放大
    // 2. 头视图部分显示,即contentY >= 0 && contentY < HEAD_HEIGHT,此时headerView跟随contentY移动
    // 3. 头视图隐藏(或者只显示segment),即contentY >= HEAD_HEIGHT,此时headerView固定frame
    if (contentY < 0) {
        self.headView.frame = CGRectMake(SCREEN_WIDTH * contentY / HEAD_HEIGHT /2, 0, SCREEN_WIDTH * (HEAD_HEIGHT - contentY)/HEAD_HEIGHT, HEAD_HEIGHT - contentY);//头视图放大
//        self.headView.frame = CGRectMake(0, -contentY, SCREEN_WIDTH, HEAD_HEIGHT);//头视图跟随下移
    }else if (contentY >= 0 && contentY < HEAD_HEIGHT) {
        self.headView.frame = CGRectMake(0, - contentY, SCREEN_WIDTH, HEAD_HEIGHT);
    }else if (contentY >= HEAD_HEIGHT) {
        if (CGRectGetMinY(self.headView.frame) != -HEAD_HEIGHT) {
            self.headView.frame = CGRectMake(0, - HEAD_HEIGHT, SCREEN_WIDTH, HEAD_HEIGHT);

        }}    
}

但是此时左右滑动,切换page时,发现各个page的状态不同步,为了减少代码的调用次数多了,所以在另外两个代理中实现各个page的contentOffet的同步

//放开手指时调用
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    if (scrollView == self.hScrollView) {
        
        return;
    }
    CGFloat contentY = scrollView.contentOffset.y;
    [self updateTableViewFrame:contentY];

}

//放开手指后,若tableView仍然自己滚动,自己滚动结束时会调用
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    if (scrollView == self.hScrollView) {
        
        return;
    }
    CGFloat contentY = scrollView.contentOffset.y;
    [self updateTableViewFrame:contentY];
}

- (void)updateTableViewFrame:(CGFloat)offsetY {
    if (offsetY >= HEAD_HEIGHT) {
  //头视图已隐藏时,若其他page的tableView的contentOffset的状态是headview没隐藏的状态,则更改为头视图已隐藏时的偏移量
        if ( self.tableView1.contentOffset.y <= HEAD_HEIGHT) {
            self.tableView1.contentOffset = CGPointMake(0, HEAD_HEIGHT);
        }
        
        if ( self.tableView2.contentOffset.y <= HEAD_HEIGHT) {
            self.tableView2.contentOffset = CGPointMake(0, HEAD_HEIGHT);
        }
    }else if (offsetY >= 0 && offsetY < HEAD_HEIGHT) {
// 有视图部分显示若其他page的tableView的contentOffset的状态不是headview部分隐藏的状态,则更改为头视图部分隐藏的偏移量
        self.tableView1.contentOffset = CGPointMake(0, offsetY);
        self.tableView2.contentOffset = CGPointMake(0, offsetY);
        
    }else if (offsetY < 0) {
//头视图完全显示时再下拉
        if ( self.tableView1.contentOffset.y > 0) {
            self.tableView1.contentOffset = CGPointMake(0, 0);
        }
        
        if ( self.tableView2.contentOffset.y > 0) {
            self.tableView2.contentOffset = CGPointMake(0, 0);
        }
    }
}

完成


封装

明白了怎么实现,也通过上面的简单demo完成了任务,然后呢,我们需要一劳永逸

如果每次有这样的需求,我们都实现一遍,明显是很费脑子的,我们程序员的脑细胞死的本来就多,就不要再做这些无谓的牺牲了,那么封装一下,一步到位才是我们想要的结果!!!

封装目标:

  1. 每页的数据由单独的UIViewController控制
  2. 继承封装好的ViewController后,只需要childVC,headerView,segment.height信息
  3. 能够监测到childVC切换到了第几个

注意: 因为每页(UIViewController)的tableView由UIViewController单独完成,所以tableView的代理肯定在它的VC中实现。所以封装的VC采用KVO监测contentOffset的变化。

#import <UIKit/UIKit.h>


@interface SHViewController : UIViewController


/**
 添加要左右滑动的viewController
 使用viewController能够更好的

 @param childVCArray vc数组
 @param headerView 头视图
 @param segmentHeight segment高度
 */
- (void)addChildVCWithArray:(NSArray <UIViewController *> *)childVCArray
                 headerView:(UIView *)headerView
              segmentHeight:(CGFloat)segmentHeight;

/**
 切换vc时调用,index为要显示的vc下表
 */
@property (nonatomic , copy) void(^viewControllerScrollToIndex)(NSInteger index);

@end
#import "SHViewController.h"
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
#define SCREEN_HEIGHT [UIScreen mainScreen].bounds.size.height
#define WEAKSELF __weak typeof(self) weakSelf = self;
@interface SHViewController ()
<
UIScrollViewDelegate
>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic, strong) NSArray <UIViewController *> *vcArray;
@property (nonatomic, strong) UIView *headerView;//头视图
@property (nonatomic, assign) CGFloat headerHeight;//头视图的高度
@property (nonatomic, assign) CGFloat segmentHeight;//segment的高度
@property (nonatomic, assign) CGFloat headerMaxScrollHeight;//headerView最大的上移距离
@property (nonatomic, assign) CGFloat viewHeight;//self.view的高度

@end

@implementation SHViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // Do any additional setup after loading the view.
    self.automaticallyAdjustsScrollViewInsets = NO;
    self.navigationController.navigationBar.translucent = NO;
    [self.view addSubview:self.scrollView];
    
}

- (CGFloat)viewHeight {
    if (_viewHeight > 0) {
        return _viewHeight;
    }
    
    CGFloat height = SCREEN_HEIGHT;
    if (self.navigationController && self.navigationController.isNavigationBarHidden == NO) {
        height -= 64;
    }
    
    if (self.tabBarController.tabBar.isHidden == YES) {
        height -= 49;
    }
    
    _viewHeight = height;
    return _viewHeight;
}

- (UIScrollView *)scrollView {
    if (!_scrollView) {
        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, self.viewHeight)];
        _scrollView.showsHorizontalScrollIndicator = NO;
        _scrollView.pagingEnabled = YES;
        _scrollView.delegate = self;
    }
    return _scrollView;
}

- (void)addChildVCWithArray:(NSArray <UIViewController *> *)childVCArray
                 headerView:(UIView *)headerView
                segmentHeight:(CGFloat)segmentHeight {
    //滚动的头视图
    if (headerView) {
        [self.view addSubview:headerView];
        self.headerView = headerView;
        self.headerHeight = CGRectGetHeight(headerView.frame);
        self.segmentHeight =  segmentHeight;
        self.headerMaxScrollHeight = self.headerHeight - self.segmentHeight;
    }
    
    if (!childVCArray || childVCArray.count <= 0) {
        return;
    }
    //scrollview的contentSize
    self.scrollView.contentSize = CGSizeMake(SCREEN_WIDTH * childVCArray.count, self.viewHeight);
    //需要左右滚动的segmentVC
    self.vcArray = childVCArray;
    WEAKSELF
    [childVCArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        UIViewController* childVC = (UIViewController *)obj;
        childVC.view.frame = CGRectMake(SCREEN_WIDTH * idx, CGRectGetMinY(childVC.view.frame), SCREEN_WIDTH, CGRectGetHeight(childVC.view.frame));
        [weakSelf.scrollView addSubview:childVC.view];
        [weakSelf addChildViewController:childVC];
        UIScrollView *scrollView = [weakSelf getScrollViewWithVC:childVC];
        
        [scrollView addObserver:weakSelf forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial context:nil];
    }];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
    UIScrollView *scrollView = object;
    CGFloat offsetY = scrollView.contentOffset.y;
    if ([keyPath isEqualToString:@"contentOffset"]) {
        //headerview的frame变化
        if (offsetY >= self.headerMaxScrollHeight) {
            if (CGRectGetMinY(self.headerView.frame) != -self.headerMaxScrollHeight) {
                self.headerView.frame = CGRectMake(0, - self.headerMaxScrollHeight, SCREEN_WIDTH, self.headerHeight);
                
            }}else if (offsetY >= 0 && offsetY < self.headerMaxScrollHeight) {
                
                self.headerView.frame = CGRectMake(0, - offsetY, SCREEN_WIDTH, self.headerHeight);
            }else if (offsetY < 0) {
//                self.headerView.frame = CGRectMake(SCREEN_WIDTH * offsetY / self.headerHeight /2.0,0, SCREEN_WIDTH * (self.headerHeight - offsetY)/self.headerHeight, self.headerHeight - offsetY);//头视图随着拉伸变大
                self.headerView.frame = CGRectMake(0, -offsetY, SCREEN_WIDTH, self.headerHeight);
            }
    
        //各个vc中scrollView的frame变化
        WEAKSELF
        [self.vcArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            UIViewController* childVC = (UIViewController *)obj;
            UIScrollView *scrollView = [weakSelf getScrollViewWithVC:childVC];
            if (offsetY >= weakSelf.headerMaxScrollHeight) {
                if (scrollView.contentOffset.y < weakSelf.headerMaxScrollHeight)
                    scrollView.contentOffset = CGPointMake(0, weakSelf.headerMaxScrollHeight);
            }else if (offsetY >= 0 && offsetY < weakSelf.headerMaxScrollHeight) {
                if(scrollView.contentOffset.y != offsetY)
                    scrollView.contentOffset = CGPointMake(0, offsetY);
            }else if (offsetY < 0) {
                if (scrollView.contentOffset.y > 0)
                    scrollView.contentOffset = CGPointMake(0, 0);
            }
        }];

    }

}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    if (self.viewControllerScrollToIndex) {
        NSInteger index = scrollView.contentOffset.x / SCREEN_WIDTH;
        self.viewControllerScrollToIndex(index);
    }
}

- (UIScrollView *)getScrollViewWithVC:(UIViewController *)vc {
    for (UIView *tempView in vc.view.subviews) {
        if ([tempView isKindOfClass:[UIScrollView class]]) {
            return (UIScrollView *)tempView;
        }
    }
    
    return nil;
}

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

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,618评论 4 59
  • 代码创建UIWindow对象 Xcode7之后使用代码创建UIWindow对象: //创建UIWindow对象 s...
    云之君兮鹏阅读 1,260评论 0 2
  • 每天上班在地铁里总看到许多人都低着头在看手机、平板电脑、kindle,出于无聊,我暗暗观察了这些低头族,主要...
    柠哥说书阅读 786评论 0 6
  • 文/冬月之恋 柳塘镇通往外界有两条路。一条蜿蜒的石板路连着镇西的公路,公路往西北延伸约二十公里便到达县城,县城再一...
    冬月之恋阅读 370评论 1 5