软件的生命周期贯穿产品的开发,测试,生产,用户使用,版本升级和后期维护等过程,只有易读,易维护的软件代码才具有生命力。
一、思路
最近重构项目组件,看到项目中存在一些命名和方法分块方面存在一些问题,结合平时经验和 Apple官方代码规范 在此整理出 iOS 工程规范。提出第一个版本,如果后期觉得有不完善的地方,继续提出来不断完善,文档在此记录的目的就是为了大家的代码可读性较好,后来的人或者团队里面的其他人看到代码可以不会因为代码风格和可读性上面造成较大时间的开销。
先梳理出规范,然后使用一些脚本的方式,提高大家使用的便捷性与效率,最后开发一些协作脚本。
二、一些原则
- 长的,描述性的方法和变量命名是好的。不要使用简写,除非是一些大家都知道的场景比如 VIP。不要使用 bgView,推荐使用 backgroundView
- 见名知意。含义清楚,做好不加注释代码自我表述能力强。(前提是代码足够规范)
- 不要过分追求技巧,降低代码可读性
- 删除没必要的代码。比如我们新建一个控制器,里面会有一些不会用到的代码,或者注释起来的代码,如果这些代码不需要,那就删除它,留着偷懒吗?下次需要自己手写
- 在方法内部不要重复计算某个值,适当的情况下可以将计算结果缓存起来
- 尽量减少单例的使用。
- 提供一个统一的数据管理入口,不管是 MVC、MVVM、MVP 模块内提供一个统一的数据管理入口会使得代码变得更容易管理和维护。
- 除了 .m 文件中方法,其他的地方"{"不需要另起一行。
- (void)getGooodsList
{
...
}
- (void)doHomework
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
变量
- 一个变量最好只有一个作用,切勿为了节省代码行数,觉得一个变量可以做多个用途。(单一原则)
- 方法内部如果有局部变量,那么局部变量应该靠近在使用的地方,而不是全部在顶部声明全部的局部变量。
运算符
- 1元运算符和变量之间不需要空格。例如:++n
- 2元运算符与变量之间需要空格隔开。例如: containerWidth = 0.3 * Screen_Width
- 当有多个运算符的时候需要使用括号来明确正确的顺序,可读性较好。例如: 2 << (1 + 2 * 3 - 4)
条件表达式
- 当有条件过多、过长的时候需要换行,为了代码看起来整齐些
//good
if (condition1() &&
condition2() &&
condition3() &&
condition4()) {
// Do something
}
//bad
if (condition1() && condition2() && condition3() && condition4()) { // Do something }
- 在一个代码块里面有个可能的情况时善于使用
return
来结束异常的情况。
- (void)doHomework
{
if (self.hungry) {
return;
}
if (self.thirsty) {
return;
}
if (self.tired) {
return;
}
papapa.then.over;
}
- 每个分支的实现都必须使用 {} 包含。
// bad
if (self.hungry) self.eat()
// good
if (self.hungry) {
self.eat()
}
- 条件判断的时候应该是变量在左,条件在右。 if ( currentCursor == 2 ) { //... }
- switch 语句后面的每个分支都需要用大括号括起来。
- switch 语句后面的 default 分支必须存在,除非是在对枚举进行 switch。
switch (menuType) {
case menuTypeLeft: {
...
break;
}
case menuTypeRight: {
...
break;
}
case menuTypeTop: {
...
break;
}
case menuTypeBottom: {
...
break;
}
}
类名
- 大写驼峰式命名。每个单词首字母大写。比如「申请记录控制器」ApplyRecordsViewController
- 每个类型的命名以该类型结尾。
- ViewController:使用
ViewController
结尾。例子:ApplyRecordsViewController - View:使用
View
结尾。例子:分界线:boundaryView - NSArray:使用
s
结尾。比如商品分类数据源。categories - UITableViewCell:使用
Cell
结尾。比如 MyProfileCell - Protocol:使用
Delegate
或者Datasource
结尾。比如 XQScanViewDelegate - Tool:工具类
- 代理类:Delegate
- Service 类:Service
- ViewController:使用
类的注释
有时候我们需要为我们创建的类设置一些注释。我们可以在类的下面添加。
枚举
枚举的命名和类的命名相近。
typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
UIControlContentVerticalAlignmentCenter = 0,
UIControlContentVerticalAlignmentTop = 1,
UIControlContentVerticalAlignmentBottom = 2,
UIControlContentVerticalAlignmentFill = 3,
};
宏
- 全部大写,单词与单词之间用
_
连接。 - 以
K
开头。后面遵循大写驼峰命名。「不带参数」
#define HOME_PAGE_DID_SCROLL @"com.xq.home.page.tableview.did.scroll"
#define KHomePageDidScroll @"com.xq.home.page.tableview.did.scroll"
属性
书写规则,基本上就是 @property 之后空一格,括号,里面的 线程修饰词、内存修饰词、读写修饰词,空一格 类 对象名称
根据不同的场景选择合适的修饰符。
@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, assign, readonly) BOOL loading;
@property (nonatomic, weak) id<#delegate#> delegate;
@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);
单例
单例适合全局管理状态或者事件的场景。一旦创建,对象的指针保存在静态区,单例对象在堆内存中分配的内存空间只有程序销毁的时候才会释放。基于这种特点,那么我们类似 UIApplication 对象,需要全局访问唯一一个对象的情况才适合单例,或者访问频次较高的情况。我们的功能模块的生命周期肯定小于 App 的生命周期,如果多个单例对象的话,势必 App 的开销会很大,糟糕的情况系统会杀死 App。如果觉得非要用单例比较好,那么注意需要在合适的场合 tearDown 掉。
单例的使用场景概括如下:
- 控制资源的使用,通过线程同步来控制资源的并发访问。
- 控制实例的产生,以达到节约资源的目的。
- 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
+ (instancetype)sharedInstance
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//because has rewrited allocWithZone use NULL avoid endless loop lol.
_sharedInstance = [[super allocWithZone:NULL] init];
});
return _sharedInstance;
}
+ (id)allocWithZone:(struct _NSZone *)zone
{
return [TestNSObject sharedInstance];
}
+ (instancetype)alloc
{
return [TestNSObject sharedInstance];
}
- (id)copy
{
return self;
}
- (id)mutableCopy
{
return self;
}
- (id)copyWithZone:(struct _NSZone *)zone
{
return self;
}
私有变量
推荐以 _
开头,写在 .m 文件中。例如 NSString * _somePrivateVariable
代理方法
- 类的实例必须作为方法的参数之一。
- 对于一些连续的状态的,可以加一些 will(将要)、did(已经)
- 以类的名称开头
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;
方法
- 方法与方法之间间隔一行
- 大量的方法尽量要以组的形式放在一起,比如生命周期函数、公有方法、私有方法、setter && getter、代理方法..
- 方法最后面的括号需要另起一行。遵循 Apple 的规范
- 对于其他场景的括号,括号不需要单独换行。比如 if 后面的括号。
- 如果方法参数过多过长,建议多行书写。用冒号进行对齐。
- 一个方法内的代码最好保持在50行以内,一般经验来看如果一个方法里面的代码行数过多,代码的阅读体验就很差(别问为什么,做过重构代码行数很长的人都有类似的心情)
- 一个函数只做一个事情,做到单一原则。所有的类、方法设计好后就可以类似搭积木一样实现一个系统。
- 对于有返回值的函数,且函数内有分支情况。确保每个分支都有返回值。
- 函数如果有多个参数,外部传入的参数需要检验参数的非空、数据类型的合法性,参数错误做一些措施:立即返回、断言。
- 多个函数如果有逻辑重复的代码,建议将重复的部分抽取出来,成为独立的函数进行调用
- (instancetype)init
{
self = [super init];
if (self) {
<#statements#>
}
return self;
}
- (void)doHomework:(NSString *)name
period:(NSInteger)second
score:(NSInteger)score;
- 方法如果有多个参数的情况下需要注意是否需要介词和连词。很多时候在不知道如何抉择测时候思考下苹果的一些 API 的方法命名。
//good
- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
//bad
- (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;
- (void)tableView:(UITableView *)tableView :(NSIndexPath *)indexPath;
.m
文件中的私有方法需要在顶部进行声明- 方法组之间也有个顺序问题。
- 在文件最顶部实现属性的声明、私有方法的声明(很多人省去这一步,问题不大,但是蛮多第三方的库都写了,看起来还是会很方便,建议书写)。
- 在生命周期的方法里面,比如 viewDidLoad 里面只做界面的添加,而不是做界面的初始化,所有的 view 初始化建议放在 getter 里面去做。往往 view 的初始化的代码长度会比较长、且一般会有多个 view 所以 getter 和 setter 一般建议放在最下面,这样子顶部就可以很清楚的看到代码的主要逻辑。
- 所有button、gestureRecognizer 的响应事件都放在这个区域里面,不要到处乱放。
文件基本上就是
//___FILEHEADER___
#import "___FILEBASENAME___.h"
/*ViewController*/
/*View&&Util*/
/*model*/
/*NetWork InterFace*/
/*Vender*/
@interface ___FILEBASENAMEASIDENTIFIER___ ()
@end
@implementation ___FILEBASENAMEASIDENTIFIER___
#pragma mark - life cycle
- (void)viewWillAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = <#value#>;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidAppear:animated];
}
#ifdef DEBUG
- (void)dealloc
{
NSLog(@"%s",__func__);
}
#endif
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
//...(多个代理方法依次往下写)
#pragma mark - getters and setters
@end
图片资源
- 单个文件的命名 文件资源的命名也需要一定的规范,形式为:功能模块名_类别_功能_状态@nx.png [email protected]、[email protected] [email protected]、[email protected]
- 资源的文件夹命名 最好也参考 App 按照功能模块建立对应的实体文件夹目录,最后到对应的目录下添加相应的资源文件。
注释
- 对于类的注释写在当前类文件的顶部
- 对于属性的注释需要写在属性后面的地方。 //**<userId*/
- 对于 .h 文件中方法的注释,一律按快捷键
command+option+/
。三个快捷键解决。按需在旁边对方法进行说明解释、返回值、参数的说明和解释 - 对于 .m 文件中的方法的注释,在方法的旁边添加
//
。 - 注释符和注释内容需要间隔一个空格。 例如: // fetch goods list
版本规范
采用 A.B.C 三位数字命名,比如:1.0.2,当有更新的情况下按照下面的依据
版本号 | 右说明对齐标题 | 示例 |
---|---|---|
A.b.c | 属于重大内容的更新 | 1.0.2 -> 2.0.0 |
a.B.c | 属于小部分内容的更新 | 1.0.2 -> 1.1.1 |
a.b.C | 属于补丁更新 | 1.0.2 -> 1.0.3 |
改进
我们知道了平时在使用 Xcode 开发的过程中使用的系统提供的代码块所在的地址和新建控制器、模型、view等的文件模版的存放文件夹地址后,我们就可以设想下我们是否可以定制自己团队风格的控制器模版、是否可以打造和维护自己团队的高频使用的代码块?
答案是可以的。
Xcode 代码块的存放地址:~/Library/Developer/Xcode/UserData/CodeSnippets
Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/
意义
- 为了个人或者团队开发者的代码更加规范。Property的书写的时候的空格、线程修饰词、内存修饰词的先后顺序
- 提供大量可用的代码块,提高开发效率。比如在 Xcode 里面敲 UITableView_init 便可以自动懒加载创建一个 UITabelView 对象,你只需要设置在指定的位置写相应的参数
- 通过一些代码块提高代码规范、避免一些bug。比如曾看到过 block 属性用 strong 修饰的代码,造成内存泄漏。举个例子你在 Xcode 中输入 Property_delegate 就会出来
@property (nonatomic, weak) id<<#delegate#>> delegate;
,你输入 Property_block 就会出来@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);
三、 代码块的改造
我们可以将属性、控制器生命周期方法、单例构造一个对象的方法、代理方法、block、GCD、UITableView 懒加载、UITableViewCell 注册、UITableView 代理方法的实现、UICollectionVIew 懒加载、UICollectionVIewCell 注册、UICollectionView 的代理方法实现等等组织为 codesnippets
思考
-
封装好 codesnippets 之后团队除了你编写这个项目的人如何使用?如何知道是否有这个代码块?
方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
属性:通过 Property_类型 开头,回车键自动补全。比如 Strong 类型,编写代码通过 Property_Strong 回车键自动补全成如下格式
@property (nonatomic, strong) <#Class#> *<#object#>;
方法:以 Method_关键词 回车键确认,自动补全。比如 Method_UIScrollViewDelegate 回车键自动补全成 如下格式
#pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView { }
各种常见的 Mark:以 Mark_关键词 回车确认,自动补全。比如 Method_MethodsGroup 回车键自动补全成 如下格式
#pragma mark - life cycle #pragma mark - public Method #pragma mark - private method #pragma mark - event response #pragma mark - UITableViewDelegate #pragma mark - UITableViewDataSource #pragma mark - getters and setters
-
封装好 codesnippets 之后团队内如何统一?想到一个方案,可以将团队内的 codesnippets 共享到 git,团队内的其他成员再从云端拉取同步。这样的话团队内的每个成员都可以使用最新的 codesnippets 来编码。
编写 shell 脚本。几个关键步骤:
- 给系统文件夹授权
- 在脚本所在文件夹新建存放代码块的文件夹
- 将系统文件夹下面的代码块复制到步骤2创建的文件夹下面
- 将当前的所有文件提交到 Git 仓库
四、文件模版的改造
我们观察系统文件模版的特点,和在 Xcode 新建文件模版对应。
所以我们新建 Custom 文件夹,将系统 Source 文件夹下面的 Cocoa Touch Class.xctemplate 复制到 Custom 文件夹下。重命名为我们需要的名字,我这里以“Power”为例
进入 PowerViewController.xctemplate/PowerViewControllerObjective-C
修改 ___FILEBASENAME___.h
和 ___FILEBASENAME___.m
文件内容
在替换 .h 文件内容的时候后面改为 UIViewController,不然其他开发者新建文件模版的时候出现的不是 UIViewController 而是我们的 PowerViewController
修改 TemplateInfo.plist
思考:
-
如何使用
商量好一个标识(“Power”)。比如我新建了单例、控制器、Model、UIView、UITableViewCell、UICollectionViewCell6个模版,都以为 Power 开头。
-
如何共享
以 shell 脚本为工具。使用脚本将 git 云端的代码模版同步到本地 Xcode 文件夹对应的位置就可以使用了。关键步骤:
- git clone 代码到脚本所在文件夹
- 进入存放 codesnippets 的文件夹将内容复制到系统存放 codesnippets 的地方
- 进入存放 file template 的文件夹将内容复制到系统存放 file template 的地方
六、使用
1. Xcode 中开发
-
Property 属性。敲 Property_ 自动联想,光标移动选中后敲回车自动补全
-
Mark 标识。 敲 Mark_ 自动联想,会展示各种常用的 Mark,光标移动选中后敲回车自动补全
-
Method 方法。敲 Method_ 自动联想,会展示各种常用的 Method,光标移动选中后敲回车自动补全
-
GCD。敲 GCD_ 自动联想,会展示各种常用的 GCD,光标移动选中后敲回车自动补全
-
常用 UI 控件的懒加载。敲 _init 自动联想,展示常用的 UI 控件的懒加载,光标移动选中后敲回车自动补全
-
Delegate。敲 Delegate_ 自动联想,会展示各种常用的 Delegate,光标移动选中后敲回车自动补全
-
Notification。敲 NSNotification_ 自动联想,会展示各种常用的 NSNotification 的代码块,比如发送通知、添加观察者、移除观察者、观察者方法的实现等等,光标移动选中后敲回车自动补全
-
Protocol。敲 Protocol_ 自动联想,会展示各种常用的 Protocol 的代码块,光标移动选中后敲回车自动补全
-
内存修饰代码块
-
工程常用 TODO、FIXME、Mark。敲 Mark_ 自动联想,会展示各种常用的 Mark 的代码块,光标移动选中后敲回车自动补全
-
内存修饰代码块。敲 Memory_ 自动联想,会展示各种常用的内存修饰的代码块,光标移动选中后敲回车自动补全
-
一些常用的代码块。敲 Thread_ 等自动联想,选中后敲回车自动补全。
2. Code Snippet 同步
你可能是代码块的创建者,也可能是使用方,使用的时候直接先给脚本赋权
chmod +x ./syncSnippets.sh // 为脚本设置可执行权限
./syncSnippets.sh // 同步git云端代码块和文件模版到本地
如果自己有新的代码块创建,觉得不错,想同步到远端 github repo,则可以使用下面的命令
chmod +x ./uploadMySnippets.sh // 为脚本设置可执行权限
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端
PS
不断完善中。大家有好用或者不错的代码块或者文件模版希望参与到这个项目中来,为我们开发效率的提升添砖加瓦、贡献力量
目前新建了大概58个代码段和6个类文件模版(UIViewController控制器带有方法组、模型、线程安全的单例模版、带有布局方法的UIView模版、UITableViewCell、UICollectionViewCell模版)
shell 脚本基本有每个函数和关键步骤的代码注释,想学习 shell 的人可以看看代码。代码传送门