ChinaRoad Objective-C Style Guide
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
README.md
_config.yml

README.md

参考文档

此编码规范在制定的时候参考和借鉴了以下这些优秀的Objective-C编码风格指南:

目录

代码结构

实现文件中的代码结构,提倡以下约定:

  • #pragma mark -将函数或方法按功能进行分组。

  • dealloc方法放到实现文件的最顶部。

    这样是为了时刻提醒你要记得释放相关资源。

  • delgate或协议相关方法放到一般内容之后。

     #pragma mark - Lifecycle
     
     - (void)dealloc {}
     - (instancetype)init {}
     - (void)viewDidLoad {}
     - (void)viewWillAppear:(BOOL)animated {}
     - (void)didReceiveMemoryWarning {}
     
     #pragma mark - Custom Accessors
     
     - (void)setCustomProperty:(id)value {}
     - (id)customProperty {}
     
     #pragma mark - Protocol conformance
     #pragma mark - UITextFieldDelegate
     #pragma mark - UITableViewDataSource
     #pragma mark - UITableViewDelegate
     
     #pragma mark - NSCopying
     
     - (id)copyWithZone:(NSZone *)zone {}
     
     #pragma mark - NSObject
     
     - (NSString *)description {}

代码格式化

  • 只用空格缩进,1个TAB = 4个空格字符

    在Xcode->Preferences->Text Editing->Indentation中进行如下设置:

    1. Prefer indent using: 选择 Spaces
    2. Tab key:选择 Intents in leading whitespace
    3. 所有需要填写空格数目的地方都设置成4个

    ps. 设置成4个,是因为Xcode的默认缩进是4个空格。大量遗留代码也都是采用的缩进4个空格。

  • 建议:每行代码的长度最多不超过100个字符

    为了防止代码过长,也为了兼顾Macbook上的排版效果,将每行长度限制成100个字符。

    勾选Xcode->Preferences->Text Editing->Editing,并将长度设置成100个字符来打开行宽指示。

    ps. Google倡导的每行80个字符有点少,会带来更频繁的换行,因此增加到100个字符。

  • 建议:尝试将单个函数或方法的实现代码控制在30行内

    如果某个函数或方法的实现代码过长,可以考量下是否可以将代码拆分成几个小的拥有单一功能的方法。

    ps. 30行是在13寸macbook上Xcode用14号字体时,恰好可以让一个函数的代码做到整屏完全显示的行数。

  • 建议:将单个实现文件里的代码行数控制在500~600行内

    为了简洁和便于阅读,建议将单个实现文件的代码行数控制在500~600行以内最好。

    当接近或超过800行时,就应当开始考虑分割实现文件了。

    最好不要出现代码超过1000行的实现文件。

    我们一般倾向于认为单个文件代码行数越长,代码结构就越不好。而且,翻代码翻的手软啊。

    可以使用Objective-C的Category特性将实现文件归类分割成几个相对轻量级的实现文件。

    可以勾选上Xcode->Preferences->Text Editing->Editing中的Line numbers,开启行号提示。

  • 实现文件中,函数实现或方法实现之间必须至少有一行空行

    没有空行,代码过长后,全粘在一起,很影响阅读。

     //禁止的
     - (void)loadView {
     	//load view...
     }
     - (void)viewDidLoad {
     	[super viewDidLoad];
     	
     	//Do Something...
     }
     
     
     //正确的
     - (void)loadView {
     	//load view...
     }
     
     - (void)viewDidLoad {
     	[super viewDidLoad];
     	
     	//Do Something...
     }
  • 重载父类方法时,遇到必须调用父类方法时。调用super的代码和重载的代码之间留一行空行。

    这样做是为了便于区分出对super的调用。 通常在iOS SDK中,有许多方法在重载的时候,都要求调用super。有时候忘记调用super就会出现行为怪异的bug。 因此这里要求将调用super的代码区隔开来,方便阅读,也方便查找是否忘记了对super的调用。

     - (void)viewWillDisappear:(BOOL)animated {
         [super viewWillDisappear:animated];
         
         //空一行,将super方法的调用和重载代码区隔开来。
         [NSObject cancelPreviousPerformRequestsWithTarget:self];
     }
  • 实现文件中,函数体的左花括号不另起一行,和函数名同行,并且和函数名之间保持1个空格

    此条是为了和Xcode6.1模板生成的文件的代码风格保持一致。

     //赞成的
     - (void)didReceiveMemoryWarning {
         [super didReceiveMemoryWarning];
         // Dispose of any resources that can be recreated.
     }
     
     //不赞成的
     - (void)didReceiveMemoryWarning
     {
     
         [super didReceiveMemoryWarning];
         // Dispose of any resources that can be recreated.
     }
  • 其他地方(if/else/while/switch等),左花括号不单独另起一行。左花括号后面紧接着的代码块超过5行后,代码块和括号之间要有一行空行;代码块小于5行可以不空行

    此条是为了和Xcode自动代码补全生成的代码风格保持一致。

     //赞成的
     - (void)didReceiveMemoryWarning {
         [super didReceiveMemoryWarning];
         // Dispose of any resources that can be recreated.
         
         if (somethCondtion) {
         
             //DO Something. 
         }
     }
     
     //不赞成的
     - (void)didReceiveMemoryWarning 
     {
         [super didReceiveMemoryWarning];
         // Dispose of any resources that can be recreated.
         
         if (somethCondtion) 
         {
             //DO Something
         }
     }
  • 建议: if/else中,else与上一条分支语句的右括号之间需要换行

    此条是为了防止else和上一个分支的代码块挨在一起,影响阅读,所以建议要换行。

    换行后,也便于快速定位到else分支。

     //赞成的
     if (a > 0) {
     
     	//Do Something
     } 
     else {
     
     	//Do Something
     }
     
     //不赞成的
     if (a > 0) {
     
     	//Do Something
     } else {
     
     	//Do Something
     }
  • 如果需要手动使用@synthesize@dynamic,每行只能定义一个属性

  • 方法调用中,如果block参数需要换行时,block结尾的花括弧要和声明block那一行的第一个字符对齐

     [operation setCompletionBlock:^{
         [self.delegate newDataAvailable];
     }];
  • 如果方法调用中部分的代码过长,造成内嵌的block代码缩进过长,可以适当的增加手动换行,以减少代码缩进

     //如以下代码在loadWindowWithCompletionBlock前加了手动换行,是被提倡的:
     [[SessionService sharedService]
         loadWindowWithCompletionBlock:^(SessionWindow *window) {
         
             if (window) {
                 [self windowDidLoad:window];
             } 
             else {
                 [self errorLoadingWindow];
             }
     }];

注释

  • 注释应该尽量保持简洁,代码应该尽量达到能自我解释的程度

    当然用于生成文档的注释除外,用于生成文档的注释要尽量详细,特别是你的接口可能有副作用的时候,要注释清楚。

  • 注释必须和代码保持同步。不要出现代码修改了,注释不更新的情况

  • 对函数或API接口的注释,都采用Javadoc风格规范

    因为Xcode5支持直接将Javadoc风格的注释生成文档。

    也有用于添加Javadoc风格注释的Xcode插件:VVDocumenter-Xcode

    Xcode8已经内嵌VVDocumenter, 快捷键: option + command + /

命名规范

无论什么情况下,都要尽量坚持苹果的命名规范,特别是涉及到内存管理规则

这里的"内存管理规则",强调的是底层Core Foundation框架中,名字带Create或Copy的函数,返回的对象,你要负责它的释放。

类名/类别名/协议

  • 类名、类别名字及协议名字,都采用大驼峰式命名规则

  • 文件名要能反映出它所包含的类的名称

    如:NSString.h 和 NSString.m 包含了NSString类的定义和实现

  • Category的文件名要包含它所扩展的那个类的名称,并且类别名称要尽量能够描述它的功能

    UIImage+Resize.h 或 UIImage+TintColor.h

  • 在面向特定应用的代码中,类名尽量避免使用前缀,每个类都使用相同的前缀会影响可读性

    面向特定应用的代码,指那些只会在一个项目中使用的代码,不会被用于其他项目中的代码。

  • 在面向多应用的代码中,类名要使用前缀,防止命名冲突

    面向多应用的代码,指那些会被多个项目共同使用的代码。

    比如CRKit这个类库中,使用了CR前缀。

  • 建议:前缀至少使用三个字母

    此条是为了减少命名冲突。但鉴于目前流行前缀大多都是两个字母,所以此条不做强制要求

  • 协议声明或定义中,类型标识符、协议名称、尖括号之间不留空格

     @interface MyProtocoledClass : NSObject<NSWindowDelegate> 
     {
         @private
         id<MyFancyDelegate> _delegate;
     }
     
     - (void)setDelegate:(id<MyFancyDelegate>)aDelegate;
     @end

方法

  • 方法名和参数名都采用小驼峰式命名规则。

    如:- (BOOL)isFileExistedAtPath:(NSString *)filePath;

  • 方法声明中,-/+和返回值类型之间要空1个空格,方法名和参数类型之间以及参数类型和参数名之间不留空格

     - (void)invokeWithTarget:(id)target; //正确
     - (void)invokeWithTarget: (id)target; //错误
     - (void)invokeWithTarget:(id) target; //错误
     - (void)invokeWithTarget: (id) target; //错误
  • 方法声明中,参数过多超过一行时,可以增加手动换行,使每个参数占用一行,以冒号对齐

     - (void)doSomethingWith:(GTMFoo *)theFoo
                        rect:(NSRect)theRect
                    interval:(float)theInterval;
  • 方法名第一段比其他部分短时,每个参数占用一行,每行至少缩进4个空格,尽量保持参数以冒号对齐

    同时选中多行代码,用快捷键"command+["或"command+]"可以减少或增加缩进。

     - (void)short:(GTMFoo *)theFoo
                   longKeyword:(NSRect)theRect
             evenLongerKeyword:(float)theInterval
                         error:(NSError **)theError; 
  • 方法名和参数名应该尽量读起来像一句话。具体参见苹果的方法名命名规范

    如:convertPoint:fromRect: 或者 replaceCharactersInRange:withString:

  • 当各个参数是接收者的某个属性时,方法名中不要用"and"来连接

     //赞成的
     - (int)runModalForDirectory:(NSString *)path file:(NSString *) name types:(NSArray *)fileTypes;
     
     //不赞成的
     - (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
  • 如果方法名描述了两种不同的动作,要使用"and"来连接

     - (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
  • getter方法的方法名应该和变量名字相同,不允许使用"get"前缀

    本规则仅适用于Objective-C,C++使用C++的相关规范

     - (id)delegate;		// 正确
     - (id)getDelegate;	//禁止
  • 类私有方法以下划线开头

    如:- (void)_startDownloadFiles;

    Objective-C里面没有真正严格意义上私有方法。这里所说的"私有方法"指那些不需要公开的、只会在实现文件中使用的方法。

    这样做的好处是,可以直观的快速区别实现文件中的私有方法和公有方法。

    这样做会很便于重构。如果某个方法废弃了,需要移除的时候,发现它是以下划线开头的,那么就可以确定这个方法是私有的,只会在这个实现文件中被用到。那么直接在该实现文件搜索这个方法的名字,然后清理掉搜索到的地方就可以了。不必再在整个项目中查找是否没有清理干净。

    根据苹果的建议,这种做法可能覆盖掉父类的私有方法。 但是目前还没有遇到过这种情况,而且我们认为此条约定带来的好处远远大于它潜在的危险,因此仍然推行这条约定。

函数

函数指纯C函数,这里提倡与苹果风格类似的约定。

  • 函数名采用大驼峰式命名方式,参数名采用小驼峰式命名方式

  • 如果函数和某个特定类型相关,那么函数名前缀要和类型前缀一样

    CGRectMake()CGContextCreate()

常量

  • 创建NSString, NSDictionary, NSArray, 以及NSNumber等常量时,使用Literals语法

     //例如:
     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", @"Mobile Web", nil]; 
     NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES]; 
     NSNumber *ZIPCode = [NSNumber numberWithInteger:10018];
  • 定义枚举常量时,使用NS_ENUM或NS_OPTIONS

    NS_ENUM和NS_OPTIONS都提供了类型检查

     //例如:
     typedef NS_ENUM(NSUInteger, PPNavBarButtonColor) {
         PPNavBarButtonColorBlack,
         PPNavBarButtonColorGreen,
         PPNavBarButtonColorDefault = PPNavBarButtonColorBlack
     };
     
     typedef NS_OPTIONS(NSUInteger, PSTCollectionViewScrollPosition) {
         PSTCollectionViewScrollPositionNone                 = 0,
     	
         PSTCollectionViewScrollPositionTop                  = 1 << 0,
         PSTCollectionViewScrollPositionCenteredVertically   = 1 << 1,
         PSTCollectionViewScrollPositionBottom               = 1 << 2,
     	
         PSTCollectionViewScrollPositionLeft                 = 1 << 3,
         PSTCollectionViewScrollPositionCenteredHorizontally = 1 << 4,
         PSTCollectionViewScrollPositionRight                = 1 << 5
     };
  • 定义常量时,除非明确的需要将常量当成宏使用,否则优先使用const,而非#define

  • 只在某一个特定文件里面使用的常量,用static

    static关键字保证变量只有文件作用域,可以避免变量名重名造成的链接错误问题。

    比如:

    static CGFloat const RWImageThumbnailHeight = 50.0;

  • 常量名以小写k开头,采用首字母大写的方式来分割单词

     //例如:
     const int kNumberOfFiles = 12;
     NSString *const kUserKey = @"kUserKey";
     enum DisplayTinge {
     	kDisplayTingeGreen = 1,
     	kDisplayTingeBlue = 2
     };
  • 和特定类型相关的枚举常量使用类名作为前缀,而不用小写k开头。

     typedef NS_OPTIONS(NSUInteger, UICollectionViewScrollPosition) {
         UICollectionViewScrollPositionNone                 = 0,
         UICollectionViewScrollPositionTop                  = 1 << 0,
         UICollectionViewScrollPositionCenteredVertically   = 1 << 1,
         UICollectionViewScrollPositionBottom               = 1 << 2,
         UICollectionViewScrollPositionLeft                 = 1 << 3,
         UICollectionViewScrollPositionCenteredHorizontally = 1 << 4,
         UICollectionViewScrollPositionRight                = 1 << 5
     };

变量

  • 属性名和变量名都采用小驼峰式命名规则

  • 实例变量名以下划线开头,局部变量不能以下划线开头

  • 禁止使用匈牙利标记法或含糊不清的缩写单词来命名变量

    for循环中的i、j、k这种情况例外。

    Objective-C中,变量名应该尽量清楚的描述它的用途。这样可以使别人立即明白代码的意思,不要担心这样会导致代码过长。

     //以下这些都是错误的命名规范
     int w;
     int nerr;
     int nCompConns;
     tix = [[NSMutableArray alloc] init];
     obj = [someObject object];
     p = [network port];
     
     //以下这些才是赞成的命名规范
     int numErrors;
     int numCompletedConnections;
     tickets = [[NSMutableArray alloc] init];
     userInfo = [someObject object];
     port = [network port];
  • 指针符号 "*" 靠近变量名字。(常量定义除外)

     NSString *varName; //赞成的
     
     NSString* varName; //不赞成的
  • 使用property时,优先使用点语法

    使用点语法会让代码简洁。但对于其他情况,都应该使用方括号语法。

     //赞成的
     NSInteger arrayCount = [self.array count];
     view.backgroundColor = [UIColor orangeColor];
     [UIApplication sharedApplication].delegate;
     
     //不赞成的
     NSInteger arrayCount = self.array.count;
     [view setBackgroundColor:[UIColor orangeColor]];
     UIApplication.sharedApplication.delegate;

通知和异常

  • 通知名字的命名规则:[相关联的类名字] + [Did | Will] + [独一无二的一段名称] + Notification

    如:UIApplicationDidBecomeActiveNotification

  • 异常名字的命名规则:[前缀] + [独一无二的一段名称] + Exception

    如:NSColorListIOException

布尔值

  • Objective-C的布尔值只使用YESNO

  • truefalse只能用于CoreFoundation,C或C++的代码中

  • 禁止将某个值或表达式的结果与YES进行比较

    因为BOOL被定义成signed char。这意味着除了YES(1)和NO(0)以外,它还可能是其他值。

    因此C或C++中的非0为真并不一定就是YES

     //以下都是被禁止的
     - (BOOL)isBold {
         return [self fontTraits] & NSFontBoldTrait;
     }
     
     - (BOOL)isValid {
         return [self stringValue];
     }
     
     if ([self isBold] == YES) {
     	//...
     }
     
     
     //以下才是赞成的方式
     - (BOOL)isBold {
         return ([self fontTraits] & NSFontBoldTrait) ? YES : NO;
     }
     
     - (BOOL)isValid {
         return [self stringValue] != nil;
     }
     
     - (BOOL)isEnabled {
         return [self isValid] && [self isBold];
     }
     
     if ([self isBold]) {
     	//...
     }
  • 虽然nil会被直接解释成NO,但还是建议在条件判断时保持与nil的比较,因为这样代码更直观。

     //比如,更直观的代码
     if (someObject != nil) {
     	//...
     }
     
     //没那么直观的代码
     if (!someObject) {
     	//...
     }
  • 在C或C++代码中,要注意NULL指针的检测。

    向一个nil的Objective-C对象发送消息不会导致崩溃。但由于Objective-C运行时不会处理给NULL指针的情况,所以为了避免崩溃,需要自行处理对于C/C++的NULL指针的检测。

  • 如果某个BOOL类型的property的名字是一个形容词,建议为getter方法加上一个"is"开头的别名。

     @property (assign, getter = isEditable) BOOL editable;
  • 在方法实现中,如果有block参数,要注意检测block参数为nil的情况。

     - (void)exitWithCompletion:(void(^)(void))completion {
     	// 错误。 如果外部调用此方法时completion传入nil,此处会发生EXC_BAD_ACCESS
         completion();
         
         // 正确。如果completion不存在则不调用。
         if (completion) {
             completion();
         }
     }
    

条件语句

  • 条件语句的语句体,即便只有一行,也不能省略花括弧

    这样可以减少失误。比如你在if语句体中增加第二行语句的时候,就可能会因为没有花括号而导致新增的第二行语句没有被包含在if语句体中。另外,这里还提到了其他的一些危险情况。

     //赞成的
     if (error == nil) {
       return success;
     }
     
     //不赞成的
     if (error == nil)
       return success;
       
     //
     if (error == nil) return success;
  • 多层嵌套的条件语句,优先考虑条件不成立可以立即跳出的情况

    Objective-C的代码普遍比较长,如果再加上多层嵌套的条件语句,代码缩进会增多,代码会变得更长,会影响可读性。比如,下面这种情况,换成优先考虑可以跳出的情况,可以有效的减少代码缩进长度:

     //一般流程
     if (a) {
     
         if (b) {
         
             if (c) {
             
             	//do something
            	} 
         } 
     
     } 
     
     
     //优先考虑可以跳出的流程
     if (!a) {
     	return;
     }
     
     if (!b) {
     	return;
     }
     
     if (!c) {
     	return;
     }
     
     //do something
  • 三目运算只有在能增加代码清晰度和整洁度的时候才推荐使用

    三目运算符(?:),如果不能增加代码整洁度和清晰度,使用时就要谨慎。特别是,嵌套使用多个三目运算,这种要尽量避免。因为它会使代码更难阅读。

    另外,三目运算符中的条件判断是一个语句,最好用小括号括起来。如果直接是一个布尔值则无需括号。例如:

     //赞成的
     NSInteger value = 5;
     result = (value != 0) ? x : y;
     
     BOOL isHorizontal = YES;
     result = isHorizontal ? x : y;
     
     //不赞成的
     result = a > b ? x = c > d ? c : d : y;

初始化方法

  • 初始化方法的返回类型用instancetype

    关于instancetype的介绍参见NSHipster.com

CGRect函数

  • 访问CGRect中的x、y、width或height元素时,不直接访问而是使用CGGeometry相关函数

    CGGeometry里面的函数,会对CGRect参数进行隐式的标准化处理,然后再计算结果。因此,你应该避免直接读取或重写CGRect数据结构里面的值,而要使用这些函数来进行相关操作。

    什么叫标准化处理,参见CGGeometry Reference的Overview章节。

     //赞成的
     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 };

Xcode工程结构

  • 实体文件应该和Xcode工程文件保持同步,防止出现文件不一致

    任何手动创建的Xcode Group都应该在文件系统有一个对应的文件夹。代码不仅要根据类型组织,更要以更加清晰的特征来区分归类。

  • 建议:在可能的情况下,始终要勾选在Build设置选项中”Treat Warnings as Errors(将告警视为错误)“选项。同时尽可能多的暴露更多的additional warnings(附加告警)。如果要忽略某类特定Warning(告警),请使用Clang's pragma feature

    此条不做强制要求,但是"将警告视为错误"是你应当要有的态度。

Just for fun

最后贴张图娱乐一下,虽然说Objective-C中长名是美德,但是什么东西还是要有个度。有人写了个脚本统计Cocoa Framework中各种最长的命名,结果发现低估了苹果程序员的造句能力。Mac平台最长的常量名96个字符,最长的方法名150个字符,C函数名都能到68个字符! -_-# 泥煤,自从学会了Objective-C,妈妈再也不用担心我的造句能力了。