这篇文章是整理自掘金作者minjing_lin,里面总结了OC编程的常用代码规范。文章并非原创,自己转录以及补充。

为什么要写这篇文章

最近公司将代码规范列入考核标准,然后一直想系统的学习一下代码规范,算是对之前的一个复习吧。正好看到掘金上有篇文章介绍代码规范的,自认为写的很不错,转录过来共同学习。

任何一个傻瓜都能写出来计算机可以理解的代码。唯有写出人类容易理解的代码,才是优秀的程序员。 ----未知

本文是笔者结合公司代码规范要求和之前看的《禅与Objective-C 编程艺术》与《Effective Objective-C 2.0》书籍,以及参考相关播客,总结出来的一套iOS开发规范。有不足的地方,欢迎指正。


大括号

除了.m文件中的方法,其他地方的大括号"{}"不需要另起一行。例如:

不推荐

if (!error) 
{
    doSomeThing();
}

推荐

if (!error) {
    doSomeThing();
}

当然,如果是方法,两种写法都可,例:

- (void)doSomeThing {
}

- (void)doSomeThong 
{
}

运算符

1. 一元运算符与变量之间没有空格

!aValue、-aValue、++aValue、--aValue、*aValue

2. 二元运算符与变量之间必须有空格

fWidth = 5 + 5;
fLength = fWidth * 2;
for (int i = 0;i < 10;i++)

3. 三元运算符

当三元运算符的第二个参数(if分支)返回和条件语句中已经检查的对象有一样的对象的时候,下面的表达方式更灵巧:

推荐

result = object ? : [self createObject];

不推荐

result = object ? object : [self creatObject];

if语句

1. 尽量列出所有的情况,且给出明确的结果。

推荐

var hintStr;
if (count < 3) {
    hintStr = "Good";
} else {
    hineStr = "";
}

2. 黄金大道

在使用条件语句编程时,代码的左边距应该是一条"黄金大道",也就是说善宇使用return来提前返回不符合的情况。

推荐

- (void)someMethod {
    if (![someOther boolValue]) {
        return;
    }
    //do something
}

3. 复杂的表达式

条件表达式如果比较复杂,则需要将他们提取出来赋给一个BOOL变量。

推荐

BOOL nameContainSwift = [sessionName containString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2009;
BOOL isSwiftSession = nameContainSwift && isCurrentYear;
if (isSwiftSession) {
    //do something
}

4. 优先表达式

优先表达式是指,拿一个常量去和变量比较而不是拿变量去和常量比较。

推荐

if (count == 6) {
}

if (myValue == nil) {
}

if (!object) {
}

5. 条件语句体应该总是被大括号包围

尽管有时候你可以不使用大括号(比如,条件语句体只有一行内容),但是这样会带来问题隐患。

推荐

if (!error) {
    return success;
}

不推荐

if (!error) return success;

if (!error)
    return success;

Switch语句

1. 每个分支都必须用大括号括起来

推荐

switch (number) {
    case 1:
    {
        //do case1
        break;
    }
    case 2:
    {
        //do case2
        break;
    }
    default:
    {
        break;
    }   
}

2. 除了使用枚举类型以外,都必须有default分支

switch (menuType) {
    case menuTypeLeft: {
        break;
    }
    case menuTypeRight: {
        break;
    }
}

在switch语句使用枚举类型的时候,如果使用了default分支,在将来就无法通过编译器来检查新增的枚举类型了。


函数

1. 一个函数的长度尽量限制在50行以内

如果一个方法里面的代码函数过多,代码的阅读体验极差。

2. 一个函数只做一件事情(单一原则)

每个函数的职责都应该划分的很明确(就像类一样)。

3. 对于有返回值的函数,确保每个分支都有返回值

推荐

int function() {
    if (condition1) {
        return count1;
    } else if (condition2) {
        return count2;
    } else {
        return count3;
    }
} 

4. 外部传入的参数需要检验参数的非空、数据类型的合法性,参数错误应立即返回或者断言

推荐

void function (param1,param2) {
    if (!param1) {
        return;
    }
    if (!param2) {
        return;
    }
    //do somethis after
}

5. 多个函数如果有逻辑重复的代码,建议将重复的部分提取出来,成为独立的函数进行调用

6. 如果方法参数过多过长,建议多行书写,每个参数占用一行,用冒号对齐

推荐

- (void)initWithAge:(NSInteger)age
               name:(NSString *)name
             weight:(CGFloat)weight;

7. 方法名中不应该使用and,而且签名要与对应的参数名保持一致

推荐

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推荐

- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;

注释

1. 类的注释

对于类的注释写在当前类文件的顶部。

2. 属性注释

对于属性的注释建议写在属性上面,用的时候,会有提示功能。

/// 刷新按钮
@property (nonatomic, strong) UIButton *refreshButton;

3. 方法注释

对于.h文件中方法的注释,通过快捷键commend+option+/快速注释;

对于.m文件中方法的注释,在方法上面添加//,注释符和注释内容之间要加一个空格;

4. 功能注释

版本迭代中,在同事写的代码的基础上开发,一定要写上版本功能注释,方便以后询问具体功能。

推荐

// 这是一个新加的功能 v1.0.1 by author

变量

1. 变量名必须使用驼峰格式、包括类名、协议名、方法名等

推荐

HomePageController.h
<HeaderViewDelegate>

NSString *personName;

2. 变量的名称必须同时包含功能与类型

UIButton *deleteButton;
UILabel *nameLabel;

3. 系统常用类作实例变量声明时加入后缀

推荐

类型                       后缀
UIViewCOntroller          VC
UIView                      View
UILabel                   Lbl
UIButton                  Btn
UIImage                      Img
UIImageView                  ImageView
NSArray                      Array
NSMutableArray              MutableArr
NSDictionary               Dict
NSMutableDictionary          MutableDict
NSString                  Str
NSMutableString              MutableStr
NSSet                      Set
NSMutableSet               MutableSet

常量

1. 常量以相关类名作为前缀

推荐

static const NSTimeInterval TimeViewControllerFadeOutDuration = 0.4;

不推荐

static const NSTimeInterval FadeOutDuration = 0.4;

2. 建议使用类型常量,不建议使用#define预处理命令

首先比较一下这两种声明变量的区别:

  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改。
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。

推荐

static const CGFloat TimeViewHeight = 50.0f;

不推荐

#define TimeViewHeight = 50.0f;

3. 对外公开某个常量

如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串(通知的名称),那么显然这个字符串是不能被轻易修改的,而且可以在不同的地方获取到。这个时候就需要定义一个外界可见的字符串常量。

推荐

// .h

extern NSString *const TimeViewDidRemoveNotification;

// .m

static NSString *const TimeViewDidRemoveNotification = @"timeViewDidRemove";

1. 字母全部大写、单词与单词之间用_分割

推荐

#define URL_GAIN_LIST @"/v1/list"

2. 宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来

#define MY_MIN(a,b) ((A)>(B)?(B):(A))

枚举

当使用enum的时候,建议使用新的固定的基础类型定义,因为它有强大的类型检查和代码补全。

推荐

typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
    UIControlContentVerticalAlignmentCenter  = 0,
    UIControlContentVerticalAlignmentTop     = 1,
    UIControlContentVerticalAlignmentBottom  = 2,
    UIControlContentVerticalAlignmentFill    = 3,
};

泛型

建议在定义NSArray和NSDictionary时使用泛型,可以保证程序的安全性:

推荐

NSArray<NSString *> *testArr =@[@"hello",@"world"];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key":@(1), @"age":@(10)};

NSMutableArray

1. addObject之前要非空判断

2. 取下标之前要判断是否越界

3. 取第一个元素或最后一个元素的时候使用firstObject或lastObject


字面量语法

尽量使用字面值来创建NSString、NSDictionary、NSArray、NSNumber这些不可变对象:

推荐

NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve"];
NSDictionary *productManagers = @{@"iPhone" : @"Kate", @"iPad" : @"Kamal", @"Mobile Web" : @"Bill"}; 
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingZIPCode = @10018; 

不推荐

NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", nil];
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018]; 

Block

为常用的block创建typedef

如果我们需要重复创建某种block(相同参数、返回值)的变量,我们就可以通过typedef来给某一种块定义属于它自己的新类型。

例如

int (^variableName)(BOOL flag,int value) = ^(BOOL flag,int value) {
    //do somethings
    return someInt;
}

这个block有一个bool参数和一个int参数,并返回int类型。我们可以给它定义类型:

typedef int(^VariableBlock)(BOOL flag,int value);

再次定义的时候,就可以通过简单的赋值操作,例如:

VariableBlock = ^(BOOL flag,int value) {
    // do somethings
    return soneInt;
}

定义作为参数的block:

例如,我们这里有如下代码:

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

这里的completion为一个block参数,此时我们可以:

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”

通过typedef定义的block签名的好处是:如果需要修改block,例如:增加参数,那么只需修改def的代码即可。


属性

1. 书写规则

@property、空格、括号、线程修饰词、读写修饰符、空格、括号、类、对象名称;

根据不同的场景选择合适的修饰符

推荐

@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, strong, readwrite) UIView *headerView;
@property (nonatomic, weak) id<#delegate#> delegate;

2. Block属性应该使用copy关键字

推荐

typedef void (^ErrorCodeBlock)(NSString *message);

@property (nonatomic, copy) ErrorCodeBlock errorBlock;

@property (nonatomic, copy) void (^errorBlock)(NSString *meg);

3. 形容词性的BOOL属性的getter应该加上is前缀

推荐

@property (nonatomic, assign, getter=isEditable) BOOL editable;

4. 对外尽量使用不可变对象

尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体的做法是:

  • 在头文件中,设置对象的属性为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

在这里,我们将firends属性设置为不可变的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. 代理方法的第一个参数必须为委托者

代理方法必须以委托者作为第一个参数(参考UITableViewDelegate)的方法。其目的是为了区分不同委托者的实例。因为同一个控制器是可以作为多个tableview的代理的。例如:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

2. 向代理发送消息时需要判断其是否实现该方法

推荐

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) { 
 

[self.delegate signUpViewControllerDidPressSignUpButton:self]

; }

3. 遵循代理过多的时候,换行对齐显示

推荐

@interface ShopViewController () <UIGestureRecognizerDelegate,
                                  HXSClickEventDelegate,
                                  UITableViewDelegate,
                                  UITableViewDataSource>

4. 代理的方法需要明确必须执行和可不执行

  • @@required:必须实现的方法
  • @optional:可选是否实现的方法
@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries; 
@end 

1. 类的名称

应该以三个大写字母为前缀;创建子类的时候,应该把代表子类特点的部分放在前缀和父类名的中间。

推荐

//父类
ZOCSalesListViewController

//子类
ZOCDaySalesListViewController
ZOCMonthSalesListViewController

2. 所有返回类对象和实例对象的方法都应该使用instancetype

将instancetype关键字作为返回值的时候,可以让编译器进行类型检查,同时适用于子类的检查,这样就保证了返回类型的正确性(一定为当前的类对象或实例对象)

推荐

- (instancetype)init 
{ 
    self = [super init]; // call the designated initializer 
    if (self) { 
        // Custom initialization 
    } 
    return self; 
} 


@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end 

不推荐

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end 

3. 在类的.h文件中尽量少引用其他头文件

有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头温江,而应该使用向前声明(forward declaring)使用class关键字,并在A的实现文件引用B的头文件。

// EOCPerson.h
#import <Foundation/Foundation.h>

@class EOCEmployer;

@interface EOCPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;//将EOCEmployer作为属性

@end

// EOCPerson.m
#import "EOCEmployer.h"

优点

  • 不在A的头文件中引入B的头文件,就不会一并引入B的全部内容,这样就减少了编译时间。
  • 可以避免循环引用:因为如果两个类在自己的头文件中都引用了对方的头文件,那么久会导致其中一个类无法被正确编译。

但是个别的时候,必须在头文件中引入其他类的头文件:

  • 该类继承于某个类,则需要引入父类的头文件。
  • 该类遵循某个协议,则应该引入该协议的头文件。而且最好将协议单独放在一个头文件中。

4. 类的布局

#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated

#pragma mark - Override 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 - Setters and Getters

可以使用代码块一键生成,参考Xcode 添加代码块.


相等性的判断

判断两个person类是否相等的合理做法:

-  (BOOL)isEqual:(id)object 
{

    if (self == object) {  
        return YES; //判断内存地址
    } 

    if (![object isKindOfClass:[ZOCPerson class]]) { 
        return NO; //是否为当前类或派生类 
     } 

     return [self isEqualToPerson:(ZOCPerson *)object]; 

}

//自定义的判断相等性的方法
-  (BOOL)isEqualToPerson:(Person *)person 
{ 
        if (!person) {  
              return NO;
        } 
        BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name]; 
        BOOL birthdaysMatch = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday]; 
        return haveEqualNames && haveEqualBirthdays; 
} 

图片命名

1. 命名规范:不能有中文、大写、特殊符号、空白

2. 命名格式(推荐):fileType[function]project[pageName]imageName[status]{.jpg.png}

万能公式:类别功能模块名称状态.png

icon_tab_bookshelf_sel@2x.png

关于OCLint

OCLint是一个静态代码分析工具,提高质量和减少缺陷通过检查C 、C++ 和Objective-C 代码和寻找潜在的问题
静态代码分析是一个来检测对于编译不可见的缺陷的关键技术。

OCLint官网地址

OCLint如何安装与使用


参考文献:

禅与 Objective-C 编程艺术

iOS代码规范

看完这个你们团队的代码也很规范


你远道而来这人世间,想必也是因为热爱吧 ||