版本
内容
修订人
时间
0.1.0
草稿
黄鑫
2018/06/11
0.2.0
修改文档组织
黄鑫
2018/06/16
0. 前言
"代码是写给人看的"
例子🌰
//
// M2User.h
// M2API
//
// Created by Kim on 2018/06/11.
//
// 头文件引入
#import
#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. 布局与风格
良好布局的目的
准确表现代码的逻辑结构
始终如一地表现代码的逻辑结构
改善可读性
经得起修改
布局技术
分组 从另一个角度看,空白也是分组,也是确保相关到语句组成放在一起。
空行 是指示一个程序如何组织的手段。可以用空行将相关语句各自划分成段落,分开各个子程序,突出注释部分。
缩进 使用缩进形式显示程序的逻辑结构。
“当程序有两到四个空格的缩进时,受试者对程序的理解分数会比毫无缩进的程序高出20%到30%。”
— 《程序缩进和可理解性》
2. 代码组织
Objective-C的类通常分成头文件和实现文件。
头文件
头文件通常包含:
文件说明与版权
头文件引入
宏定义
常量定义
类型前置声明
块类型定义
枚举定义
函数定义
协议定义
类定义 - 类定义通常包含
类方法。
属性。
公开方法。
分类定义
分类方法。
🚧 注意:内容排列顺序与上面一致。
如下面的头文件模板所示。按照下面的顺序定义。
// 文件说明与版权
//
// M2API2Client.h
// M2API
//
// Created by Kim on 2017/11/11.
// Copyright (c) 2017 Kim Studio. All rights reserved.
//
// 头文件引入 (见下文说明)
#import
#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
- (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
#import
#import
#import
#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
#import "M2APIUser.h"
// 协议定义
@protocol M2APIUserEndPoint
/**
登陆
@param user 用户名
@param password 密码
@return 信号 M2APIUser
*/
- (RACSignal *)loginWithUser:(NSString *)user password:(NSString *)password;
/**
获取用户信息
@return 信号 用户信息
*/
- (RACSignal *)userInfo;
@end
/// 类定义
/**
用户模块客户端
*/
@interface M2APIUserClient : M2HTTPClient
@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;
}
属性
公开属性
属性特性排列顺序如下:
是否原子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;
}
方法定义
响应函数
规则:
控件响应函数:前缀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;
条件语句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
参考: