iOS Objective-C 编码规范

这份规范指南概括了本 iOS 团队的代码约定。

目录

  • 源文件排版
  • 命名
  • 常量,全局量,静态量
  • 代码的文档化
  • Objective-C最佳实践
  • Xcode工程

源文件排版

1.缩进

  • 一个缩进使用4个空格,不要使用制表符(tab)进行缩进。请确保在Xcode中正确设置。

2.空格

  • 关键字(if/else/for/switch/while 等等)与后面的内容要加空格。
  • 双目运算符,比较运算符,逻辑运算符与左,右操作数之间要加空格。
  • for循环中,;之后要加空格。
  • 在方法签名中,在 -/+ 符号后应该有一个空格。
  • @property和其后的括号之间要加一个空格。
  • @property之后的括号中,每个attribute之后的逗号之后加一个空格。
  • <>中的多个协议之间,要加一个空格。

3.大括号

  • 方法的大括号和其他的大括号(if/else/for/switch/while 等等)始终和声明在同一行开始,在新的一行结束。
  • if/else/for/switch/while 等等,应始终在其后使用大括号,即使其后的代码块只有一行代码。

4.空行和换行

  • else应单独占一行,其左边不应再出现右大括号。
  • @interface, @implementation, @protocol之后,@end之前,都要有空行。
  • 方法之间要加空行。
  • 使用空行来分隔方法中的功能块,可以提高代码的可读性。
  • @synthesize@dynamic 在实现中每个都应该占一个新行。
  • 每个源文件,不管是头文件,还是实现文件,以空行结束。

5.导入

如果有一个以上的 import 语句,就对这些语句进行分组。每个分组的注释是可选的。
注:对于模块使用@import语法。

// Frameworks
@import QuartzCore;

// Models
#import "NYTUser.h"

// Views
#import "NYTButton.h"
#import "NYTUserView.h"

6.将代码分组

  • 在头文件和实现文件中,都可以将属性,方法进行合理的分组。属于某功能或逻辑的代码放在一起,便于阅读和维护。
  • dealloc 方法应该放在实现文件的最上面,并且刚好在 @synthesize@dynamic 语句的后面。在任何类中,init 都应该直接放在 dealloc 方法的下面。
  • 在每一组代码的开头处,写上#pragma mark - xxx,其中’xxx’用于解释这组代码的用途。

推荐:

int x = 0;
x += 1;

if (x < 2 && y < 3) {
}

for (int k = 0; k < 10; k++) {
}

- (void)setExampleText:(NSString *)text image:(UIImage *)image;

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

@interface NYTSection: NSObject<UITableViewDataSource, UITableViewDelegate>

@property (copy, nonatomic) NSString *headline;

@end

命名

1.语言

  • 不要使用汉语拼音来命名
  • 使用英国英语,例如:使用color,而不是colour
  • 不能出现单词拼写错误

2.前缀

  • 选择一个与公司,应用或者二者均有关联的名称作为命名时使用的前缀。
  • 前缀最好是三个字母,因为苹果保留了使用所有的两字母前缀的权利。
  • 类名,协议名,分类名,常量名,都要加大写的前缀
  • 自定义分类的方法,要加小写的前缀和下划线。

3.变量(属性,实例变量,局部变量)

  • 使用驼峰命名法,首字母小写,之后的每个单词的首字母大写。
  • 使用完整的单词,不要嫌名字过长。只有当一个缩写是众所周知的,才可以使用。
  • 单字母的变量名,只应当出现在for循环这样的场合。
  • 不要在名字前加任何前缀,如m, p等。
  • 实例变量以下划线开头。
  • 指针类型的*号,要紧跟变量名,不加空格。例如:NSString *text;,不能是NSString* text;或是NSString * text;

4.方法

  • 方法名以小写字母开头,之后每个单词的首字母大写。
  • 如果第一个单词是TIFF或是PDF这样的众所周知的缩写,则不必小写字母开头。
  • 定义在Category中的方法,要加app或是organization相关的前缀,避免命名冲突。
  • 每个参数前的关键字不能省略,且要正确的描述参数的用途。
  • 不要在关键字之前加and或者with,只有当一个方法描述了不同的动作时,才用and来连接。

5.通知

通知的命名格式为:[类名] + [Did | Will] + [描述] + Notification
例子:

UIApplicationDidEnterBackgroundNotification
NSApplicationDidBecomeActiveNotification
NSWindowDidMiniaturizeNotification
NSTextViewDidChangeSelectionNotification

6.异常

异常的命名格式为:[Prefix] + [UniquePartOfName] + Exception
例子:

NSColorListIOException
NSColorListNotEditableException
NSDraggingException
NSFontUnavailableException
NSIllegalSelectorException

常量,全局量,静态量

  • 不要在代码中出现常量的字符串,用于文案的显示。
  • 不要再使用宏(#define)来定义常量,使用const,枚举来替代。
  • 常量名字前加前缀,每个单词的首字母都大写。
  • 常量要定义在.m, .c, .cpp文件中,在头文件中进行extern声明。
  • 如果常量仅在某个实现文件中使用,则在定义的时候加static关键字,可以避免符号重定义的错误。
  • 如果一个全局量是非const的,则名字前加g。
    例子:
.h:
extern NSString * const RWTAboutViewControllerCompanyName;
extern CGFloat const RWTImageThumbnailHeight;
extern BOOL gRWTEnableHardwareEncoding;

.m:
NSString * const RWTAboutViewControllerCompanyName = @"RayWenderlich.com";
CGFloat const RWTImageThumbnailHeight = 50.0;
BOOL gRWTEnableHardwareEncoding = YES;

static NSInteger const RWTMaxNameLength = 20;

代码的文档化

参考文章:https://www.raywenderlich.com/66395/documenting-in-xcode-with-headerdoc-tutorial
使用HeaderDoc规范来写代码的文档,可以使得我们能够象查看官方文档一样,查看自己的代码的文档,而不必跳转到对方的头文件去查看注释。

1.选择注释符号

  • 对于单行的注释,使用///
  • 对于块注释,使用
    /** 一段文字 */
    或者
    /*! 一段文字 */

2.使用Tags

  • @brief, @abstract
    用于添加简要的描述信息
  • @discussion
    和@brief, @abstract类似,不过允许多行
  • @param
    用于描述函数参数的名字,含义
  • @return
    用于描述函数的返回值
  • @typedef
    用于说明这是一个类型定义
  • @enum
    用于说明这是一个枚举定义
  • @constant
    用于为枚举值添加注释
  • @field
    用于为结构体中的字段添加注释
  • @code
    用于在文档中添加示例代码
  • @warning
    用于添加警告信息

3.示例

@interface MathAPI : NSObject

/*!
 * @discussion A really simple way to calculate the sum of two numbers.
 * @param firstNumber An NSInteger to be used in the summation of two numbers
 * @param secondNumber The second half of the equation.
 * @warning Please make note that this method is only good for adding non-negative numbers.
 * @return The sum of the two numbers passed in.
 */
+ (NSInteger)addNumber:(NSInteger)firstNumber toNumber:(NSInteger)secondNumber;

@end
/*!
 * @enum CarType
 * @brief A list of newer car types.
 * @constant CarTypeHatchback 
                        Hatchbacks are fun, but small.
 * @constant CarTypeSedan
                        Sedans should have enough room to put your kids, and your golf clubs
 * @constant CarTypeEstate
                        Estate cars should hold your kids, groceries, sport equipment, etc.
 * @constant CarTypeSport
                        Sport cars should be fast, fun, and hard on the back.
*/
typedef NS_ENUM(NSInteger, CarType){
    /// Hatchbacks are fun, but small.
    CarTypeHatchback,
    
    /// Sedans should have enough room to put your kids, and your golf clubs
    CarTypeSedan,
    
    /// Estate cars should hold your kids, groceries, sport equipment, etc.
    CarTypeEstate,
    
    /// Sport cars should be fast, fun, and hard on the back.
    CarTypeSport
};

/*!
 * @typedef Old Car Types
 * @brief A list of older car types.
 * @constant OldCarTypeModelT A cool old car.
 * @constant OldCarTypeModelA A sophisticated old car.
 */
typedef enum {
    OldCarTypeModelT,
    OldCarTypeModelA
} OldCarType;

/*!
 * @brief A block that makes the car drive.
 * @param distance The distance is equal to a distance driven when the block is ready to execute. It could be miles, or kilometers, but not both. Just pick one and stick with it. ;]
 */
typedef void(^driveCompletion)(CGFloat distance);

@interface Car : NSObject

@property (nonatomic) UIColor *exteriorColor;

@property (nonatomic) NSString *nickname;

/// Indicates the kind of car as enumerated in the "CarType" NS_ENUM
@property (nonatomic, assign) CarType carType;

/*!
 * @brief The car will drive, and then execute the drive block
 * @param completion A driveCompletion block
 * @code [car driveCarWithCompletion:^(CGFloat distance){
     NSLog(@"Distance driven %f", distance);
 }];
 */
- (void)driveCarWithCompletion:(driveCompletion)completion;

@end

Objective-C 最佳实践

1.点语法

  • 访问其它类的属性时,使用点语法,而不是普通的加方括号的函数调用形式。
  • 在自定义类的内部实现中,对于property仍然是尽量使用点语法,但以下4个场合除外:init, dealloc, 自定义的setter,自定义的getter。在init, dealloc中使用点语法,会有造成副作用的危险。而在自定义的访问方法中,使用点语法会造成无限的递归调用。
    关于副作用的讨论,可以看Stack Overflow上的一篇讨论:Initializing a property, dot notation

2.使用模板的写法

  • 在声明集合类型的变量或属性时,如果有可能,要指出里面的元素的类型。这样做的好处是,写代码的时候编辑器会给出代码提示。并且,如果输入了对应类型不支持的方法,编译器会给出警告,有助于及早的发现错误。
  • 在使用IBOutletCollection时,Xcode产生的代码没有使用模板的写法,我们可以手动去修改。

3. init方法或是类的工厂方法中,使用instancetype作为返回类型

目的是为了提高类型安全,在编译器就可以发现错误的函数调用。

4. 使用NS_DESIGNATED_INITIALIZER来标识指定的初始化函数

  • 指定的初始化函数,拥有最多的参数,是最灵活的初始化函数。
  • 其它的初始化函数,被称为便利初始化函数,是为了使用时方便,它们需要的参数比较少,未提供的参数使用默认值。这些初始化函数本身并不会去实现初始化过程,它们都是简单的调用指定的初始化函数。

5.使用新式语法

  • 构建字面量
    NSString:@”Hello”
    NSNumber: @(100)
    NSNumber: @(YES)
    NSArray: @[@”1″, @”2″, @”3″]
    NSDictionary: @{@”1″ : @”Hello”, @”2″ : @”World”}

  • 使用下标运算符
    对于字典和数组,当访问里面的元素时,不再建议调用[NSArray objectAtIndex:]或[NSDictionary objectForKey:],而是应该使用更简洁,可读性更好的下标运算符。

6.类的属性声明

  • @property之后的括号中,要显式的写出所有需要的attribute,特别是不能省略内存管理的attribute,不要让编译器去使用默认值。
  • 声明attribute时的顺序:内存管理,原子性,其它。这样可以和Xcode为IBOutlet产生的代码保持一致。
  • 如果一个属性不想被外部写入,那么应该在头文件中使用readonly关键字。如果在类实现中需要读写该属性的值,那么可以重新声明该属性,并去掉readonly关键字,或将之改为readwrite关键字。
  • 不必再写@synthesize,除非少了它就不能编译通过。如果一个类所遵从的协议中声明了属性,那么该类需要使用@synthesize
  • 对于NSString, NSArray, NSDictionary类型的属性,建议使用copy关键字。如果使用strong,然后将一个可变类型的对象赋值给该属性,那么在赋值以后,该对象可能会被改变,从而造成一些运行期的bug出现。
  • 不需要暴露给外部的属性,将其在类的扩展里声明。
  • 如果大于1个的同类型的IBOutlet,可以被放在一个数组里,那么不要声明单个的IBOutlet,而是声明IBOutletCollection类型的属性。

7.枚举和位域的定义

  • 定义枚举,使用NS_ENUM,并遵循苹果的命名规范。例如:
typedef NS_ENUM(NSInteger, UIViewAnimationCurve) {
    UIViewAnimationCurveEaseInOut,         // slow at beginning and end
    UIViewAnimationCurveEaseIn,            // slow at beginning
    UIViewAnimationCurveEaseOut,           // slow at end
    UIViewAnimationCurveLinear,
};
  • 定义位域,使用NS_OPTIONS,命名规范同枚举。例如:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

8.使用nonnull, nullable来修饰函数的参数或是类的属性

加上此类修饰符,可以提高代码的可读性,调用者不需要查看源代码,就可以明白设计者的意图,并据此传递正确的参数。
如果对一个nonnull修饰的参数或是属性,传入一个nil参数,那么在输入代码以后,IDE就可以给出警告,这样就可以第一时间发现问题。

9.访问数组元素时要小心出现下标越界的错误

  • 取数组元素时,为安全起见,总是要判断下标是否越界。
  • 如果是取第一个,或是最后一个元素,那么可以调用firstObject'lastObject方法,这两个方法是安全的。

10.检查参数是否为nil的几种情况

  • 针对nil调用方法,我们都知道什么也不会发生,很安全,但其它情况下使用nil就未必了。
  • 往数组,字典中添加nil对象时,会导致崩溃。因此,最好总是检查参数的值是否有效,除非非常确信是有效的。
  • 使用NSJsonSerializationJSONObjectWithData:options:error:方法时,如果data参数为nil,也会导致崩溃,所以这里也需要检查。

11.单例

单例对象应该使用线程安全的模式创建共享的实例。

+ (instancetype)sharedInstance {
    static id sharedInstance = nil;

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });

    return sharedInstance;
}

12.使用CGRect函数

当访问一个 CGRectxywidthheight 时,应该使用CGRect相关的函数代替直接访问结构体成员。苹果的 CGGeometry 参考中说到:

All functions described in this reference that take CGRect data structures as inputs implicitly standardize those rectangles before calculating their results. For this reason, your applications should avoid directly reading and writing the data stored in the CGRect data structure. Instead, use the functions described here to manipulate rectangles and to retrieve their characteristics.

推荐:

CGRect frame = self.view.frame;

CGFloat x = CGRectGetMinX(frame);
CGFloat y = CGRectGetMinY(frame);
CGFloat width = CGRectGetWidth(frame);
CGFloat height = CGRectGetHeight(frame);

反对:

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;

13.错误处理

当引用一个返回错误参数(error parameter)的方法时,应该针对返回值,而非错误变量。

推荐:

NSError *error;
if (![self trySomethingWithError:&error]) {
    // 处理错误
}

反对:

NSError *error;
[self trySomethingWithError:&error];
if (error) {
    // 处理错误
}

14.头文件中要尽可能少的import其它的头文件

  • 在声明中用到其它类类型时,优先使用前向声明,除非使用前向声明不能通过编译。
  • #import语句尽可能的转移到.m文件中。
    这样做可以减少文件间的编译依赖,特别是某个文件修改时,能减少需要编译的文件的数量,加快编译速度。

Xcode 工程

  • 若无特殊需要,严禁在Project Navigator中使用Group功能,Navigator中的目录结构,应该和文件系统中的实际目录严格对应。
  • Build时不能产生警告。有些警告如果自己确定没有问题,可以想办法消除掉。例如针对performSelector的警告可以这样消除:
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:sel withObject:arguments];
#pragma clang diagnostic pop
  • 图片资源,需要提供所有的scale版本,且应该是严格的1x, 2x, 3x的关系。这是因为如果图片的大小和视图的大小不一致,会产生像素不对齐的问题,伤性能。
  • 除非特殊情况(如做本地化),图片资源都应该使用Assets进行管理,这样做的好处有两个:1)下载后的安装包里,只会包含一套@2x或是@3x的资源,而不会2套资源都包含。这属于苹果在应用瘦身方面做的一项优化。2)相比文件夹方式,加载速度快。
  • 图片资源应该根据使用场景进行合理的目录划分,但在Assets中最好避免图片很多的目录,因为点击一个大目录时,Xcode有时容易无响应。
  • 图片资源不要用中文命名,不要用拼音命名,也不要进行简单的命名。正确的命名格式如:profile_follow_button_normal.png, profile_follow_button_pressed.png, loading_animation_1.png。合理的命名,有助于找出工程中的垃圾资源。
  • 程序运行时,不能有布局错误的警告。
  • 程序最好能同时在模拟器和实际设备上运行。在模拟器上运行的好处:1)方便测试屏幕适配 2)检查一些性能问题,如像素不对齐,离屏渲染等。
    原文作者:buptwsg
    原文地址: https://www.jianshu.com/p/e33331291254
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞