小团队的iOS代码规范

在不断填坑中前进。。

Posted by 三十一 on August 11, 2017

[TOC]

小团队的iOS代码规范

本文摘抄自iOS 代码规范,稍加修改。

1 核心原则

1.1 代码应该简洁易懂,逻辑清晰

因为软件是需要人来维护的。这个人在未来很可能不是你。所以首先是为人编写程序,其次才是计算机:

  • 不要过分追求技巧,降低程序的可读性。
  • 简洁的代码可以让bug无处藏身。要写出明显没有bug的代码,而不是没有明显bug的代码。

1.2 编程时首先达到正确性,其次考虑效率

在正确可用的代码写出之前就过度地考虑扩展,重用的问题,使得工程过度复杂。

  1. 先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
  2. 先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
  3. 先写出可用,简单,明显没有bug的代码,再考虑测试的问题。

1.3 面向变化编程,而不是面向需求编程

不要相信产品说的话,要时刻做好需求会改的准备。 不要相信产品说的话,要时刻做好需求会改的准备。 不要相信产品说的话,要时刻做好需求会改的准备。

每次迭代不能仅仅为了当前的需求,写出扩展性强,易修改的程序才是负责任的做法,对自己负责,对公司负责。

2 具体规范

2.1 变量、属性

2.1.1 命名必须使用驼峰格式

类,协议使用大驼峰

HomePageViewController.h
<HeaderViewDelegate>

对象等局部变量使用小驼峰:

NSString *personName = @"";
NSUInteger totalCount = 0;

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

UIButton *addBtn //添加按钮
UILabel *nameLbl //名字标签
NSString *addressStr//地址字符串

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

类型 后缀
UIViewController VC
UIView View
UILabel Lbl
UIButton Btn
UIImage Img
UIImageView ImagView
NSArray Array
NSMutableArray Marray
NSDictionary Dict
NSMutableDictionary Mdict
NSString Str
NSMutableString MStr
NSSet Set
NSMutableSet Mset

2.1.3 下划线不应出现在局部变量

推荐:

NSDictionary *notificationInfo;

不推荐:

NSDictionary *_notificationInfo;

2.1.4 定义指针类型的变量

定义指针类型的变量时,*放在变量前,与类型保持一个空格。 推荐:

NSDictionary *notificationInfo;

不推荐:

NSDictionary*notificationInfo;
NSDictionary* notificationInfo;
NSDictionary * notificationInfo;

2.1.5 不要出现仅依靠大小写区分的命名

程序中不要出现仅靠大小区分的相似的标识符。 不推荐:

NSDictionary *notificationInfo;
NSDictionary *NotificationInfo;

2.1.6 私有属性应放到匿名分类中

@interface RWTDetailViewController ()  
@property (strong, nonatomic) GADBannerView *googleAdView;  
@property (strong, nonatomic) ADBannerView *iAdView;  
@property (strong, nonatomic) UIWebView *adXWebView;  
@end

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

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

2.1.8 使用getter方法做懒加载

实例化一个对象是需要耗费资源的,如果这个对象里的某个属性的实例化要调用很多配置和计算,就需要懒加载它,在使用它的前一刻对它进行实例化:

//推荐这样写
@property(nonatomic,strong)UIView *demoView;

- (UIView *)demoView{
    if (_demoView == nil) {
        _demoView = ({
            UIView *view = [[UIView alloc] initWithFrame:self.view.bounds];
            view.backgroundColor = [UIColor redColor];
            view.alpha = 0.8f;
            view;
        });
    }
    
    return _demoView;
}

但是也有对这种做法的争议,但是整体利大于弊,推荐使用。

2.1.9 除了 initdealloc 方法,建议都使用点语法访问属性

使用点语法的好处: setter

  1. setter 会遵守内存管理语义(strong, copy, weak)。
  2. 通过在内部设置断点,有助于调试bug。
  3. 可以过滤一些外部传入的值。
  4. 捕捉 KVO 通知。

getter

  1. 允许子类化。
  2. 通过在内部设置断点,有助于调试bug。
  3. 实现懒加载(lazy initialization)。

注意:

  • 懒加载的属性,必须通过点语法来读取数据。因为懒加载是通过重写 getter 方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
  • initdealloc 方法里面使用点语法的后果是:因为没有绕过 settergetter ,在 settergetter 里面可能会有很多其他的操作。而且如果它的子类重载了它的 settergetter 方法,那么就可能导致该子类调用其他的方法。

2.1.10 属性声明严把权限

对不需要外部修改的属性使用 readonly

2.1.11 尽量使用不可变对象

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

  • 在头文件中,设置对象属性为 readonly
  • 在实现文件中设置为 readwrite

2.2 函数

2.2.1 函数保持单一性

函数是为一特定功能而编写,不是万能工具箱,保持单一性,一个函数只做一件事情。

2.2.2 函数名与形参不能留空格,返回类型与函数标识符有一个空格

- (void)someMethod:(id)parameter;

2.2.3 如果参数过长,则每个参数一行,以冒号对齐。

- (void)someMethod:(id)parameter
        parameter1:(id)parameter1
        parameter2:(id)parameter2;

2.2.4 如果函数名比参数名短,则每个参数占用一行,垂直对齐(非冒号对齐)

- (void)someMethod:(id)parameter
        a:(id)parameter1
        b:(id)parameter2;

2.2.5 函数名用小写字母开头的单词组合而成

函数名力求清晰、明了、通过函数名就能够判断函数的主要功能。函数名中不同意义字段之间不要用下划线连接,而要把每个字段的首字母大写以示区分。

推荐

- (NSString *)descriptionWithLocale:(id)locale;

不推荐

- (NSString *)DescriptionWithLocale:(id)locale;
- (NSString *)description_locale:(id)locale;

2.2.6 避免函数有太多的参数,参数个数尽量控制在5个以内

如果参数的确比较多,考虑这些参数定义成一个结构(或一个类)。

2.2.7 对输入参数的正确性和有效性进行检查

很多程序错误和崩溃是由非法参数引起的。对外提供的函数接口,一定要检查输入参数。 对输入参数的正确性和有效性进行检查,参数错误立即返回。

void function(param1,param2)
{
      if(param1 is unavailable) {
           return;
      }
    
      if(param2 is unavailable) {
           return;
      }
     //Do some right thing
}

2.2.8 函数(方法)体的规模不能太大

函数(方法)体的规模不能太大,尽量控制在可以在一屏幕内显示。

2.2.9 对于有返回值的函数(方法),每一个分支都必须有返回值

推荐

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

不推荐

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

2.2.10 如果在不同的函数内部有相同的功能

如果在不同的函数内部有相同的功能,应该把相同的功能抽取出来单独作为另一个函数

2.2.11 方法名前缀

  • 刷新视图的方法名要以 refresh 为首。
  • 更新数据的方法名要以 update 为首。
- (void)refreshHeaderViewWithCount:(NSUInteger)count;

- (void)updateDataSourceWithViewModel:(ViewModel *)viewModel;

2.2.12 函数之间保留一个空行

推荐

- (void)refreshHeaderViewWithCount:(NSUInteger)count;

- (void)updateDataSourceWithViewModel:(ViewModel *)viewModel;

- (void)refreshHeaderViewWithCount:(NSUInteger)count
{
}

- (void)updateDataSourceWithViewModel:(ViewModel *)viewModel
{
}

2.2.13 初始化方法

初始化方法返回类型必须是 instancetype ,不能是 id

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

2.3 if 语句

2.3.1 必须列出所有分支(穷举所有的情况),而且每个分支都必须给出明确的结果

推荐

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

不推荐

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

2.3.2 不要使用过多的分支,要善于使用 return 来提前返回错误的情况

推荐

- (void)someMethod { 
  if (!goodCondition) {
    return;
  }
  //Do something
}

不推荐

- (void)someMethod { 
  if (goodCondition) {
    //Do something
  }
}

2.3.3 条件表达式如果很长,则需要将他们提取出来赋给一个 BOOL

推荐

let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
   // Do something
}

不推荐

if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014) ) { 
    // Do something
}

2.3.4 条件语句的判断应该是变量在左,常量在右

推荐

if ( count == 6 ) {
}

if ( count == 6 ) {
}

if ( object == nil ) {
}

if ( !object ) {
}

不推荐

if ( 6 == count ) {
}

if ( nil == object ) {
}

2.3.5 每个分支的实现代码都必须被大括号包围

推荐

if ( !error ) {
  return success;
}

不推荐

if ( !error )
    return success;

if ( !error ) return success;


2.3.6 条件过多,过长的时候应该换行

推荐

if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do something
}

不推荐

if (condition1() && condition2() && condition3() && condition4()) {
  // Do something
}

2.4 注释

推荐写注释,但不是必须的,有以下几种情况是必须写注释的:

  • 公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)。
  • 涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。
  • 容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。

每次修改代码后,要相应的修改注释。 每次修改代码后,要相应的修改注释。 每次修改代码后,要相应的修改注释。

2.5 运算符

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

!bValue
~iValue
++iCount
*strSource
&fSum

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

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

2.5.3 多个不同的运算符同时存在时应该使用括号来明确优先级

在多个不同的运算符同时存在的时候应该合理使用括号,不要盲目依赖操作符优先级。 因为有的时候不能保证阅读你代码的人就一定能了解你写的算式里面所有操作符的优先级。

2.6 Switch 语句

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

switch (integer) {  
  case 1:  {
    // ...  
    break;  
   }
  case 2: {  
    // ...  
    break;  
  }  
  case 3: {
    // ...  
    break; 
  }
  default:{
    // ...  
    break; 
  }
}

2.6.2 使用枚举类型时,不能有 default 分支, 除了使用枚举类型以外,都必须有 default 分支

RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;  
switch (menuType) {  
  case RWTLeftMenuTopItemMain: {
    // ...  
    break; 
   }
  case RWTLeftMenuTopItemShows: {
    // ...  
    break; 
  }
  case RWTLeftMenuTopItemSchedule: {
    // ...  
    break; 
  }
}

2.7 单例模式

推荐

+ (instancetype)sharedInstance {
    static id sharedInstance = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}

不推荐

+ (instancetype)sharedInstance {
    static id sharedInstance;
    @synchronized(self) {
        if (sharedInstance == nil) {
            sharedInstance = [[MyClass alloc] init];
        } }
    return sharedInstance; 
}

2.8 文件目录

Xcode 工程文件的物理路径要和逻辑路径保持一致。

2.9 字面量

在编码中应多用字面量语法,少用与之等价的方法。

2.9.1 声明时的字面量语法

在声明 NSNumberNSArrayNSDictionary 时,应该尽量使用简洁字面量语法。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSArray *animals = @[@"cat", @"dog",@"mouse", @"badger"];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};

2.9.2 集合类取下标的字面量语法

NSString *cat = animals[0];
NSString *iphone = dict[@"phone"];

2.10 NSArrayNSDictionary

2.10.1 尽可能的使用字面量初始化

NSArray *animals = @[@"cat", @"dog",@"mouse", @"badger"];
Dictionary *dict = @{@"animal":@"tiger",@"phone":@"iPhone 6"};

2.10.2 使用泛型

在定义 NSArrayNSDictionary 时使用泛型,可以保证程序的安全性,而且在使用 枚举块 函数时会自动识别 item 类型

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

2.10.3 判断边界

数组取下标的时候尽可能的判断是否越界。

2.10.4 取第一个元素或最后一个元素的时候使用系统提供的方法

取第一个元素或最后一个元素的时候使用 firtstObjectlastObject

2.10.5 非空判断

addObject 之前要非空判断。

2.10.6 多用枚举块

当遍历集合元素时,建议使用块枚举,因为相对于传统的 for 循环,它更加高效,而且简洁,还能获取到用传统的 for 循环无法提供的值.

推荐

NSArray <SYPushUserModel *>*sourceArray = @[...];
[sourceArray enumerateObjectsUsingBlock:^(SYPushUserModel * obj, NSUInteger idx, BOOL * stop) {
            
}];

NSDictionary <NSString *,SYPushUserModel *>*sourceDic = @{...};
[sourceDic enumerateKeysAndObjectsUsingBlock:^(NSString * key, SYPushUserModel * obj, BOOL * stop) {
        
}];

2.11 类

2.11.1 类的布局

推荐

#pragma mark - Life Cycle
- (instancetype)init;

- (void)viewDidLoad;

- (void)viewWillAppear:(BOOL)animated;

- (void)viewDidAppear:(BOOL)animated;

- (void)dealloc

#pragma mark - Intial Methods
//初始化数据
- (void)assignDate

- (void)settingAppearance

- (void)loadSubViews

#pragma mark - Target Methods
//点击事件或通知事件

#pragma mark - Network Methods
//网络请求

#pragma mark - Public Method
//外部方法

#pragma mark - Private Method
//本类方法

#pragma mark - Delegate
//代理方法

#pragma mark - Lazy Loads
//懒加载 Getter方法

#pragma mark - set
//Setter方法

2.11.2 在类声明中包含多个 Protocol

如果在类声明中包含多个 Protocol,每个 Protocol 占用一行,与第一个对齐,保持清晰易读。

@interface SYNotificationTool : NSObject<UNUserNotificationCenterDelegate,
                                         UITableViewDelegate,
                                         UITableViewDataSource,
                                         UITabBarControllerDelegate,
                                         UIContentSizeCategoryAdjusting>

如果第一行显示不完全,推荐

@interface SYOpenMessageListNotificationTool : NSObject
<
UNUserNotificationCenterDelegate,
UITableViewDelegate,
UITableViewDataSource,
UITabBarControllerDelegate,
UIContentSizeCategoryAdjusting
>

2.11.3 把类的实现代码分散到便于管理的多个分类中

一个类可能会有很多公共方法,而且这些方法往往可以用某种特有的逻辑来分组。我们可以利用 Objecctive-C 的分类机制,将类的这些方法按一定的逻辑划入几个分区中。使得代码逻辑清晰,便于维护。

2.12 枚举

要多使用 NS_ENUM,尽量少使用 C 语言风格的enum

2.12.1 用枚举表示状态,选项,状态码

推荐使用用枚举表示状态,选项,状态码。

typedef NS_ENUM(NSUInteger, EOCConnectionState) {
  EOCConnectionStateDisconnected,
  EOCConnectionStateConnecting,
  EOCConnectionStateConnected,
};

2.12.2 使用枚举类型时,不能有 default 分支

Switch 语句使用枚举类型时,不能有 default 分支, 除了使用枚举类型以外,都必须有 default 分支 在 Switch 语句使用枚举类型的时候,如果使用了 default 分支,在将来就无法通过编译器来检查新增的枚举类型了。

2.13 大括号该不该换行

仁者见仁,智者见智的问题。

程序的分界符 {}ifelseelse ifforwhiledo等语句时, { 前添加空格紧跟语句后,空格分割。在方法(函数)应独占一行并且位于同一列,同时与引用他们的语句对齐。 {} 之内的代码块使用缩进规则对齐。

推荐:

- (void)dealloc
{
    // Do Something
}

if (isUpdated) {
    // Do Something
}

if (user.isHappy) {
    //Do something
}
else {
    //Do something else
}

2.14 常量

2.14.1 常量应该以相关类名作为前缀

推荐:
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
不推荐:
static const NSTimeInterval fadeOutTime = 0.4;

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

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

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

使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:

  • 不具备类型信息。
  • 可以被任意修改。

2.14.3 对外公开某个常量

推荐:
//头文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
//实现文件
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;

不推荐:
#define CompanyName @"Apple Inc." 
#define magicNumber 42

2.15 宏

2.15.1 宏、常量名都要使用大写字母,用下划线 _ 分割单词。

#define URL_GAIN_QUOTE_LIST @"/v1/quote/list"
#define URL_UPDATE_QUOTE_LIST @"/v1/quote/update"
#define URL_LOGIN  @"/v1/user/login”

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

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

2.16 其他

2.16.1 小括号内联复合表达式

A compound statement enclosed in parentheses。推荐使用,形式如下:

RETURN_VALUE_RECEIVER = {(
    // Do whatever you want
    RETURN_VALUE; // 返回值
)};

初始化时推荐使用,有点像block和内联函数的结合体,它最大的意义在于将代码整理分块,将同一个逻辑层级的代码包在一起;逻辑清晰,也方便拷贝使用。 推荐:

UIView *backgroundView = ({
    UIView *view = [[UIView alloc] initWithFrame:self.view.bounds];
    view.backgroundColor = [UIColor redColor];
    view.alpha = 0.8f;
    view;
});

2.16.2 源代码中关系较为紧密的代码应尽可能相邻

CGFloat fWidth;
CGFloat fLength;
CGFloat fHeight;

2.16.3 相关的赋值语句等号对齐

promotionsEntity.promotionImageStr   = activityItemDict[@"promotion_image"];
promotionsEntity.promotionIdNum      = activityItemDict[@"promotion_id"];
promotionsEntity.promotionNameStr    = activityItemDict[@"promotion_name"];
promotionsEntity.promotionColorStr   = activityItemDict[@"promotion_color"];