我的 Cocoa Programming Guidelines

代码外观

关于头文件的定义

每个头文件定义应该保持简洁和有序像下面这样:

  1. Imports
  2. Forward class declarations
  3. Enumeration types
  4. Constants
  5. Notification names and its user info dictionary keys
  6. Delegate protocol
  7. Data source protocol
  8. Class interface

接口和方法等应该有统一的前缀

  1. Properties
  2. Data source property
  3. Delegate property
  4. Class methods
  5. Instance methods
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#import <Foundation/Foundation.h>
@class ExampleClass;
typedef NS_ENUM(NSInteger, Enumeration) {
EnumerationA,
EnumerationB
};
extern CGFloat const ExampleClassDefaultHeight;
extern NSString * const ExampleClassWillPerformActionNotification;
extern NSString * const ExampleClassDidPerformActionNotification;
extern NSString * const ExampleClassActionNameKey;
@protocol ExampleClassDelegate <NSObject>
- (void)exampleClass:(ExampleClass *)exampleClass didPerformAction:(Action *)action;
@optional
- (void)exampleClass:(ExampleClass *)exampleClass willPerformAction:(Action *)action;
@end
@protocol ExampleClassDataSource <NSObject>
- (NSInteger)exampleClassNumberOfActions:(ExampleClass *)exampleClass;
@end
@interface ExampleClass : NSObject
@property (strong, nonatomic) NSURL * initialProperty;
@property (weak, nonatomic) id<ExampleClassDataSource> dataSource;
@property (weak, nonatomic) id<ExampleClassDelegate> delegate;
+ (id)exampleClassWithInitialProperty:(NSURL *)initialProperty;
- (id)initWithInitialProperty:(NSURL *)initialProperty;
- (void)performAction;
@end

实现文件定义

实现文件中应该使用“pragma marks”进行不同功能模块的划分,不同部分不同mark组

1
2
3
4
5
6
7
8
9
10
#pragma mark - Public Properties
#pragma mark - Public Class Methods
#pragma mark - Public Instance Methods
#pragma mark - IBActions
#pragma mark - Overridden
#pragma mark - Private Properties
#pragma mark - Private Class Methods
#pragma mark - Private Instance Methods
#pragma mark - Protocols
#pragma mark - Notifications

1
2
3
#pragma mark - Overridden (UIView)
#pragma mark - Overridden (UIContainerViewControllerCallbacks)
#pragma mark - Overridden (UIViewControllerRotation)

属性的定义应该保持顺序

|[assign | weak | strong | copy] + [nonatomic | atomic] + [readonly | readwrite] + [getter = ]|

如都为readwrite属性则能省略readwrite关键字

1
2
3
4
@property (assign, nonatomic) CGFloat height;
@property (strong, nonatomic) UIColor * color;
@property (copy, nonatomic) NSString * name;
@property (weak, nonatomic) id <UITableViewDelegate> delegate;

对于boolean属性的get方法应该显示定义和重命名系统默认生成的get方法

1
2
3
@property (assign, nonatomic, getter = isVisible) BOOL visible;
@property (assign, nonatomic, getter = isEnabled) BOOL enabled;
@property (assign, nonatomic, getter = isTracking) BOOL tracking

协议和常量的命名应该添加类前缀

协议,通知,枚举和其他的常量定义应该加上类的前缀

1
2
3
4
5
6
7
8
9
10
11
12
typedef NS_ENUM(NSInteger, UITableViewStyle) {
UITableViewStylePlain,
UITableViewStyleGrouped
};
UIKIT_EXTERN NSString *const UITableViewIndexSearch;
UIKIT_EXTERN const CGFloat UITableViewAutomaticDimension;
UIKIT_EXTERN NSString *const UITableViewSelectionDidChangeNotification;
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate>

IBOutlets应该声明为私有并定义成类的扩展

Outlets 应该使用 weak 属性声明并在类扩展中靠前部分声明和其他属性的定义分开

1
2
3
4
5
6
7
8
9
@interface PanelViewController ()
@property (weak, nonatomic) IBOutlet UIButton * infoButton;
@property (weak, nonatomic) IBOutlet UIButton * closeButton;
@property (weak, nonatomic) IBOutlet UILabel * descriptionLabel;
@property (weak, nonatomic) UIView * overlayView;
@end

方法的实现应该尽早返回

方法实现部分如果过多的嵌套会让代码阅读变得困难

1
2
3
4
5
6
7
8
9
- (BOOL)loginUser:(NSString *)user withPassword:(NSString *)password
{
if (user.length >= 6) {
if ((password.length >= 8) {
// the actual logging code
}
}
return NO;
}

对应方法实现中参数应该首先判断参数是否有效

1
2
3
4
5
6
- (BOOL)loginUser:(NSString *)user withPassword:(NSString *)password
{
if (user.length < 6) return NO;
if (password.length < 8) return NO;
// actual logging code
}

这样可以更早的添加错误处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BOOL)loginUser:(NSString *)user withPassword:(NSString *)password error:(NSError **)error
{
if (user.length < 6) {
if (error != NULL)
*error = [NSError errorWithDomain:ExampleDomain
code:1001
userInfo:@{NSLocalizedDescriptionKey: @”User has to...”}];
return NO;
}
if (password.length < 8) {
if (error != NULL)
*error = [NSError errorWithDomain:ExampleDomain
code:1002
userInfo:@{NSLocalizedDescriptionKey: @”Password has to...”}];
return NO;
}
// actual logging code
}

方法实现部分中不应该使用if else或其他进行平分如:

1
2
3
4
5
6
7
8
9
- (void)method
{
if (self.valueX == 10) {
// perform some actions
}
else {
// perform some other actions
}
}

General 规则

引用头文件仅在需要的时候

引用头文件应该遵循下面规则:

  1. 如果类需要遵循某个协议 那么需要引入协议声明的头文件
  2. 如果类继承其他的类 那么需要引入父类头文件
  3. 如果类在接口部分使用了其他类的枚举 那么需要引入枚举定义的头文件
  4. 其他一些必须引入头文件的情况

Delegate方法应该带着发送对象

Delegate方法第一个参数应该为发送方对象

1
2
3
- (NSInteger)exampleClassNumberOfActions:(ExampleClass *)exampleClass
- (void)exampleClass:(ExampleClass *)exampleClass willPerformAction:(Action *)action;
- (void)exampleClass:(ExampleClass *)exampleClass didPerformAction:(Action *)action;

属性如果有默认值应该注释标明

1
2
3
4
/*
* A Boolean value that determines whether the view is hidden.
*/
@property(nonatomic, getter=isHidden) BOOL hidden

Init方法应该像这样

所有的设置参数应该通过Init方法进行初始化如:

1
2
@property (strong, nonatomic, readonly) DownloaderMode downloaderMode;
- (id)initWithDownloaderMode:(DownloaderMode)downloaderMode;

或者如果你需要一个通用的方法创建不同实例那应该写一个工厂方法

抽象类Init应该加断言避免创建抽象实例如:

1
2
3
4
5
6
7
8
9
- (id)init
{
self = [super init];
if (self) {
NSAssert1([self isMemberOfClass:[MyAbstractClass class]] == NO,
@”%@ is an abstract class. Please do not create instances of it.”, [self class]);
}
return self;
}

抽象方法应该抛出一个异常

强制子类去实现抽象类的方法时候,应该在抽象类方法中抛出异常避免调用抽象类的实例方法

1
2
3
4
- (void)abstractMethod
{
[NSException raise:NSInternalInconsistencyException format:@”It’s template method. Implementation must be provided in subclass.”];
}

默认情况下尽量使用高层的API

1
2
3
4
5
6
7
8
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// code
}];
//如需要更多控制则再使用GCD
dispatch_sync(dispatch_get_main_queue(), ^{
// code
}

惰性初始化可以节省内存的使用

1
2
3
4
5
6
7
- (NSMutableDictionary *)cacheDictionary
{
if (_cacheDictionary == nil) {
_cacheDictionary = [NSMutableDictionary dictionary];
}
return _cacheDictionary;
}

但请注意它不是线程安全的,如2个线程同时试着初始化_cacheDictionary对象

方法内部尽量不使用NSError对象作为返回值

应该使用nil或NO作为失败,YES或非0值代表成功如:

1
2
- (id)initWithContentsOfURL:(NSURL *)aURL;
- (BOOL)writeToURL:(NSURL *)aURL atomically:(BOOL)atomically;

如果失败时NSError对象应该做为附加信息返回

1
2
3
4
- (id)initWithContentsOfURL:(NSURL *)aURL options:(NSDataReadingOptions)mask
error:(NSError **)errorPtr;
- (BOOL)writeToURL:(NSURL *)aURL options:(NSDataWritingOptions)mask
error:(NSError **)errorPtr

自定义错误信息

自定义错误应该带着error domain和error code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
extern NSString *const MyErrorDomain;
typedef NS_ENUM(NSInteger, MyErrorCode) {
MyErrorCode1,
MyErrorCode2,
MyUnknownErrorCode,
};
if (error != NULL) {
if(error_situation_1) {
error* = [NSError errorWithDomain:MyErrorDomain
code:MyErrorCode1
userInfo:@{NSLocalizedDescriptionKey: @”Description of error 1”}];
}
else if (error_situation_2) {
error* = [NSError errorWithDomain:MyErrorDomain
code:MyErrorCode2
userInfo:@{NSLocalizedDescriptionKey: @”Description of error 2”}];
}
else {
error* = [NSError errorWithDomain:MyErrorDomain
code:MyUnknownErrorCode
userInfo:@{NSLocalizedDescriptionKey: @”Unknown error”}];
}
}

可以向已存在的类中加入属性 分类+associated Object

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void * const navigationItemKey = (void *)&navigationItemKey;
- (void)setNavigationItem:(UINavigationItem *)navigationItem
{
objc_setAssociatedObject(self,navigationItemKey,navigationItem,OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UINavigationItem *)navigationItem
{
UINavigationItem * navigationItem = objc_getAssociatedObject(self, navigationItemKey);
if (navigationItem == nil) {
navigationItem = [[UINavigationItem alloc] init];
self.navigationItem = navigationItem;
}
return navigationItem;
}

并发性规则

GCD大多数情况下使用默认优先级队列DISPATCH_QUEUE_PRIORITY_DEFAULT避免优先级带来的问题

互斥代码应该加入串行队列避免使用NSLock和@synchronized

1
2
3
4
5
6
7
8
9
- (id)init
{
self = [super init];
if (self) {
NSString * label = [NSString stringWithFormat:@”%@.isolationQueue.%p”, [self class], self];
_isolationQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
}
return self;
}

创建队列时应该像上面那样为队列添加标签方便调试

1
2
3
4
5
6
7
8
9
10
11
12
- (void)startDownloading
{
dispatch_async(self.isolationQueue, ^{
// critical code section A
}
}
- (void)cancelDownloading
{
dispatch_async(self.isolationQueue, ^{
// critical code section B
}
}

多个读一个写的情况

并发队列为了保证同步访问属性应该采用dispatch_barrier_async方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- (id)init
{
self = [super init];
if (self) {
NSString * queueLabel = [NSString stringWithFormat:@”%@.syncQueue.%p”, [self class], self];
_syncQueue = dispatch_queue_create([queueLabel UTF8String], DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (void)setObject:(id)anObject forKey:(id <NSCopying>)aKey
{
aKey = [aKey copyWithZone:NULL];
dispatch_barrier_async(self.syncQueue, ^{
[self.mutableDictionary setObject:anObject forKey:aKey];
});
}
- (id)objectForKey:(id)aKey
{
__block id object;
dispatch_sync(self.syncQueue, ^{
object = [self.mutableDictionary objectForKey:aKey];
});
return object;
}

NSOperation在执行前是可以被取消的

main方法在执行前检查是否已取消

1
2
3
4
5
- (void)main
{
if (self.isCancelled) return;
// code
}

UIView相关

View的接口

UIView应该暴露所需最小的属性和方法集

特殊View的接口

特殊View的接口应该暴露出一个方法用于配置对应的MODEL对象

1
2
3
@interface PersonTableViewCell : UITableViewCell
- (void)configureWithPerson:(Person *)person
@end

#未完