《代码规范》
类的布局
- (void)dealloc
- (instancetype)init
#pragma mark - Life Cycle Methods
- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated
#pragma mark - Override Methods
#pragma mark - Intial Methods
#pragma mark - Network Methods
#pragma mark - Target Methods
#pragma mark - Public Methods
#pragma mark - Private Methods
#pragma mark - UITableViewDataSource
#pragma mark - UITableViewDelegate
#pragma mark - Lazy Loads
#pragma mark - NSCopying
#pragma mark - NSObject Methods
#pragma mark - description
Switch语句
1. 每个分支都必须用大括号括起来
switch (integer) {
case 1: {
// ...
break;
}
case 2: {
// ...
break;
}
case 3: {
// ...
break;
}
default:{
// ...
break;
}
}
2. 使用枚举类型时,不能有default分支, 除了使用枚举类型以外,都必须有default分支
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;
switch (menuType) {
case RWTLeftMenuTopItemMain: {
// ...
break;
}
case RWTLeftMenuTopItemShows: {
// ...
break;
}
case RWTLeftMenuTopItemSchedule: {
// ...
break;
}
}
在Switch语句使用枚举类型的时候,如果使用了default分支,在将来就无法通过编译器来检查新增的枚举类型了。
函数
1. 一个函数的长度必须限制在80~100行以内
2. 一个函数只做一件事(单一原则)
每个函数的职责都应该划分的很明确(就像类一样)。
推荐这样写:
dataConfiguration()
viewConfiguration()
不推荐这样写:
void dataConfiguration()
{
...
viewConfiguration()
}
3. 对于有返回值的函数(方法),每一个分支都必须有返回值
推荐这样写:
int function()
{
if(condition1){
return count1
}else if(condition2){
return count2
}else{
return defaultCount
}
}
4. 对输入参数的正确性和有效性进行检查,参数错误立即返回
推荐这样写:
void function(param1,param2)
{
if(param1 is unavailable){
return;
}
if(param2 is unavailable){
return;
}
//Do some right thing
}
5. 将函数内部比较复杂的逻辑提取出来作为单独的函数
一个函数内的不清晰(逻辑判断比较多,行数较多)的那片代码,往往可以被提取出去,构成一个新的函数,然后在原来的地方调用它这样你就可以使用有意义的函数名来代替注释,增加程序的可读性。
举一个发送邮件的例子:
openEmailSite();
login();
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
send();
中间的部分稍微长一些,我们可以将它们提取出来:
void writeEmail(title, content,receiver,attachment)
{
writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);
}
然后再看一下原来的代码:
openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();
6. 避免使用全局变量,类成员(class member)来传递信息,尽量使用局部变量和参数。
在一个类里面,经常会有传递某些变量的情况。而如果需要传递的变量是某个全局变量或者属性的时候,有些朋友不喜欢将它们作为参数,而是在方法内部就直接访问了:
class A {
var x;
func updateX()
{
...
x = ...;
}
func printX()
{
updateX();
print(x);
}
}
我们可以看到,在printX方法里面,updateX和print方法之间并没有值的传递,乍一看我们可能不知道x从哪里来的,导致程序的可读性降低了。
而如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个类,而且更加容易理解,不易出错:
func updateX() -> String
{
x = ...;
return x;
}
func printX()
{
String x = updateX();
print(x);
}
三. iOS规范
变量
1. 变量名必须使用驼峰格式
类,协议使用大驼峰:
HomePageViewController.h
对象等局部变量使用小驼峰:
NSString *personName = @"";
NSUInteger totalCount = 0;
2. 变量的名称必须同时包含功能与类型
UIButton *addBtn //添加按钮
UILabel *nameLbl //名字标签
NSString *addressStr//地址字符串
3. 系统常用类作实例变量声明时加入后缀
常量
1. 常量以相关类名作为前缀
推荐这样写:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
2. 建议使用类型常量,不建议使用#define预处理命令
首先比较一下这两种声明常量的区别:
预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。
使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:
不具备类型信息。
可以被任意修改。
3. 对外公开某个常量:
如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串(通知的名称),那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。
推荐这样写:
//头文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
//实现文件
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推荐这样写:
#define CompanyName @"Apple Inc."
#define magicNumber 42
宏
1. 宏、常量名都要使用大写字母,用下划线‘_’分割单词。
#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN @"/v1/user/login”
2. 宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来。
#define MY_MIN(A, B) ((A)>(B)?(B):(A))
CGRect函数
其实iOS内部已经提供了相应的获取CGRect各个部分的函数了,它们的可读性比较高,而且简短,推荐使用:
推荐这样写:
CGRect frame = self.view.frame;
CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);
CGRect frame = CGRectMake(0.0, 0.0, width, height);
而不是
CGRect frame = self.view.frame;
CGFloat x = frame.origin.x;
CGFloat y = frame.origin.y;
CGFloat width = frame.size.width;
CGFloat height = frame.size.height;
CGRect frame = (CGRect){ .origin = CGPointZero, .size = frame.size };
Block
为常用的Block类型创建typedef
定义类型:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
通过简单的赋值来实现:
EOCSomeBlock block = ^(BOOL flag, int value){
// Implementation
};
定义作为参数的Block:
- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;
这里的Block有一个NSData参数,一个NSError参数并没有返回值
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”
通过typedef定义Block签名的好处是:如果要某种块增加参数,那么只修改定义签名的那行代码即可。
字面量语法
尽量使用字面量值来创建 NSString , NSDictionary , NSArray , NSNumber 这些不可变对象:
推荐这样写:
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"};
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018;
不推荐这样写:
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018];
属性
1. 属性的命名使用小驼峰
推荐这样写:
@property (nonatomic, readwrite, strong) UIButton *confirmButton;
2. 属性的关键字推荐按照 原子性,读写,内存管理的顺序排列
推荐这样写:
@property (nonatomic, readwrite, copy) NSString *name;
@property (nonatomic, readonly, copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;
3. Block属性应该使用copy关键字
推荐这样写:
typedef void (^ErrorCodeBlock) (id errorCode, NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock; //将block拷贝到堆中
4. 形容词性的BOOL属性的getter应该加上is前缀
推荐这样写:
@property (assign, getter=isEditable) BOOL editable;
5. 使用getter方法做懒加载
实例化一个对象是需要耗费资源的,如果这个对象里的某个属性的实例化要调用很多配置和计算,就需要懒加载它,在使用它的前一刻对它进行实例化:
- (NSDateFormatter *) dateFormatter
{
if (!_dateFormatter) {
_dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[_dateFormatter setLocale:enUSPOSIXLocale];
[_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
}
return _dateFormatter;
}
6. 除了init和dealloc方法,建议都使用点语法访问属性
使用点语法的好处:
setter:
setter会遵守内存管理语义(strong, copy, weak)。
通过在内部设置断点,有助于调试bug。
可以过滤一些外部传入的值。
捕捉KVO通知。
getter:
允许子类化。
通过在内部设置断点,有助于调试bug。
实现懒加载(lazy initialization)。
注意:
懒加载的属性,必须通过点语法来读取数据。因为懒加载是通过重写getter方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
在init和dealloc方法里面使用点语法的后果是:因为没有绕过setter和getter,在setter和getter里面可能会有很多其他的操作。而且如果它的子类重载了它的setter和getter方法,那么就可能导致该子类调用其他的方法。
7. 尽量使用不可变对象
建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:
在头文件中,设置对象属性为readonly。
在实现文件中设置为readwrite。
这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。
如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。所以当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:
在公共API中:
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
@end
在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。
在实现文件里:
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
NSMutableSet *_internalFriends; //实现文件里的可变集合
}
- (NSSet*)friends
{
return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}
- (void)addFriend:(EOCPerson*)person
{
[_internalFriends addObject:person]; //在外部增加集合元素的操作
//do something when add element
}
- (void)removeFriend:(EOCPerson*)person
{
[_internalFriends removeObject:person]; //在外部移除元素的操作
//do something when remove element
}
- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName
{
if ((self = [super init])) {
_firstName = firstName;
_lastName = lastName;
_internalFriends = [NSMutableSet new];
}
return self;
}
我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。
这里最重要的代码是:
- (NSSet*)friends
{
return [_internalFriends copy];
}
这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。
类
1. 类的名称应该以三个大写字母为前缀;创建子类的时候,应该把代表子类特点的部分放在前缀和父类名的中间
推荐这样写:
//父类
ZOCSalesListViewController
//子类
ZOCDaySalesListViewController
ZOCMonthSalesListViewController
2. initializer && dealloc
推荐:
将 dealloc 方法放在实现文件的最前面
将init方法放在dealloc方法后面。如果有多个初始化方法,应该将指定初始化方法放在最前面,其他初始化方法放在其后。
2.1 dealloc方法里面应该直接访问实例变量,不应该用点语法访问
2.2 init方法的写法:
init方法返回类型必须是instancetype,不能是id。
必须先实现[super init]。
- (instancetype)init
{
self = [super init]; // call the designated initializer
if (self) {
// Custom initialization
}
return self;
}
2.3 指定初始化方法
指定初始化方法(designated initializer)是提供所有的(最多的)参数的初始化方法,间接初始化方法(secondary initializer)有一个或部分参数的初始化方法。
注意事项1:间接初始化方法必须调用指定初始化方法。
@implementation ZOCEvent
//指定初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
//间接初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
//间接初始化方法
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
@end
注意事项2:如果直接父类有指定初始化方法,则必须调用其指定初始化方法
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
}
return self;
}
注意事项3:如果想在当前类自定义一个新的全能初始化方法,则需要如下几个步骤
定义新的指定初始化方法,并确保调用了直接父类的初始化方法。
重载直接父类的初始化方法,在内部调用新定义的指定初始化方法。
为新的指定初始化方法写文档。
看一个标准的例子:
@implementation ZOCNewsViewController
//新的指定初始化方法
- (id)initWithNews:(ZOCNews *)news
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
_news = news;
}
return self;
}
// 重载父类的初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
return [self initWithNews:nil];
}
@end
在这里,重载父类的初始化方法并在内部调用新定义的指定初始化方法的原因是你不能确定调用者调用的就一定是你定义的这个新的指定初始化方法,而不是原来从父类继承来的指定初始化方法。
假设你没有重载父类的指定初始化方法,而调用者却恰恰调用了父类的初始化方法。那么调用者可能永远都调用不到你自己定义的新指定初始化方法了。
而如果你成功定义了一个新的指定初始化方法并能保证调用者一定能调用它,你最好要在文档中明确写出哪一个才是你定义的新初始化方法。或者你也可以使用编译器指令__attribute__((objc_designated_initializer))来标记它。
方法文档
看一个指定初始化方法的注释:
/
* Designated initializer. * 总结性的短语
* @param store The store for CRUD operations. 参数的描述
* @param searchService The search service used to query the store. 参数的描述
* @return A ZOCCRUDOperationsStore object. 返回值的描述
*/
- (instancetype)initWithOperationsStore:(id)store searchService:(id)searchService;
NSArray& NSMutableArray
1. addObject之前要非空判断。
2. 取下标的时候要判断是否越界。
3. 取第一个元素或最后一个元素的时候使用firtstObject和lastObject
NSNotification
1. 通知的名称
建议将通知的名字作为常量,保存在一个专门的类中:
// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification
// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
2. 通知的移除
通知必须要在对象销毁之前移除掉。
1. Xcode工程文件的物理路径要和逻辑路径保持一致。
2. 忽略没有使用变量的编译警告
对于某些暂时不用,以后可能用到的临时变量,为了避免警告,我们可以使用如下方法将这个警告消除:
- (NSInteger)giveMeFive
{
NSString *foo;
#pragma unused (foo)
return 5;
}
3. 手动标明警告和错误
- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
//手动明确一个错误:
#error Whoa, buddy, you need to check for zero here!
return (dividend / divisor);
}
- (float)divide:(float)dividend by:(float)divisor
{
手动明确一个警告:
#warning Dude, don't compare floating point numbers like this!
if (divisor != 0.0) {
return (dividend / divisor);
} else {
return NAN;
}
}