iOS

写点更简洁的代码

最近在review整个项目的代码,因为代码量很大,参与开发的人很多,所以代码很多地方写得不够简洁。这里总结出一些代码片段,用来简化代码。

最近在review整个项目的代码,因为代码量很大,参与开发的人很多,所以代码很多地方写得不够简洁。这里总结出一些代码片段,用来简化代码。

1、让TableView多余的Cell不可见。
原来的实现:
给TableView增加一个空的FooterView。但是当很多地方都需有这个需求时,类似的代码就重复出现。

1
2
3
UIView *view = [UIView new];
view.backgroundColor = [UIColor clearColor];
[tableView setTableFooterView:view];

改进的实现:
增加一个UITableView的category,这样在调用的地方只需一行就够了。

1
2
3
4
5
6
7
8
@implementation UITableView(Addtions)

- (void)hideEmptyCells
{
UIView *view = [UIView new];
view.backgroundColor = [UIColor clearColor];
[self setTableFooterView:view];
}

使用

1
[self.tableView hideEmptyCells];

2、让TableView Cell之间的分隔线左间距为0
这个问题是iOS7之后才出现的,iOS7的解决方法到了iOS8又出问题,所以每次用到TableView, 都要给TableView设置一便,又要给TableViewCell设置一遍,代码很多很散。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//设置tableView
if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([self.tableView respondsToSelector:@selector(setLayoutMargins:)]) {
[self.tableView setLayoutMargins:UIEdgeInsetsZero];
}

//设置tableviewcell
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
[cell setSeparatorInset:UIEdgeInsetsZero];
}

if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}

改进的实现:
给UITableView和UITableViewCell各增加一个Category方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//UITableView
- (void)hideSeparatorLeftInset
{
if ([self respondsToSelector:@selector(setSeparatorInset:)])
{
[self setSeparatorInset:UIEdgeInsetsZero];
}

if ([self respondsToSelector:@selector(setLayoutMargins:)])
{
[self setLayoutMargins:UIEdgeInsetsZero];
}
}
//UITableViewCell
- (void)hideSeparatorLeftInset
{
if ([self respondsToSelector:@selector(setLayoutMargins:)]) {
[self setLayoutMargins:UIEdgeInsetsZero];
}
}

然后在创建UITableView和UITableViewCell的地方分别调用就好了。

1
2
3
4
5
6
7
8
9
10
11
12
//创建Table
_newsTable = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - 64 -49) style:UITableViewStylePlain];
_newsTable.delegate = self;
_newsTable.dataSource = self;
[_newsTable hideSeparatorLeftInset];

//创建Cell
if (cell == nil) {
cell = [[FindBigImageTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:simpleCell];
[cell setSelectionStyle:UITableViewCellSelectionStyleNone];
[cell hideSeparatorLeftInset];
}

这样做的好处:

  • 代码简洁,只要一行。因为在很多类似的页面都会出现重复的代码,所以不同的人写往往会不一致,造成bug。
  • 以后api升级,只要改一处地方即可,不用满世界的找代码然后修改。

总结:当你写代码的时候需要复制粘贴的时候,肯定是没有封装。想想能不能用Util方法和Category进行抽象,把不变的剥离出来,组成新的方法,这比复制粘贴好太多。

3、网络请求的封装
目前代码中的网络请求,是基于Http的,主要分成如下几个过程:
1、设置请求参数
2、调用封装好的http方法发出请求
3、对清求结果进行处理
典型用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
MBProgressHUD* hudProgress;
__block int result;
hudProgress = [[MBProgressHUD alloc] initWithWindow:[UIApplication sharedApplication].keyWindow];

hudProgress.labelText = @"XXX";

APIPackageLoginGoHeader *goHead = [[APIPackageLoginGoHeader alloc]init];
APIPackageLoginBackHeader *backHead = [[APIPackageLoginBackHeader alloc]init];
goHead.loame = @"XXXX";
goHead.paord = @"XXXX";
goHead.surce = @"XXXX
goHead.sin = @"XXXX";
goHead.versNo = @"XXXX";
[goHead makeDictionary];
NSMutableDictionary *dicBack = [[NSMutableDictionary alloc]init];[hudProgress showAnimated:NO whileExecutingBlock:^{
result = [GMPostServer GetServerBack:SERVER_LOGIN path_Param:nil query_Param:goHead.dicGo body_Param:nil method:GM_NETWORK_METHOD_POST returnValue:dicBack];
[backHead getBodyDataItems:dicBack];
}completionBlock:^{
if (result == GM_POSTBACK_SUCCESS)
{
[UserInfoEntity shareEntity].pne = backHead.ph;
[UserInfoEntity shareEntity].niame = backHead.name;
[UserInfoEntity shareEntity].hasLogin = YES;
[UserInfoEntity shareEntity].userId = [NSString stringWithFormat:@"%@",backHead.userId];
[UserInfoEntity shareEntity].state = backHead.state;

if ([launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"] != nil) {
self.pushInfo = [launchOptions objectForKey:@"UIApplicationLaunchOptionsRemoteNotificationKey"];
BasicHomeViewController *basicVc = (BasicHomeViewController*)self.window.rootViewController;
UINavigationController *viewController = (UINavigationController *)[[basicVc.tabbar.buttonData objectAtIndex:basicVc.tabbar.selectIndex] viewController];
[viewController dismissViewControllerAnimated:NO completion:nil];
[[PushEventManager sharedInstance] pushEvent:self.pushInfo target:viewController];

[APService setBadge:0];
[UIApplication sharedApplication].applicationIconBadgeNumber = 0;
}
[self requestUserInfo];
}
else
{
showErroMsg(backHead.errorMsg);
}
}];

上述代码是用户登录,在ViewController里面的,有一个很大的特点:长。而且集合了参数准备,发请求,请求结果处理,还和UI相互耦合。
试想一下,用户登录不会只在一个界面里有,如果多个地方存在登录的情况,这一块代码就会多次出现,而且大致结构都差不多吧?只有参数的取值和成功失败的处理逻辑不太一样,那么就把剩下的固定的逻辑:参数复制,hud,请求放到一个方法里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
+ (void)userLoginWithUserName:(NSString*)userName
password:(NSString*)password
success:(void(^)(APIPackageLoginBackHeader*))sucBlock
failure:(void(^)(APIPackageLoginBackHeader*))failBlock
animated:(BOOL)animated
loadingText:(NSString*)loadingText
inView:(UIView*)containerView;
{
MBProgressHUD* hudProgress;
__block NSInteger result;
hudProgress = [[MBProgressHUD alloc] initWithWindow:[UIApplication sharedApplication].keyWindow];

if (animated) {
[containerView addSubview:hudProgress];
[containerView bringSubviewToFront:hudProgress];
hudProgress.labelText = loadingText;
}

APIPackageLoginGoHeader *goHead = [[APIPackageLoginGoHeader alloc]init];
APIPackageLoginBackHeader *backHead = [[APIPackageLoginBackHeader alloc]init];
goHead.logame = userName;
goHead.pard = password;
goHead.sodde = @"iphone";
goHead.swn = @"ios";
goHead.versdddo = [LCSystemUtil appVersion];
[goHead makeDictionary];

NSMutableDictionary *dicBack = [[NSMutableDictionary alloc]init];
[hudProgress showAnimated:animated whileExecutingBlock:^{

result = [GMPostServer GetServerBack:SERVER_LOGIN path_Param:nil query_Param:goHead.dicGo body_Param:nil method:GM_NETWORK_METHOD_POST returnValue:dicBack];
[backHead getBodyDataItems:dicBack];


}completionBlock:^{

if (result == GM_POSTBACK_SUCCESS)
{
[UserInfoEntity shareEntity].phone = backHead.pe;
[UserInfoEntity shareEntity].nikename = backHead.niccdame;
[UserInfoEntity shareEntity].hasLogin = YES;
[UserInfoEntity shareEntity].userId = [NSString stringWithFormat:@"%@",backHead.uddrId];
[UserInfoEntity shareEntity].state = backHead.stcde;

if(sucBlock){
sucBlock(backHead);
}

}
else
{
if (failBlock) {
failBlock(backHead);
}
}
}];
}

外部调用,只要一行代码:

1
2
3
4
5
6
7
8
9
10
11
12
[HJUserProvider userLoginWithUserName:userName
password:password
success:^(APIPackageLoginBackHeader *backHeader) {
[self backButtonPressed:nil];
[HJUserProvider requestUserInfoWithSuccess:nil failure:nil];
}
failure:^(APIPackageLoginBackHeader *backHeader) {
[HJUIUtil showFailedMsg:backHeader.errorMsg];
}
animated:YES
loadingText:NSLocalizedString(@"Logging now...", nil)
inView:self.view];

这样的做法就把网络请求从ViewController分离开了,而且没有任何的耦合。同一个接口的请求逻辑只需写一遍(之前是copy、paste,因为代码不是唯一的,所以不同的人,不同的时间改了了其中一处,就会造成差异,引起未知的bug)。另外一个好处就是把各种请求逻辑都集中在了一处,便于阅读和修改。

4、数据归数据,UI归UI
从本质上说,程序就分成两部分:数据和UI。从ViewController的角度来说,就是State和View。View随着State的改变而改变,所以两者应该分开使ViewController结构更加清晰。
举个例子:viewDidLoad方法,这个方法里应该写些什么?

This method is called after the view controller has loaded its view hierarchy into memory. This method is called regardless of whether the view hierarchy was loaded from a nib file or created programmatically in the loadView method. You usually override this method to perform additional initialization on views that were loaded from nib files.

官方的建议是写视图上的额外的初始化工作,而和视图不相关的初始化工作就不该在这里写(题外话,iOS6之前,遇到内存警告,viewDidLoad是会调多次的,所以把对象的初始化写在这里是有问题的,iOS6之后反正没发现过viewDidLoad调用多次的情况),那么对象的初始化(状态变量的复制)应该写在哪里呢?——初始化方法!不要因为基类提供了默认的初始化方法而偷懒不写,任何一个类,除非简单的不能再简单,都要有一个初始化方法,哪怕里面什么都没写。下面这段代码的问题就是上班部分代码应该出现在初始化方法中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (void)viewDidLoad {
[super viewDidLoad];
_newsDataAry = [[NSMutableArray alloc] init];
_currentPage = 0;

//获取缓存文章
NSArray *arr = [[ArticleSqliteData shareManager] dataGetHeadArticle];
[_newsDataAry addObject:arr];

NSArray *arrNewsInfo = [[ArticleSqliteData shareManager] dataHomeViewIsFirstGet:YES];
[_newsDataAry addObjectsFromArray:arrNewsInfo];

if ([arrNewsInfo count] != 0) {
ArticleModel *model = [arrNewsInfo lastObject];
_isExpire = [self calculationTime:model.updateTime];
}
else{
_isExpire = YES;
}

//上面的代码和对象的状态相关,不该出现在这里
[self addTableView];

[self setTitleViewWithText:@"发现"];
[self setLeftButtonWithTitle:@"秘籍" action:@selector(showCheats)];
[self setRightButtonWithImageName:@"find_xiala.png" action:@selector(showCatagory)];
self.showCategory = NO;//这一行也是状态
// Do any additional setup after loading the view from its nib.

/**
* 初始化分类页面
*/
self.categoryView = [[HJFindCategoryView alloc] initWithFrame:CGRectMake(0, 64, SCREEN_WIDTH, SCREEN_HEIGHT)];
[self.categoryView addBorderTopLine];
self.categoryView.hjFindCategoryViewDelegate = self;
self.categoryView.hidden = YES;//UI应该依赖状态,而不是写死
[[AppDelegate appDelegate].window addSubview:self.categoryView];
[self getBannerNewsTitle];
}

5、成员方法与Util方法
一个类代码比较多的一个原因,就是加入了不该加入的成员方法所致。一个方法,之所以称为成员方法,是因为它操作了对象的状态,如果它没有操作对象的状态,拿它就跟该对象没有紧密的关系,就不能放在类里面实现。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-(BOOL)calculationTime:(NSDate *)date
{
NSDateFormatter *dateFormatter=[[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSDate * senddate=[NSDate date];
//结束时间
NSDate *endDate = date;
//当前时间
NSDate *senderDate = [dateFormatter dateFromString:[dateFormatter stringFromDate:senddate]];
//得到相差秒数
NSTimeInterval time=[senderDate timeIntervalSinceDate:endDate];

int days = ((int)time)/(3600*24);
if (days < 0) {
return YES;
}
else{
return NO;
}
}

上面这个方法和对象没什么关系,更应该抽象成为一个Util方法,可以在这个类里面调用,更可以再其他更多的地方调用。

6、功能点单一入口
在代码中,有些功能逻辑是相对固定的,但是在很多地方会反复出现。
比如登录功能,除了用户主动登录,还有在未登录状态下使用某些需要登录的功能,都会去调用登录页面。我看了一下现在的代码,一共出现了14次调用登录页面。代码重复是一个方面,还有就是定位bug麻烦。比如进入首页的时候莫名其妙弹出个登录页面,因为首页逻辑复杂,我得打很多断点才能定位到产生问题的代码。
还有对tabbar的操作,也是出现在了多出地方。其实只要搞清楚tabbar是哪个类管理的,然后让这个类封装出一个public的方法,控制tabbar的隐藏与否即可。

所以一个功能点我们只保留一个出口,其他地方只要调用方法即可。把相同的地方写在方法内部,不同的地方作为参数对外开放。