版本 | 内容 | 修订人 | 时间 |
---|---|---|---|
0.1.0 | 草稿 | 黄鑫 | 2018/06/11 |
0.2.0 | 修改文档组织 | 黄鑫 | 2018/06/16 |
0. 前言
"代码是写给人看的"
例子??
//
// M2User.h
// M2API
//
// Created by Kim on 2018/06/11.
//
// 头文件引入
#import <Foundation.h>
#import "M2Defines.h"
// 常量定义
FOUNDATION_EXPORT NSString * const M2UserErrorDomain;
/**
性别枚举
*/
typedef NS_ENUM(NSUInteger, M2Gender) {
M2GenderUnknow = 0, //!< 未知
M2GenderMale, //!< 男性
M2GenderFemale //!< 女性
};
/** 用户 */
@interface M2User : NSObject
@property (nonatomic, readonly, copy) NSString *name; //!< 名字
@property (nonatomic, readonly, assign) NSUInteger age; //!< 年龄
@property (nonatomic, readonly, assign) M2Gender gender; //!< 性别
/**
初始化
@param name 用户名
@param age 年龄
@param gender 性别
*/
+ (instancetype)userWithName:(NSString * __Nonnull)name
age:(NSUInteger)age
gender:(M2Gender)gender;
- (instancetype)initWithName:(NSString * __Nonnull)name
age:(NSUInteger)age
gender:(M2Gender)gender;
@end
// 实现
@implementation M2User
+ (instancetype)userWithName:(NSString * __Nonnull)name
age:(NSUInteger)age
gender:(M2Gender)gender {
return [[self alloc] initWithName:name age:age gender:gender];
}
- (instancetype)initWithName:(NSString * __Nonnull)name
age:(NSUInteger)age
gender:(M2Gender)gender {
if (self = [super init]) {
_name = name;
_age = age;
_gender = gender;
}
return self;
}
@end
1. 布局与风格
良好布局的目的
- 准确表现代码的逻辑结构
- 始终如一地表现代码的逻辑结构
- 改善可读性
- 经得起修改
布局技术
- 分组 从另一个角度看,空白也是分组,也是确保相关到语句组成放在一起。
- 空行 是指示一个程序如何组织的手段??梢杂每招薪喙赜锞涓髯曰殖?strong>段落,分开各个子程序,突出注释部分。
- 缩进 使用缩进形式显示程序的逻辑结构。
“当程序有两到四个空格的缩进时,受试者对程序的理解分数会比毫无缩进的程序高出20%到30%。”
— 《程序缩进和可理解性》
2. 代码组织
Objective-C的类通常分成头文件和实现文件。
头文件
头文件通常包含:
- 文件说明与版权
- 头文件引入
- 宏定义
- 常量定义
- 类型前置声明
- 块类型定义
- 枚举定义
- 函数定义
- 协议定义
- 类定义 - 类定义通常包含
- 类方法。
- 属性。
- 公开方法。
- 分类定义
- 分类方法。
?? 注意:内容排列顺序与上面一致。
如下面的头文件模板所示。按照下面的顺序定义。
// 文件说明与版权
//
// M2API2Client.h
// M2API
//
// Created by Kim on 2017/11/11.
// Copyright (c) 2017 Kim Studio. All rights reserved.
//
// 头文件引入 (见下文说明)
#import <Foundation.h>
#import "M2APIClient.h"
// 宏定义 (见下文说明。必须是才使用宏)
#define M2_DEBUG 0
#define M2_TEST 1
// 常量定义
FOUNDATION_EXPORT NSString * const M2UserErrorDomain;
// 类型前置声明
@class User;
// 类型定义
typedef NSString * const M2APIHTTPMethod;
// Block类型定义
typedef void (^M2APISuccessBlock)(id response);
typedef void (^M2APIFailureBlock)(NSError *error);
// 枚举定义
typedef NS_ENUM(NSUInteger, M2DirectionType) {
M2DirectionTypeUnknown = 0,
M2DirectionTypeTop,
M2DirectionTypeLeft,
M2DirectionTypeButtom,
M2DirectionTypeRight
};
// 协议定义
@protocol M2LoginViewDelegate : NSObject
@end
// 类定义
@interface M2User : NSObject
@property (nonatomic, readonly, copy) NSString *name;
@end
// 分类定义
@interface M2User <M2Debug>
- (NSString *)debugInfo;
@end
?? 通常使用文件模板
版权
文件头中增加版权信息。
//
// M2API2Client.h
// M2API
//
// Created by Kim on 2017/11/11.
// Copyright (c) 2017 Kim Studio. All rights reserved.
//
头文件引入
头文件引入规则顺序:
- 系统库
- 第三方库
- 工程内类引入
注意:
- 系统库/第三方库与工程内类引入直接的分组空行。
- 工程内类如果有多个头文件引入,也可以增加空行按功能进行分组。
//
// M2API2Client.h
// M2API
//
// Created by Kim on 2017/11/11.
// Copyright (c) 2017 Kim Studio. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AFNetworking/AFNetworking.h>
#import <CocoaLumberjack/CocoaLumberjack.h>
#import <AFHTTPSessionManagerLogger.h>
#import "M2APIConfiguration.h"
#import "M2APIHTTPSessionManager.h"
实现文件
实现文件通常包含:
// 文件描述
// 头文件引入
// 常量定义
// 文件内私有类定义
// 类私有方法定义
// 类实现
类定义
一般类定义组成如下:
- 协议
- 类
- 成员变量
- 属性
- 类方法
- 构造函数
- 公开方法
- 控件响应函数
- 通知响应函数
- 委托方法
- 私有方法
空行与注释:
- 协议与类之间留2行空行。
-
@protocol/@interface
与第一个属性或方法后,空1行。 - 最后一个方法与
@end
之间空1行。 - 类最后空1行。
- 属性与第一个方法之间空1行。
- 属性如果有长注释,则空1行。
-
属性如果使用短注释,则在属性后使用
//!<
进行注释。 - 如果方法定义有注释,则空1行。
- 如果方法定义没有注释,则可以不留空行进行分组。
- 方法分组之间,空1行。
- 函数定义的注意点,见后面函数一节
////// 头文件说明
//
// M2APIUserClient.h
// M2UserAPI
// Created by Kim on 2018/06/10
// Copyright (c) 2017 Kim Studio. All rights reserved.
////// 头文件引入
#import <M2HTTPClient.h>
#import "M2APIUser.h"
////// 协议定义
@protocol M2APIUserEndPoint <NSObject>
/**
登陆
@param user 用户名
@param password 密码
@return 信号 M2APIUser
*/
- (RACSignal *)loginWithUser:(NSString *)user password:(NSString *)password;
/**
获取用户信息
@return 信号 用户信息
*/
- (RACSignal *)userInfo;
@end
/////// 类定义
/**
用户??榭突Ф? */
@interface M2APIUserClient : M2HTTPClient <M2APIUserEndPoint>
@property (nonatomic, strong) M2APIConfiguration *configuration; //!< 配置消息
@property (nonatomic, strong) M2APISigner *signer; //!< 签名类
+ (instancetype)sharedClient;
+ (instancetype)clientWithConfiguration:(M2APIConfiguration *)configuration;
- (instancetype)initWithConfiguration:(M2APIConfiguration *)configuration;
@end
类实现
类实现内部组织 使用#pargma mark -
来分割功能组。一个典型的ViewController
的实现功能分组有:
-
类方法。
- 单件函数。
- 其他类方法。
-
Lifecycle。 对象生命周期函数
- 对象生命周期函数。
init
,dealloc
,descrition
。
- 对象生命周期函数。
自定义属性。
UI对象的事件响应函数。
公开方法。
私有方法和辅助函数。
通知处理函数。
委托方法。
#pargma mark - Class methods
+ (instancetype)sharedInstance {}
+ (CGFloat)viewHeightForObject:(id)object {}
#pargma mark - Lifecycle
- (instancetype)init {}
- (void)dealloc {}
- (void)viewDidLoad {}
- (void)viewWillAppear:(BOOL)animated {}
- (void)didReceiveMemoryWarning {}
#pargma mark - Custom Accessors
- (void)setCustomProperty:(id)value {}
- (id)customProperty {}
#pargma mark - IBActions
- (IBAction)onSubmitDataAction:(id)sender {}
#pargma mark - Public
- (void)publicMethod {}
#pargma mark - Private helpers or utils
- (void)m2_privateMethod {}
#pargma mark - Notification Handlers
- (void)onEnterBackgroundHandler:(NSNotification *)notification {}
#pargma mark - Delegate methods
// 多个delegate 进行分组
?? 使用文件模板
3. 命名
Apple命名规则尽可能坚持,特别是与这些相关的memory management rules(NARC)。
长的,描述性的方法和变量命名是好的。
命名涉及到比较多:
- 库名
- 文件名
- 类名
- 函数名
- 变量名
库名
设计一个库通常使用前缀+名称的方式。eg.
- UIKit
- AVFoundation
- SDWebImage
- AFNetworking
文件名
文件名命名规则与类命名规则一致:
- 命名空间。本项目/或项目模块缩略前缀。eg.
M2API
,M2BL
,M2PL
。 - 功能名词。User,Device,File,VideoPlayer。
- 功能分类名字。例如,
-
Client
代表DAL的网络访问客户端。 -
Data
代表DAL的DTO (Data Transfer Object)。 - 使用名词作为领域模型名称。
-
Service
代表BL的业务逻辑类。 -
Item
代表PL中View的VO(View Object)。 -
View
代表PL的视图类。 -
Controller
代表PL的中MVC
模式的C
控制器 -
ViewModel
/Store
代表PL的MVVM
的VM
或者MVCS的S
。
-
eg.
UIViewController.h
M2PLLoginView.h
命名空间
由于Objective-C 没有命名空间,所以通常使用项目名的头字母用于:
宏
常量
枚举
C函数名
全局变量名
类名
块类型名
前缀应由不少于3个字母组成(苹果保留所有2个字母的前缀)。可以是APP名、公司名缩写等。
例子??
// 宏
#debug M2_DEBUG
// 常量
FOUNDATION_EXPORT NSString * const M2UserErrorDomain;
// 别名
typedef NSString * const M2HTTPMethod;
// 块类型名
typedef void (^M2APISuccessBlock)(id response);
typedef void (^M2APIFailureBlock)(NSError *error);
// 类名
@class M2User;
宏
尽量少使用宏来定义常量。宏通常用于编译条件。
- 增加命名空间。
- 单词使用大写。
- 使用下划线连接单词。
使用
#define M2_DEBUG
不使用
#define Production
#define M2_Production
常量
常量通常使用与字符串类型常量与值类型常量。注意点:
- 命名空间。命名空间前缀全部大写
- 使用驼峰命名
// 头文件 .h
FOUNDATION_EXPORT NSString * const M2UserErrorDomain;
FOUNDATION const CGFloat M2UserMaxAge;
// 实现文件 .m
static NSString * const M2UserError = @"net.kim.M2UserErrorDomain"; // 跨文件使用
static const CGFloat M2UserMaxAge = 200; // 跨文件使用
static const CGFloat M2UserMinAge = 0; // 文件内部使用
块类型
块类型定义。注意点:
- 命名。命名空间 + 功能名词 +
Block
。- 命名空间。见《命名空间一节》
- 功能名词。
- 后缀
Block
。以区别其他类型。
- 注意返回类型后需要增加一个空格。
例
typedef void (^M2APISuccessBlock)(id response);
typedef void (^M2APIFailureBlock)(NSError *error);
4. 类
- 类方法
- 单例模式
- 属性
- 实例方法
类方法
单例模式
单例对象应该使用线程安全模式来创建共享实例。
+ (instancetype)sharedInstance {
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
这会防止possible and sometimes prolific crashes.
属性
公开属性
属性特性排列顺序如下:
- 是否原子
atomic/nonatomic
。虽然默认为atomic
, 原子访问时还是需要显式说明。 - 读写
readonly/readwrite
。默认为readwrite
。只读是需要显式说明。 - 访问器
getter/setter
。如果是布尔类型getter
,需要加is
前缀。 - 存储特性
weak/strong/copy/assign
。放在最后。
??Tip 不可变性
Immutable
为了避免数据遭到不必要的修改:
- 不应该被外部直接修改的属性,应该声明
(readonly)
(可以在Extension
中重新声明为(readwrite)
,使它对外只读,对内可读写)。- 不要把
NSMutable(Array/Dictionary/Set...)
暴露出来,应该只留一个setter
给外部使用,以免它们被其他类修改时,类自身难以察觉。- 如果数据不是特别多,
copy
的代价不是特别大,留给其他类的getter
应尽量用copy
方法,返回一个不可变的对象。
例子
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) M2Gender gender;
@property (nonatomic, readonly, getter=isLogin, assign) BOOL login;
??Tip 控件属性命名
- 控件功能名词 + 后缀不带命名空间的控件类型名
例子
@interface M2PLoginView : UIView
@property (nonatomic, weak) IBOutlet UILabel *userLabel; //!< 用户名标签
@property (nonatomic, weak) IBOutlet UITextField *userTextField; //!< 用户名输入
@end
私有属性
如果私有属性在??槟诓靠梢苑梦?,则使用私有头文件。
eg. M2User_Private.h 在有需要使用到的实现文件引入即可。
?? 注意:私有头文件,在??橥獠坎豢煞梦?。生成库时需要注意忽略改头文件。
例子
/// 实现文件
@interface M2User ()
@property (nonatomic, copy)NSString *name;
@property (nonatomic, assign)NSUInteger age;
@property (nonatomic, assign)M2Gender gender;
@end
@implementation M2User
// ...
@end
自定义属性
下面是一个懒加载的自定义属性:
- (NSMultableDictionary *)extraInfo {
if (!_extraInfo) {
_extraInfo = [NSMutableDictionary dictionary];
}
return _extraInfo
}
自定义属性设置:
- (void)setExtraInfo:(NSDictionary *)extraInfo {
_extraInfo = extraInfo;
// Do something else.
// 其他副作用。
}
?? 注意:在自定义属性内增加副作用需要特别注意。需要在属性增加注释说明。
成员变量
私有属性,一般定义在类的实现文件。
/// 实现文件
@interface M2User () {
BOOL _status;
}
@end
@implementation M2User
// ...
@end
标识位
@interface Fool () {
struct {
BOOL step1Done;
BOOL step2Done;
} _flags;
}
// 使用
_flag.step1Done = YES;
_flag.step1Done = NO;
类初始化方法
@interface Airplan
+ (instancetype)airplanWithType:(AirplanType)type;
@end
@implementation Airplan
关于更多instancetype信息,请查看NSHipster.com
Init方法
Init方法应该遵循Apple生成代码模板的命名规则。返回类型应该使用instancetype
而不是id
- (instancetype)init {
if (self = [super init]) {
// ...
}
return self;
}
// 或者
- (instancetype)initWithName:(NSString *)name {
self = [super init];
if (self) {
// ...
}
return self;
}
查看关于instancetype的文章Class Constructor Methods
方法定义
响应函数
规则:
-
控件响应函数:前缀
on
+ 功能动作 + 后缀Action
。 -
通知响应函数:前缀
on
+ 通知 + 后缀Handler
。
使用
// 控件响应函数
- (IBAction)onLoginAction:(id)sender {
// ...
}
// 通知响应函数
- (void)onEnterBackgroundHandler:(NSNotification *)notification {
// ...
}
不使用
- (IBAction)login:(id)sender {
// ...
}
- (void)enterBackground:(NSNotification *)noti {
// ...
}
分类
@interface M2APIUserClient (User)
// 登陆
- (RACSignal *)loginWithUser:(NSString *)user password:(NSString *)password;
// 登出
- (RACSignal *)logout;
@end
5. 子程序
变量[TODO]
布尔值
Objective-C使用YES
和NO
。因为true
和false
应该只在CoreFoundation,C或C++代码使用。既然nil
解析成NO
,所以没有必要在条件语句比较。不要拿某样东西直接与YES
比较,因为YES
被定义为1和一个BOOL
能被设置为8位。
这是为了在不同文件保持一致性和在视觉上更加简洁而考虑。
使用
if (someObject) {}
if (![anotherObject boolValue]) {}
不使用
if (someObject == nil) {}
if ([anotherObject boolValue] == NO) {}
if (isAwesome == YES) {} // Never do this.
if (isAwesome == true) {} // Never do this.
如果BOOL
属性的名字是一个形容词,属性就能忽略"is"前缀,但要指定get访问器的惯用名称。例如:
@property (assign, getter=isEditable) BOOL editable;
文字和例子从这里引用Cocoa Naming Guidelines
条件语句if/else
条件语句主体为了防止出错应该使用大括号包围,即使条件语句主体能够不用大括号编写(如,只用一行代码)。这些错误包括添加第二行代码和期望它成为if语句;还有,even more dangerous defect可能发生在if语句里面一行代码被注释了,然后下一行代码不知不觉地成为if语句的一部分。除此之外,这种风格与其他条件语句的风格保持一致,所以更加容易阅读。
使用
// Good!
if (!error) {
return success;
}
不使用
// Bad
// 没有花括号,容易多些空行,造成逻辑提前返回。
if (!error)
return success;
if (!error) return success;
if (error != nil)
return success
多条件情而且单行过长的情况下,使用换行。条件符放行最后。
使用
// Good!
if (direction == M2Left ||
direction == M2Right) {
// ...
}
不使用
// Bad!
if (direction == M2Left
|| direction == M2Right) {
// ...
}
使用
if (user.isHappy) {
// Do something
} else {
// Do something else
}
不使用
if (user.isHappy)
{
// ...
}
else {
// ...
}
Switch-Case
- (void)handleMessage:(M2Message *)message {
// 注意花括号与break。
M2MessageType type = message.type;
switch(type) {
case M2MessageChat: {
// ...
} break;
case M2MessageNotify: {
// ...
} break;
case M2MessageSystem: {
// ...
} break;
default:
break;
}
}
三元操作符 ?:
当需要提高代码的清晰性和简洁性时,三元操作符?:
才会使用。单个条件求值常常需要它。多个条件求值时,如果使用if
语句或重构成实例变量时,代码会更加易读。一般来说,最好使用三元操作符是在根据条件来赋值的情况下。
Non-boolean的变量与某东西比较,加上括号()会提高可读性。如果被比较的变量是boolean类型,那么就不需要括号。
使用
NSInteger value = 5;
result = (value != 0) ? x : y;
Bool isHorizontal = YES;
result = isHorizontal ? x : y;
NSString *name = nil;
result = name ?: @"";
不使用
BOOL result = value!=0 ?x:y;
块block
使用
typedef void (^M2SuccessBlock)(id response);
M2SuccessBlock successBlock = ^(id response) {
// Do something.
};
不使用
// 可读性不够强,另外可能导致过长行。
void (^successBlock)(id response) = ^(id response) {
// Do something.
};
使用
// 风格1
[userClient loginWithUser:user password:password success:^(id user) {
// do something.
} failure:^(NSError *error) {
// ...
}];
// *******************************************
// 如果success/failure块过长,则可以前缀定义块。
// 风格2
M2APISuccessBlock successBlock = ^(id response) {
// do something.
};
M2APIFailureBlock failureBlok = ^(NSError *error) {
// ...
};
// 单行过长,则换行。
[userClient loginWithUser:user
password:password
success:successBlock
failure:failureBlock];
字面值
使用
// 数组
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
// 字典
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
// Number
NSNumber *shouldUseLiterals = @YES;
NSNumber *buildingStreetNumber = @10018;
不使用
// 数组
NSArray *names = [NSArray arrayWithObjects:@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul", nil];
// 字典
NSDictionary *productManagers = [NSDictionary dictionaryWithObjectsAndKeys: @"Kate", @"iPhone", @"Kamal", @"iPad", @"Bill", @"Mobile Web", nil];
// Number
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];
NSNumber *buildingStreetNumber = [NSNumber numberWithInteger:10018];
数组
// 数组
NSArray *names = @[@"Brian", @"Matt", @"Chris", @"Alex", @"Steve", @"Paul"];
names[]
字典
// 字典创建
NSDictionary *productManagers = @{@"iPhone": @"Kate", @"iPad": @"Kamal", @"Mobile Web": @"Bill"};
// 访问
NSString *product = productManagers[@"iPhone"];
//
NSMutableDictionary *user = [NSMutableDictionary dictionary];
// 设置
user[@"name"] = @"Bob";
user[@"title"] = @"IT Manager";
user[@"age"] = @25;
// 迭代
[user enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"key : %@, value : %@", key, obj);
}];
CGRect 函数
使用
// Good!
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);
不使用
// Bad
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 };
注释
当需要注释时,注释应该用来解释这段特殊代码为什么要这样做。任何被使用的注释都必须保持最新或被删除。
一般都避免使用块注释,因为代码尽可能做到自解释,只有当断断续续或几行代码时才需要注释。例外:这不应用在生成文档的注释
空格
空行
6. 资源
iOS应用包含多种资源文件
- storyboard/xib
- 图片
- 字符串
- 字体
- 多媒体
storyboard/xib
图片
字符串
字体
多媒体
7. 模块
8. Xcode工程
物理文件应该与Xcode工程文件保持同步来避免文件扩张。任何Xcode分组的创建应该在文件系统的文件体现。代码不仅是根据类型来分组,而且还可以根据功能来分组,这样代码更加清晰。
尽可能在target的Build Settings打开"Treat Warnings as Errors,和启用以下additional warnings。如果你需要忽略特殊的警告,使用 Clang's pragma feature。
9. 辅助工具
- Spacecommander
参考:
- 《苹果Cocoa代码规范》
- 《NYTimes Objective-C代码规范》
- 《Effective Objective-C》
- 《raywenderlich.com Objective-C编码规范》
- 《大规模C++编程》
- Robots & Pencils
- New York Times
- GitHub
- Adium
- Sam Soffes
- CocoaDevCentral
- Luke Redpath
- Marcus Zarra