项目中经常会用到这样的页面结构,页面顶部有tab栏,点击能切换到对应的页面(有滑动效果),下方的页面也能够拖动:
当页面变多时,tab栏也能够拖动了,并且当页面滑动使tab栏的头部或尾部tab选中的时候,tab栏还会根据情况自动滑动:
还有可能有其他的效果,比如选中的tab下方有下划线,下划线的长度在页面滑动过程中会根据两个tab的长度实时的变化,还有tab的文字颜色也会根据页面的滑动变化,等等。
因此,封装了一个滑动切换页面的框架,能够应对这种页面结构的大多数需求。最简单的使用如下:
// 创建tab栏对应的title数组
NSArray<NSString *> *titleArr = @[@"腾讯", @"蚂蚁金服", @"YY", @"网易"];
// 创建页面对应的子控制器数组
NSMutableArray<UIViewController *> *childVcArr = [NSMutableArray new];
for (int i = 0; i < titleArr.count; i++) {
UIViewController *vc = [UIViewController new];
vc.view.backgroundColor = [UIColor whiteColor];
UILabel *label = [UILabel lzs_labelWithText:titleArr[i] font:[UIFont systemFontOfSize:14] color:[UIColor redColor]];
label.center = vc.view.center;
[vc.view addSubview:label];
[childVcArr addObject:vc];
}
// tab栏的样式
LZSTitleStyle *style = [LZSTitleStyle new];
// 创建pageView
LZSPageView *pageView = [[LZSPageView alloc] initWithFrame:self.view.bounds titleArr:titleArr style:style childViewControllers:childVcArr parentViewController:self];
[self.view addSubview:pageView];
页面如下:
LZSPageView负责tab栏和页面的交互,以及tab栏的样式。所以需要四个参数构建pageView:
1.tab栏对应的title数组
2.页面对应的子控制器数组
3.负责tab栏样式的对象( LZSTitleStyle)
4.页面对应的子控制器数组的父控制器
这样就得到了一个pageView。具体的页面的业务逻辑,还是写在页面对应的子控制器的类中。
其中LZSTitleStyle这个类,是用来自定义tab栏样式的,如果你想使用框架的默认样式,那么创建一个LZSTitleStyle对象然后传进来就行了,如果想自定义样式,LZSTitleStyle提供如下的属性:
@interface LZSTitleStyle : NSObject
//titleView高度,默认44
@property(nonatomic,assign)float titleViewHeight;
//titleView的宽度,默认等于pageView宽度
@property(nonatomic,assign)float titleViewWidth;
//titleView的x值,当titleView的宽度等于pageView宽度时,一定为0,当titleView的宽度不能等于pageView宽度时,可以设置,不设置默认为0
@property(nonatomic,assign)float titleViewX;
//title未选中颜色,默认黑色
@property(nonatomic,strong)UIColor *normalColor;
//title选中颜色,默认蓝色
@property(nonatomic,strong)UIColor *selectColor;
//字体大小
@property(nonatomic,assign)float fontSize;
//标题栏不能滚动时titleLab宽度等于pageView宽度/title个数,可以滚动时titleLab宽度等于文字宽度,默认不能滚动
@property(nonatomic,assign)BOOL isScrollEnable;
//titleLab间距,标题栏不能滚动时一定为0,能滚动时可以设置间距,默认30,最左边距和最右边距为itemMargin/2
@property(nonatomic,assign)float itemMargin;
//是否显示下划线,默认显示
@property(nonatomic,assign)BOOL isShowScrollLine;
//标题栏可以滚动时下划线宽度一定会等于文字的宽度,不能滚动时可以设置下划线宽度,默认等于titleLab的宽度
@property(nonatomic,assign)float scrollLineWidth;
//下划线高度,默认2
@property(nonatomic,assign)float scrollLineHeight;
//下划线颜色,默认蓝色
@property(nonatomic,strong)UIColor *scrollLineColor;
@end
以上的LZSPageView的使用是将tab栏和子页面当做一个整体来看的。但是有时候,tab栏和子页面可能并不适合当做一个整体,就像刚开始这张图:
实际上,tab栏是布局在导航栏上面的,像这样子
self.navigationItem.titleView = titleView;
所以这个时候就不能直接创建出pageView了。LZSPageView也提供更加灵活的创建方式来应对这种tab栏和子页面分开的情况:
// 创建LZSTitleStyle,设置属性
LZSTitleStyle *style = [LZSTitleStyle new];
style.titleViewHeight = 44;
style.titleViewWidth = 240;
style.titleViewX = 0;
style.normalColor = k3A3D48;
style.selectColor = k4C72F5;
style.fontSize = 15;
style.scrollLineWidth = 30;
style.scrollLineHeight = 2.5;
style.scrollLineColor = k4C72F5;
// 创建LZSTitleView
self.titleView = [[LZSTitleView alloc] initWithFrame:CGRectMake(0, 0, style.titleViewWidth, style.titleViewHeight) titleArr:@[@"成长故事",@"企业文化",@"表彰文化"] style:style];
self.navigationItem.titleView = self.titleView;
// 创建LZSContentView
self.childVcArr = @[[self setupGrowthCourseVc],[self setupEnterpriseCultureVc],[self setupPraiseCultureVc]];
self.contentView = [[LZSContentView alloc] initWithFrame:CGRectMake(0, 0, self.view.width, self.view.height-kStatusBarHeight-self.navigationController.navigationBar.height) childViewControllers:self.childVcArr parentViewController:self];
[self.view addSubview:self.contentView];
// 关键,互相设置代理
self.titleView.delegate = self.contentView;
[self.contentView addDelegate:self.titleView];
就是分别创建包含tab栏的LZSTitleView和包含子页面的LZSContentView,然后两个view互相成为代理。这样,你就可以单独拿到titleView和contentView,而titleView和contentView之间的交互逻辑还是通过框架来解决。
这里LZSContentView设置代理的方式是使用addDelegate这个方法,而不是提供一个delegate属性去设置。因为可能还有其他地方需要监听LZSContentView的滚动,提供一个delegate属性就只能给titleView使用了,其他地方就监听不了LZSContentView的滚动。而addDelegate的方法可以让多个类成为LZSContentView的代理。内部实现是使用一个数组去记录这些delegate,然后需要调用的时候遍历数组调用delegate的方法,这里使用的这个数组不能使NSMutableArray,因为NSMutableArray会对它的元素强引用,OC提供了NSPointerArray这个类,这个类对它的元素是弱引用。
这个框架还对子控制器的view的加载时机(viewDidLoad这个方法)做了处理。LZSContentView的内部使用一个UICollectionView去放置子控制器的view的,不过子控制器的view并不会在cell一出现在界面的时候就加载,而会等到collectionView停止滚动的时候再加载。这样做的目的是,如果cell一出现在界面就加载子控制器的view(也就是走viewDidLoad方法),如果子页面很多,而我是通过点击tab栏去切换子页面的,那么刚开始我直接点击tab栏很后面的tab时,就会滑过中间很多界面,这些界面可能是用户不想看的,然后这些页面就都走了viewDidLoad方法,这样既会造成滑动动画的卡顿,也会造成不必要的页面的加载:
相关代码如下:
@interface LZSContentView ()<UICollectionViewDataSource,UICollectionViewDelegate>
@property(nonatomic,strong)NSMutableArray<NSNumber *> *isLoadViewArr;
@end
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
LZSContentCollectionViewCell *cell = [_collectionView dequeueReusableCellWithReuseIdentifier:@"contentCollectionViewCell" forIndexPath:indexPath];
if (indexPath.row == 0) {
cell.vc = _childVcArr[indexPath.row];
_isLoadViewArr[0] = @1;
} else {
NSNumber *number = _isLoadViewArr[indexPath.row];
if (number.integerValue) {
cell.vc = _childVcArr[indexPath.row];
} else {
cell.vc = nil;
}
}
return cell;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSNumber *number = _isLoadViewArr[indexPath.row];
if (!number.integerValue) {
cell.vc = _childVcArr[indexPath.row];
_isLoadViewArr[indexPath.row] = @1;
}
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
NSNumber *number = _isLoadViewArr[indexPath.row];
if (!number.integerValue) {
cell.vc = _childVcArr[indexPath.row];
_isLoadViewArr[indexPath.row] = @1;
}
}
就是使用一个数组记录子控制的view是否已经加载过了,如果没有加载过,那么在- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath里设置 cell.vc = nil。如果已经加载过了,则设置cell.vc = _childVcArr[indexPath.row]。在- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView和- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView里判断当前页面子控制的view是否已经加载过了,如果没有加载,设置 cell.vc = _childVcArr[indexPath.row]。