一. 主流界面
1. 第一种主流界面
iOS项目主流界面是将UITabBarController作为Window的根控制器,UITabBarController的viewControllers数组里面存放包装着UINavigationController,UINavigationController的根控制器是子控制器。
我们按照主流界面的搭建方式创建项目,页面层级如下:可以看出UITabBarController是父控制器,UINavigationController是子控制器。
为了验证,我们在子控制器里写如下代码:
UIViewController *vc1 = self.navigationController;
UIViewController *vc2 = self.tabBarController;
UIViewController *vc3 = [self.navigationController parentViewController];
UIViewController *vc4 = [[self.tabBarController childViewControllers] firstObject];
NSArray *arr1 = self.navigationController.viewControllers;
NSArray *arr2 = self.tabBarController.viewControllers;
打断点,信息如下:可以发现:vc1和vc4地址相同,vc2和vc3地址相同,验证了UITabBarController是最终的父控制器。
2. 第二种主流界面
第二种方式是将UINavigationController作为Window的根控制器,UITabBarController作为UINavigationController的跟控制器,UITabBarController的viewControllers数组里面存放着viewController(不包装UINavigationController)。
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
MainMenuTabBarVCtr *mainMenuTabBarVCtr = [[MainMenuTabBarVCtr alloc] init];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:mainMenuTabBarVCtr];
self.window.rootViewController = nav;
[self.window makeKeyAndVisible];
页面层级如下,这个就不验证了。
总结:
- 对于第一种界面: window的根控制器是tabBarController, tabBarController管理四个导航控制器,四个导航控制器的根控制器分别是他们的子控制器, 切换tabar的时候只是在四个导航控制器之间切换, 在子页面通过self.navigationController分别获取的是他们自己的导航控制器, 然后分别用他们自己的导航控制器进行push操作, 所以父控制器tabBarController底部的tabbar会存在, 需要设置viewController.hidesBottomBarWhenPushed = YES。
由于子页面的父控制器分别是四个导航控制器,所以设置标题的时候可以:
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"首页";
}
- 对于第二种界面: window的跟控制器是导航控制器, 导航控制器的根控制器是tabBarController, tabBarController管理着四个子控制器, 在子页面通过self.navigationController获取的是唯一一个导航控制器, 所有界面都是通过这一个导航控制器进行push操作, 所以子页面进行push的时候连tabBarController也一块push掉了, 不用设置viewController.hidesBottomBarWhenPushed = YES。
由于子页面的父控制器都是一个tabBarController,所以设置标题的时候可以:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
self.tabBarController.title = @"分类";
}
二. 自定义tabbar的方式
自定义tabbar的方式目前有三种:
1. 自定义tabbar继承于系统的UITabBar
老版微博tabbar界面如下图:中间的➕按钮用系统的肯定实现不了, 这时候我们就要自定义tabbar, 自定义tabbar继承于系统的UITabBar, 代码如下:
XUTabBar.h文件
#import <UIKit/UIKit.h>
@class XUTabBar;
#warning 因为XUTabBar继承自UITabBar,所以称为XUTabBar的代理,也必须实现UITabBar的代理协议
@protocol XUTabBarDelegate <UITabBarDelegate>
@optional
- (void)tabBarDidClickPlusButton:(XUTabBar *)tabBar;
@end
@interface XUTabBar : UITabBar
@property (nonatomic, weak) id<XUTabBarDelegate> delegate;
@end
XUTabBar.m文件
#import "XUTabBar.h"
@interface XUTabBar()
@property (nonatomic, weak) UIButton *plusBtn;
@end
@implementation XUTabBar
- (id)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
//添加一个➕按钮
UIButton *plusBtn = [[UIButton alloc] init];
[plusBtn setBackgroundImage:[UIImage imageNamed:@"tabbar_compose_button"] forState:UIControlStateNormal];
[plusBtn setBackgroundImage:[UIImage imageNamed:@"tabbar_compose_button_highlighted"] forState:UIControlStateHighlighted];
[plusBtn setImage:[UIImage imageNamed:@"tabbar_compose_icon_add"] forState:UIControlStateNormal];
[plusBtn setImage:[UIImage imageNamed:@"tabbar_compose_icon_add_highlighted"] forState:UIControlStateHighlighted];
plusBtn.size = plusBtn.currentBackgroundImage.size;
[plusBtn addTarget:self action:@selector(plusClick) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:plusBtn];
self.plusBtn = plusBtn;
}
return self;
}
/**
* 加号按钮点击
*/
- (void)plusClick
{
// 通知代理
if ([self.delegate respondsToSelector:@selector(tabBarDidClickPlusButton:)]) {
[self.delegate tabBarDidClickPlusButton:self];
}
}
//重新排列按钮的位置
- (void)layoutSubviews
{
[super layoutSubviews];
//设置➕按钮的位置
self.plusBtn.centerX = self.width * 0.5;
self.plusBtn.centerY = self.height * 0.5;
//设置其他按钮的位置
CGFloat tabbarButtonW = self.width / 5;
CGFloat tabbarButtonIndex = 0;
for (UIView *child in self.subviews) {
Class class = NSClassFromString(@"UITabBarButton");
if ([child isKindOfClass:class]) {
child.width = tabbarButtonW;
child.x = tabbarButtonW * tabbarButtonIndex;
tabbarButtonIndex ++;
//中间按钮的时候直接跳过
if (tabbarButtonIndex == 2) {
tabbarButtonIndex ++;
}
}
}
}
@end
在XUTabBarViewController.m里面设置, 代码如下:
//更换tabBar
XUTabBar *tabBar = [[XUTabBar alloc] init];
[self setValue:tabBar forKeyPath:@"tabBar"];
/*
[self setValue:tabBar forKeyPath:@"tabBar"];相当于self.tabBar = tabBar;
[self setValue:tabBar forKeyPath:@"tabBar"];这行代码过后,tabBar的delegate就是XUTabBarViewController
说明,不用再设置tabBar.delegate = self;
*/
/*
1.如果tabBar设置完delegate后,再执行下面代码修改delegate,就会报错
tabBar.delegate = self;
2.如果再次修改tabBar的delegate属性,就会报下面的错误
错误信息:Changing the delegate of a tab bar managed by a tab bar controller is not allowed.
错误意思:不允许修改TabBar的delegate属性(这个TabBar是被TabBarViewController所管理的)
*/
这样就实现了自定义微博的➕按钮。
2. 移除系统tabbar所有子控件,重新添加控件
首先自定义TabBarButtton继承于UIButton:
#import "TabBarButtton.h"
@implementation TabBarButtton
- (void)layoutSubviews{
[super layoutSubviews];
float imageWidth = 64;
float imageheight = 50;
self.titleLabel.hidden = YES;
[self.imageView setFrame:CGRectMake(0, (self.frame.size.width-imageWidth)/2, imageWidth, imageheight)];
}
@end
然后自定义tabBar继承于UIView:
- (void)createEocTabBar{
float tabbarWidth = self.tabBar.frame.size.width;
float tabbarHeight = self.tabBar.frame.size.height;
_eocTabBar = [[UIView alloc] initWithFrame:CGRectMake(0, 0, tabbarWidth, tabbarHeight)];
_eocTabBar.backgroundColor = [UIColor colorWithRed:240/255.0 green:240/255.0 blue:240/255.0 alpha:1];
float tabbarItemWidth = tabbarWidth/tabbarTitleAry.count;
for (NSInteger i = 0; i < tabbarTitleAry.count; i++){
TabBarButtton *tabbarBt = [[TabBarButtton alloc] init];
[tabbarBt setFrame:CGRectMake(i*tabbarItemWidth, 0, tabbarItemWidth, tabbarHeight)];
[tabbarBt addTarget:self action:@selector(selectMenuVC:) forControlEvents:UIControlEventTouchUpInside];
tabbarBt.tag = i;
[tabbarBt setBackgroundImage:[UIImage imageNamed:tabbarTitleImageAry[i]] forState:UIControlStateNormal];
[tabbarBt setBackgroundImage:[UIImage imageNamed:tabbarTitleHightImageAry[i]] forState:UIControlStateSelected];
[_eocTabBar addSubview:tabbarBt];
if (i == 0) {
_selectTabBarBt = tabbarBt;
_selectTabBarBt.selected = YES;
}
}
[self tabbarStyleOne];
//[self tabbarStyleTwo];
}
最后,移除系统tabar所有子控件,添加自定义tabar
//清空子控件
- (void)tabbarStyleOne{
// tabbar 原生子试图 all remove
NSArray *tabbarViewsAry = [self.tabBar subviews];
for (int i = 0; i < tabbarViewsAry.count; i++) {
UIView *view = tabbarViewsAry[I];
[view removeFromSuperview];
}
[self.tabBar insertSubview:_eocTabBar atIndex:0];
}
效果图如下:这是因为我们在子页面设置标题的时候直接设置self.title = @"购物车",在我们切换tabbar的时候会刷新tabbar,就多了两个UITabBarButton。
如何解决?不想让它刷新
我们可以直接操作导航条标题 self.navigationItem.title = @"购物车",或者按照下面这个方法,移除系统的tabbar,也会解决这个bug。
3. 移除系统的tabbar,将自定义的tabbar放到系统tabbar的位置
自定义tabar的代码如上不变,移除系统的tabbar
//直接移除
- (void)tabbarStyleTwo{
// 直接remove tabbar
UIView *superV = [self.tabBar superview];
_eocTabBar.frame = self.tabBar.frame;
[self.tabBar removeFromSuperview];
[superV addSubview:_eocTabBar];
}
这样会有另外一个问题:viewController.hidesBottomBarWhenPushed = YES; 这句代码没效果了,因为我们把系统的tabbar干掉了。
三. 启动页延迟的推荐方式
有时候我们有广告界面或者登陆界面,如果没有保存登录账号和密码,那么第一个界面是登录界面,如果保存了登录账号和密码,直接显示主页,这里涉及到切换主界面。
如果第一个界面不确定的话,不要修改window的根控制器,因为如果根控制器不确定,后面很多逻辑都要加 if 或者判断(打个比方,回到mainMenuTabBarVCtr的主页)。
解决方案:
- 判断是否需要展示push/present登录界面,如果需要展示就利用启动页延迟。
- 需要我们重新创建第二个window,第二个window的根控制器是启动页或者登录页面,要显示的时候先让第一个self.window调用makeKeyAndVisible,再让第二个window调用makeKeyAndVisible,这样第二个window就在第一个window上面盖着,事情做完之后再把第二个window移除。
代码如下:
#import "AppDelegate.h"
#import "MainMenuTabBarVCtr.h"
@interface AppDelegate (){
UIWindow *eocWindow;
}
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self styleOne];
[self delayLaunchImage];
return YES;
}
- (void)cancelLaunImage{
[eocWindow resignKeyWindow];
eocWindow = nil;
}
// 耦合度比较低
- (void)delayLaunchImage{
eocWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
eocWindow.rootViewController = [UIViewController new];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds];
[imageView setImage:[UIImage imageNamed:@"11.PNG"]];
[eocWindow addSubview:imageView];
[eocWindow makeKeyAndVisible];
[self performSelector:@selector(cancelLaunImage) withObject:nil afterDelay:5];
}
- (void)styleOne{
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
MainMenuTabBarVCtr *mainMenuTabBarVCtr = [[MainMenuTabBarVCtr alloc] init];
self.window.rootViewController = mainMenuTabBarVCtr;
[self.window makeKeyAndVisible];
}
@end
当我们把第二个window取消keyWindow的时候,self.window又会默认成为keyWindow,因为总要有一个是keyWindow,我们可以po进行验证。
(lldb) po self.window
<UIWindow: 0x7f8596c02300; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600001705740>; layer = <UIWindowLayer: 0x6000019093e0>>
(lldb) po eocWindow
<UIWindow: 0x7f8596e28210; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x60000172fdb0>; layer = <UIWindowLayer: 0x60000191d680>>
(lldb) po [UIApplication sharedApplication].keyWindow
<UIWindow: 0x7f8596e28210; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x60000172fdb0>; layer = <UIWindowLayer: 0x60000191d680>>
(lldb) po [UIApplication sharedApplication].keyWindow
<UIWindow: 0x7f8596c02300; frame = (0 0; 375 667); gestureRecognizers = <NSArray: 0x600001705740>; layer = <UIWindowLayer: 0x6000019093e0>>
四. MVVM和MVCC
关于架构,建议先看完iOS-架构再往下看。
1. MVVM
如果使用标准的MVC框架, 时间久了会发现C里面的代码特别多, 这时候你可能会想到添加工具类, 比如最常见的添加数据请求工具类。
- 在MVVM中, 一个View对应一个ViewModel, 它采用双向绑定:View的变动,自动反应到ViewModel上,ViewModel的变动,自动反应到View上, View和Model不再有任何沟通。
- MVVM并不是没有C,只是把C弱化了,关于MVVM可以看下图:
下面讲述MVVM怎么使用:
如下图,想要实现"推荐"这个界面, 标准的MVC代码是什么样的我就不需要多说了, 现在我们要使用MVVM拆分C的代码。
当前界面有两个主要的View,一个是self.view一个是tableView,按照MVVM结构,需要两个ViewModel,由于这个界面比较简单,写两个ViewModel又会把简单问题复杂化,我们这里只使用一个ViewModel,ViewModel代码如下:
RecommendViewModel.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class MessageModel;
typedef void(^finishLoadBlock)(id infoDict);
@interface RecommendViewModel : NSObject
@property(nonatomic, assign) NSInteger rowNumber;
- (CGFloat)messageHeightForRow:(NSInteger)row;
- (MessageModel*)messageModelForRow:(NSInteger)row;
- (NSString*)messageIdForRow:(NSInteger)row;
- (void)loadDatafromNetWithPage:(NSInteger)page finishNet:(finishLoadBlock)finishBlock;
- (void)deleteAdView:(UIView*)deleteView headView:(UIView*)headView tableview:(UITableView*)tableView;
- (void)pushMessageDetailIndex:(NSIndexPath *)indexPath viewCtr:(UIViewController*)targetVCtr;
@end
RecommendViewModel.m
#import "RecommendViewModel.h"
#import "MessageModel.h"
#import "MessageDetailViewCtr.h"
@interface RecommendViewModel (){
}
@property (nonatomic, strong)NSMutableArray *messageAry;
@end
@implementation RecommendViewModel
- (NSInteger)rowNumber{
return self.messageAry.count;
}
- (CGFloat)messageHeightForRow:(NSInteger)row{
if (row < self.messageAry.count){
MessageModel *messageModel = self.messageAry[row];
return messageModel.messageHeight;
}
return 0;
}
- (MessageModel*)messageModelForRow:(NSInteger)row{
if (row < self.messageAry.count) {
return self.messageAry[row];
}else{
NSLog(@"row越界messageAry:%ld--%ld", row, _messageAry.count);
}
return nil;
}
- (NSString*)messageIdForRow:(NSInteger)row{
if (row < self.messageAry.count) {
MessageModel *messageModel = self.messageAry[row];
return messageModel.messageId;
}else{
NSLog(@"row越界messageAry:%ld--%ld", row, _messageAry.count);
}
return nil;
}
- (void)deleteAdView:(UIView*)deleteView headView:(UIView*)headView tableview:(UITableView*)tableView{
[deleteView removeFromSuperview];
headView.frame = ({
CGRect frame = headView.frame;
frame.size.height = frame.size.height - deleteView.frame.size.height;
frame;
});
[tableView setTableHeaderView:nil];
[tableView setTableHeaderView:deleteView];
}
- (void)pushMessageDetailIndex:(NSIndexPath *)indexPath viewCtr:(UIViewController*)targetVCtr{
MessageDetailViewCtr *messageDetailVCtr = [[MessageDetailViewCtr alloc] initWithMessageID:[self messageIdForRow:indexPath.row]];
[targetVCtr.navigationController pushViewController:messageDetailVCtr animated:YES];
}
- (void)loadDatafromNetWithPage:(NSInteger)page finishNet:(finishLoadBlock)finishBlock{
for (int i = 0; i< 10; i++) {
MessageModel *messageModel = [MessageModel new];
messageModel.messageTitle = [NSString stringWithFormat:@"消息::%d", I];
[self.messageAry addObject:messageModel];
}
finishBlock(nil);
}
#pragma mark - lazy loading
- (NSMutableArray*)messageAry{
if (!_messageAry) {
_messageAry = [NSMutableArray array];
}
return _messageAry;
}
@end
推荐控制器的代码如下:
RecommendVCtr.h
#import <UIKit/UIKit.h>
/*
C 给弱化
*/
@interface RecommendVCtr : UIViewController{
IBOutlet UIView *_headView;
IBOutlet UIView *_isDeleteAdView;
IBOutlet UITableView *_tableView;
}
- (IBAction)removeSelectV:(UIButton*)sender;
@end
RecommendVCtr.m
#import "RecommendVCtr.h"
#import "RecommendCell.h"
#import "RecommendTopNewCell.h"
#import "RecommendViewModel.h"
#import "MessageDetailViewCtr.h"
#import "MessageModel.h"
#import "ReTableViewModel.h"
#import "ReMenuMoreVCtr.h"
@interface RecommendVCtr ()
@property (nonatomic, strong)RecommendViewModel *recomendViewModel;
@property (nonatomic, strong)ReTableViewModel *reTableViewModel;
@end
@implementation RecommendVCtr{
ReMenuMoreVCtr *_reMenuMoreVCtr;
}
- (void)viewDidLoad {
[super viewDidLoad];
//self addChildViewController:<#(nonnull UIViewController *)#>
self.automaticallyAdjustsScrollViewInsets = NO;
[_headView removeFromSuperview];
[_tableView setTableHeaderView:_headView];
//[self.reTableViewModel configTable:_tableView];
UITableView *tmpTableV = _tableView;
// 下载数据业务
[self.recomendViewModel loadDatafromNetWithPage:1 finishNet:^(id infoDict) {
[tmpTableV reloadData];
}];
}
- (void)viewDidLayoutSubviews{
[super viewDidLayoutSubviews];
[self.view bringSubviewToFront:_tableView];
_tableView.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height - SysNavHeigh - SysTabBarHeigh);
}
- (IBAction)removeSelectV:(UIButton*)sender{
[self.recomendViewModel deleteAdView:_isDeleteAdView headView:_headView tableview:_tableView];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
// return [RecommendCell cellHeight];
return 80.0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return self.recomendViewModel.rowNumber;
}
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
RecommendCell *cell = [tableView dequeueReusableCellWithIdentifier:@"RecommendCell"];
if (!cell) {
cell = [[[NSBundle mainBundle] loadNibNamed:@"RecommendCell" owner:nil options:nil] firstObject];
}
cell.messageModel = [self messageModelForRow:indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
[self.recomendViewModel pushMessageDetailIndex:indexPath viewCtr:self];
}
- (MessageModel*)messageModelForRow:(NSInteger)row{
// if (row < self.messageAry.count) {
// return self.messageAry[row];
// }else{
// NSLog(@"row越界messageAry:%ld--%ld", row, _messageAry.count);
// }
return nil;
}
#pragma mark - lazy loading
- (RecommendViewModel*)recomendViewModel{
if (!_recomendViewModel) {
_recomendViewModel = [RecommendViewModel new];
}
return _recomendViewModel;
}
- (ReTableViewModel*)reTableViewModel{
if (!_reTableViewModel) {
_reTableViewModel = [ReTableViewModel new];
}
return _reTableViewModel;
}
@end
可以发现,MVVM就是把所有关于View的操作交给ViewModel来做了(也有点MVC+工具类的感觉)这样就实现了控制器C代码的简化。
2. MVCC
MVCC是MVC加子控制器C的意思,比如上个界面我们想要把关于tableView的所有代码抽出来,可以创建一个tableVC,在tableVC里添加一个tableView,设置tableView的代理是tableVC,需要使用tableView的时候把tableVC的view添加到目标控制器上,并把tableVC设置为目标控制器的子控制器。
个人比较喜欢MVCC方式,反正无论什么方式,能合理清晰的抽出C的代码就可以。
其实在一些比较大的框架中,并不会强调什么MVC或者MVVM的,主要是站在业务的角度来拆分的,目标就是怎么合理怎么好怎么来,所以工作中要合理使用各种设计模式,不能太死板。
五. 路由
什么是路由?就是帮助我们进行页面跳转用的。
在一般的开发中,如果我们进行跳转,必须要导入目标控制器的头文件,然后进行跳转,如果当前界面需要跳转很多界面就会有很多头文件需要导入。
有了路由,我们只需要导入路由头文件就可以了,所有的跳转都在路由器里面处理,这样就能把业务转移,耦合度更低了。