兩種方式:
1.在原 UItabBar 樣式的基礎(chǔ)上擴(kuò)展
2.完全自定義 UITabBar 的樣式
效果預(yù)覽:
有4個(gè) tabBarItem,中間按鈕凸起一丟丟,原生半透明的背景。
簡(jiǎn)介
目前市場(chǎng)上的大部分 App UI 結(jié)構(gòu)都是使用這種標(biāo)簽式的結(jié)構(gòu),我覺(jué)得它至少有以下優(yōu)點(diǎn):
- 很清晰的展示了 App 的主要入口和當(dāng)前所在入口
- 各個(gè)入口間的切換也很簡(jiǎn)單,適合頻繁切換
缺點(diǎn):
- 入口個(gè)數(shù)不能太多(最好5個(gè)以下)
層級(jí)結(jié)構(gòu)
下面討論兩種層級(jí)結(jié)構(gòu)
1.tabBarController 里嵌 navigationController
2.navigationController 里嵌 tabBarController
可以很明顯看出這兩種結(jié)構(gòu)的區(qū)別(是否共用 navigationController)。我建議使用 tabBarController 里嵌 navigationController 結(jié)構(gòu),原因如下:
1.我不確定共用一個(gè) navigationController 是不是優(yōu)點(diǎn)?!但是缺點(diǎn)很明顯,因?yàn)榇蟛糠智闆r各個(gè) UIViewController 的 navigationBar 內(nèi)容是不一樣的(有的隱藏,有的有搜索框等),所以每次切換都需要改 navigationBar 的內(nèi)容。
2.蘋(píng)果官方也是建議使用 tabBarController 里嵌 navigationController 這種結(jié)構(gòu),在《View Controller Catalog for iOS》中的原話(huà):
An app that uses a tab bar controller can also use navigation controllers in one or more tabs. When combining these two types of view controller in the same user interface, the tab bar controller always acts as the wrapper for the navigation controllers.
開(kāi)始
正常搭建框架。因?yàn)檫@不是本文的重點(diǎn),也不難,所以可以直接下載源碼。
運(yùn)行效果如下:
本例要達(dá)到的效果是在中間插入一個(gè) button 并凸起一丟丟,如下:
方式一:在原 UITabBar 樣式的基礎(chǔ)上擴(kuò)展
使用原生的 tabBarItem,在這基礎(chǔ)上添加自定義控件
思路:
1.往 tabBar 上添加自定義的控件
2.重新布局 tabBarItem 和 自定義控件
3.使用自定義的 tabBar
動(dòng)手
第一步 新建 UITabBar 的子類(lèi) MSCustomTabBar
@interface MSCustomTabBar : UITabBar
第二步 在 MSCustomTabBar.m 里創(chuàng)建并添加中間按鈕(本例中中間按鈕比較簡(jiǎn)單使用 UIButton 就可以了,如果復(fù)雜點(diǎn)的建議新建一個(gè)類(lèi))
定義屬性
@interface MSExtensionalTabBar ()
/// 中間凸起的按鈕
@property (nonatomic, strong) UIButton *centerBtn;
@end
重寫(xiě) getter 方法
- (UIButton *)centerBtn
{
if (_centerBtn == nil) {
_centerBtn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 80, 60)];
[_centerBtn setImage:[UIImage imageNamed:@"centerIcon"] forState:UIControlStateNormal];
[_centerBtn addTarget:self action:@selector(clickCenterBtn:) forControlEvents:UIControlEventTouchUpInside];
}
return _centerBtn;
}
第三步 重新布局 tabBarItem 和 自定義控件
重寫(xiě) layoutSubviews
- (void)layoutSubviews
{
[super layoutSubviews];
// 把 tabBarButton 取出來(lái)(把 tabBar 的 subViews 打印出來(lái)就明白了)
NSMutableArray *tabBarButtonArray = [NSMutableArray array];
for (UIView *view in self.subviews) {
if ([view isKindOfClass:NSClassFromString(@"UITabBarButton")]) {
[tabBarButtonArray addObject:view];
}
}
CGFloat barWidth = self.bounds.size.width;
CGFloat barHeight = self.bounds.size.height;
CGFloat centerBtnWidth = CGRectGetWidth(self.centerBtn.frame);
CGFloat centerBtnHeight = CGRectGetHeight(self.centerBtn.frame);
// 設(shè)置中間按鈕的位置,居中,凸起一丟丟
self.centerBtn.center = CGPointMake(barWidth / 2, barHeight - centerBtnHeight/2 - 5);
// 重新布局其他 tabBarItem
// 平均分配其他 tabBarItem 的寬度
CGFloat barItemWidth = (barWidth - centerBtnWidth) / tabBarButtonArray.count;
// 逐個(gè)布局 tabBarItem,修改 UITabBarButton 的 frame
[tabBarButtonArray enumerateObjectsUsingBlock:^(UIView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) {
CGRect frame = view.frame;
if (idx >= tabBarButtonArray.count / 2) {
// 重新設(shè)置 x 坐標(biāo),如果排在中間按鈕的右邊需要加上中間按鈕的寬度
frame.origin.x = idx * barItemWidth + centerBtnWidth;
} else {
frame.origin.x = idx * barItemWidth;
}
// 重新設(shè)置寬度
frame.size.width = barItemWidth;
view.frame = frame;
}];
// 把中間按鈕帶到視圖最前面
[self bringSubviewToFront:self.centerBtn];
}
第四步 使用自定義的 tabBar
在 MSTabBarController.m 里使用自定義的 tabBar
- (void)viewDidLoad {
[super viewDidLoad];
// 利用KVO來(lái)使用自定義的tabBar
[self setValue:[[MSCustomTabBar alloc] init] forKey:@"tabBar"];
[self addAllChildViewController];
}
到這就已經(jīng)達(dá)到我們的效果了,可以運(yùn)行一下看看。
不過(guò),本例中有一個(gè)需要額外解決的問(wèn)題,還記得那個(gè)凸起一丟丟的 button 嗎,那一丟丟是接收不到點(diǎn)擊事件的,需要在 MSCustomTabBar.m 里重寫(xiě) hitTest:withEvent: 方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.clipsToBounds || self.hidden || (self.alpha == 0.f)) {
return nil;
}
UIView *result = [super hitTest:point withEvent:event];
// 如果事件發(fā)生在 tabbar 里面直接返回
if (result) {
return result;
}
// 這里遍歷那些超出的部分就可以了,不過(guò)這么寫(xiě)比較通用。
for (UIView *subview in self.subviews) {
// 把這個(gè)坐標(biāo)從tabbar的坐標(biāo)系轉(zhuǎn)為 subview 的坐標(biāo)系
CGPoint subPoint = [subview convertPoint:point fromView:self];
result = [subview hitTest:subPoint withEvent:event];
// 如果事件發(fā)生在 subView 里就返回
if (result) {
return result;
}
}
return nil;
}
想知道原理請(qǐng)看我的另一篇文章 iOS 點(diǎn)擊事件分發(fā)機(jī)制 。
到這本例就已經(jīng)完成了,完整源碼。
方式二:完全自定義 UITabBar 的樣式
tabBar 上的內(nèi)容完全自定義
思路:
1.自定義一個(gè) View 覆蓋整個(gè) tabBar
2.手動(dòng)實(shí)現(xiàn) tabBarItem 的功能
3.使用自定義的 tabBar
動(dòng)手
第一步 新建一個(gè) UIView 的子類(lèi) MSTabBarView,繪制 TabBar 上的內(nèi)容并實(shí)現(xiàn) tabBarItem 的功能
@interface MSTabBarView : UIView
繪制tabBar上的內(nèi)容(本文使用xib來(lái)繪制)
view 上是5個(gè) button,中間 button 只設(shè)置了 image 屬性,其他4個(gè) button 的 default 狀態(tài)對(duì)應(yīng) tabBarItem 的未選中狀態(tài),highlight 狀態(tài)和 disable 狀態(tài)對(duì)應(yīng) tabBarItem 的選中狀態(tài)。UIButton 的 image 和 title 是水平方向居中排列的,本例中應(yīng)該是垂直方向居中排列的,想通過(guò)設(shè)置 imageEdgeInsets 和 titleEdgeInsets 來(lái)達(dá)到這個(gè)效果的前提是 UIButton 的寬度固定,不同屏幕的寬度可能是不同的,所以需要新建 UIButton 的子類(lèi)來(lái)重載 layoutSubviews 方法來(lái)重新布局 UIButton 里面的內(nèi)容。
創(chuàng)建 UIButton 的子類(lèi)
@interface MSVerticalCenterButton : UIButton
重寫(xiě) layoutSubviews 方法,重新布局 button 的內(nèi)容
-(void)layoutSubviews {
[super layoutSubviews];
// 圖片居中
CGPoint center = self.imageView.center;
center.x = self.frame.size.width/2;
center.y = self.imageView.frame.size.height/2+5;
self.imageView.center = center;
// 文字居中
CGRect newFrame = [self titleLabel].frame;
newFrame.origin.x = 0;
newFrame.origin.y = CGRectGetMaxY(self.imageView.frame) + 2;
newFrame.size.width = self.frame.size.width;
self.titleLabel.frame = newFrame;
self.titleLabel.textAlignment = NSTextAlignmentCenter;
}
實(shí)現(xiàn) tabBarItem 的功能
由于切換 viewController 需要 tabBarController 來(lái)做,所以聲明一個(gè)協(xié)議,把點(diǎn)擊事件代理給 tabBarController 來(lái)處理
@class MSTabBarView;
@protocol MSTabBarViewDelegate <NSObject>
- (void)msTabBarView:(MSTabBarView *)view didSelectItemAtIndex:(NSInteger)index;
- (void)msTabBarViewDidClickCenterItem:(MSTabBarView *)view;
@end
@property (nonatomic, weak) id<MSTabBarViewDelegate> viewDelegate;
實(shí)現(xiàn)點(diǎn)擊事件(包括改變 button 的樣式和切換視圖)
定義一個(gè)屬性來(lái)記住當(dāng)前選擇的位置
/// 當(dāng)前選中的位置
@property (nonatomic, assign) NSInteger selectIndex;
自定義它的 setter 方法
- (void)setSelectIndex:(NSInteger)selectIndex
{
// 先把上次選擇的item設(shè)置為可用
UIButton *lastItem = _items[_selectIndex];
lastItem.enabled = YES;
// 再把這次選擇的item設(shè)置為不可用
UIButton *item = _items[selectIndex];
item.enabled = NO;
_selectIndex = selectIndex;
}
實(shí)現(xiàn) button 的點(diǎn)擊事件
- (IBAction)selectItem:(UIButton *)sender
{
// button的tag對(duì)應(yīng)tabBarController的selectedIndex
// 設(shè)置button的樣式
self.selectIndex = sender.tag;
// 讓代理來(lái)處理切換viewController的操作
if ([self.viewDelegate respondsToSelector:@selector(msTabBarView:didSelectItemAtIndex:)]) {
[self.viewDelegate msTabBarView:self didSelectItemAtIndex:sender.tag];
}
}
實(shí)現(xiàn)中間 button 的點(diǎn)擊事件(一般情況下中間按鈕的功能和其他按鈕是有區(qū)別的,所以使用單獨(dú)的一個(gè)方法來(lái)實(shí)現(xiàn)比較好)
- (IBAction)clickCenterItem:(id)sender
{
// 讓代理來(lái)處理點(diǎn)擊中間button的操作
if ([self.viewDelegate respondsToSelector:@selector(msTabBarViewDidClickCenterItem:)]) {
[self.viewDelegate msTabBarViewDidClickCenterItem:self];
}
}
第二步 新建 UITabBar 的子類(lèi) MSCustomTabBar,使用自定義的 MSTabBarView來(lái)覆蓋在 tabBar 上
創(chuàng)建 UITabBar 的子類(lèi) MSCustomTabBar
@interface MSCustomTabBar : UITabBar
使用 tabBarView 來(lái)覆蓋 tabBar 的內(nèi)容
在 .h 文件定義屬性(在 tabBarController 那里會(huì)用到),重寫(xiě) getter 方法
@property (nonatomic, strong) MSTabBarView *tabBarView;
- (MSTabBarView *)tabBarView
{
if (_tabBarView == nil) {
// xib的加載方式
_tabBarView = [[[NSBundle mainBundle] loadNibNamed:@"MSTabBarView" owner:nil options:nil] lastObject];
}
return _tabBarView;
}
添加到 tabBar 里面并覆蓋 tabBar 的內(nèi)容
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.tabBarView];
}
return self;
}
- (void)layoutSubviews
{
[super layoutSubviews];
// 設(shè)置 tabBarView 的 frame
self.tabBarView.frame = self.bounds;
// 把 tabBarView 帶到最前面,覆蓋 tabBar 的內(nèi)容
[self bringSubviewToFront:self.tabBarView];
}
第三步 在 MSCustomTabBarController 里使用自定義的 MSCustomTabBar,并實(shí)現(xiàn) MSTabBarView 的代理方法
使用自定義的 MSCustomTabBar
- (void)viewDidLoad {
[super viewDidLoad];
// 利用 KVC 來(lái)使用自定義的tabBar;
MSCustomTabBar *tabBar = [[MSCustomTabBar alloc] init];
tabBar.tabBarView.viewDelegate = self;
[self setValue:tabBar forKey:@"tabBar"];
[self addAllChildViewController];
}
實(shí)現(xiàn) MSTabBarView 的代理方法
- (void)msTabBarView:(MSTabBarView *)view didSelectItemAtIndex:(NSInteger)index
{
// 切換到對(duì)應(yīng)index的viewController
self.selectedIndex = index;
}
由于需要用到 tabBar 半透明(毛玻璃效果)的背景,所以 tabBarView 的背景是透明的,同樣不顯示 tabBarItem 的內(nèi)容(不設(shè)置 title 和 image )。
把設(shè)置 tabBarItem 內(nèi)容的相關(guān)代碼注釋掉
- (void)addChildViewController:(UIViewController *)vc title:(NSString *)title imageNamed:(NSString *)imageNamed
{
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
// 如果同時(shí)有 navigationbar 和 tabbar 的時(shí)候最好分別設(shè)置它們的 title
vc.navigationItem.title = title;
// nav.tabBarItem.title = title;
// nav.tabBarItem.image = [UIImage imageNamed:imageNamed];
[self addChildViewController:nav];
}
到這就已經(jīng)達(dá)到我們的效果了,可以運(yùn)行一下看看。
這里我們同樣需要處理凸起的那一丟丟接收不到點(diǎn)擊事件的問(wèn)題,在 MSCustomTabBar.m 里重寫(xiě) hitTest:withEvent: 方法
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (self.clipsToBounds || self.hidden || (self.alpha == 0.f)) {
return nil;
}
UIView *result = [super hitTest:point withEvent:event];
// 如果事件發(fā)生在 tabbar 里面直接返回
if (result) {
return result;
}
// 這里遍歷那些超出的部分就可以了,不過(guò)這么寫(xiě)比較通用。
for (UIView *subview in self.tabBarView.subviews) {
// 把這個(gè)坐標(biāo)從 tabbar 的坐標(biāo)系轉(zhuǎn)為 subview 的坐標(biāo)系
CGPoint subPoint = [subview convertPoint:point fromView:self];
result = [subview hitTest:subPoint withEvent:event];
// 如果事件發(fā)生在 subView 里就返回
if (result) {
return result;
}
}
return nil;
}
想知道原理請(qǐng)看我的另一篇文章 iOS 點(diǎn)擊事件分發(fā)機(jī)制。
到這本例就已經(jīng)完成了,完整源碼。
總結(jié)
推薦使用 tabBarController 里面嵌 navigationController 這樣的層級(jí)結(jié)構(gòu)。
在原 UItabBar 樣式的基礎(chǔ)上擴(kuò)展樣式就是往 tabBar 上添加擴(kuò)展的樣式,然后重載 layoutSubviews 方法重新布局 tabBar 的內(nèi)容。
完全自定義 UITabBar 的樣式就是使用自定義的視圖覆蓋在 tabBar 上,然后自己實(shí)現(xiàn) tabBarItem 的功能。
tip:如果點(diǎn)下 tabBar 上的按鈕不松手就切換 viewController 的很有可能是使用原生的 tabBarItem(比如本文中擴(kuò)展的方式、微信),如果按下時(shí)有高亮效果或者松手后才切換 viewController 的就是使用自定義的 button(比如本文中完全自定義的方式、京東)。