iOS开发代码规范

释放双眼,带上耳机,听听看~!

原则

  1. 长的,描述性的方法和变量命名是好的。不要使用简写,除非是一些大家都知道的场景比如 VIP。不要使用 bgView,推荐使用 backgroundView
  2. 见名知意。含义清楚,做好不加注释代码自我表述能力强。(前提是代码足够规范)
  3. 不要过分追求技巧,降低代码可读性
  4. 删除没必要的代码。比如我们新建一个控制器,里面会有一些不会用到的代码,或者注释起来的代码,如果这些代码不需要,那就删除它,留着偷懒吗?下次需要自己手写
  5. 在方法内部不要重复计算某个值,适当的情况下可以将计算结果缓存起来
  6. 尽量减少单例的使用。
  7. 提供一个统一的数据管理入口,不管是 MVC、MVVM、MVP 模块内提供一个统一的数据管理入口会使得代码变得更容易管理和维护。
  8. 除了 .m 文件中方法,其他的地方”{“不需要另起一行。
- (void)getGooodsList
{
    // ...
}

- (void)doHomework
{
    if (self.hungry) {
        return;
    }
    if (self.thirsty) {
        return;
    }
    if (self.tired) {
        return;
    }
    papapa.then.over;
}

变量

  1. 一个变量最好只有一个作用,切勿为了节省代码行数,觉得一个变量可以做多个用途。(单一原则)
  2. 方法内部如果有局部变量,那么局部变量应该靠近在使用的地方,而不是全部在顶部声明全部的局部变量。
  3. 变量名必须使用驼峰格式
类,协议使用大驼峰:
HomePageViewController.h
<HeaderViewDelegate>
对象等局部变量使用小驼峰:
NSString *personName = @"";
NSUInteger totalCount = 0;

  1. 变量的名称必须同时包含功能与类型
UIButton *addBtn //添加按钮
UILabel *nameLbl //名字标签
NSString *addressStr//地址字符串

  1. 系统常用类作实例变量声明时加入后缀

UIViewController —- VC
UILabel —- Lbl
NSMutableArray —– Marray
NSMutableDictionary —- Mdict

常量

  1. 常量以相关类名作为前缀
static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
不推荐这样写:
static const NSTimeInterval fadeOutTime = 0.4;

  1. 建议使用类型常量,不建议使用#define预处理命令
    首先比较一下这两种声明常量的区别:
  • 预处理命令:简单的文本替换,不包括类型信息,并且可被任意修改
  • 类型常量:包括类型信息,并且可以设置其使用范围,而且不可被修改。
    使用预处理虽然能达到替换文本的目的,但是本身还是有局限性的:
  • 不具备类型信息。
  • 可以被任意修改。
  1. 对外公开某个常量:
    如果我们需要发送通知,那么就需要在不同的地方拿到通知的“频道”字符串(通知的名称),那么显然这个字符串是不能被轻易更改,而且可以在不同的地方获取。这个时候就需要定义一个外界可见的字符串常量。
//头文件
extern NSString *const ZOCCacheControllerDidClearCacheNotification;
//实现文件
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推荐这样写:
#define CompanyName @"Apple Inc." 
#define magicNumber 42 

运算符

  1. 1元运算符和变量之间不需要空格。例如:++n
  2. 2元运算符与变量之间需要空格隔开。例如: containerWidth = 0.3 * Screen_Width
  3. 当有多个运算符的时候需要使用括号来明确正确的顺序,可读性较好。例如: 2 << (1 + 2 * 3 – 4)

条件表达式

  1. 当有条件过多、过长的时候需要换行,为了代码看起来整齐些
//good
if (condition1() && 
    condition2() && 
    condition3() && 
    condition4()) {
  // Do something
}
//bad
if (condition1() && condition2() && condition3() && condition4()) { // Do something }

  1. 在一个代码块里面有个可能的情况时善于使用 return 来结束异常的情况。
- (void)doHomework
{
    if (self.hungry) {
        return;
    }
    if (self.thirsty) {
        return;
    }
    if (self.tired) {
        return;
    }
    papapa.then.over;
}

  1. 每个分支的实现都必须使用 {} 包含。
// bad
if (self.hungry) self.eat() 
// good
if (self.hungry) {
    self.eat()
}

  1. 条件判断的时候应该是变量在左,条件在右。 if ( currentCursor == 2 ) { //… }
  2. switch 语句后面的每个分支都需要用大括号括起来。
  3. switch 语句后面的 default 分支必须存在,除非是在对枚举进行 switch。

类名

  1. 大写驼峰式命名。每个单词首字母大写。比如「申请记录控制器」
    ApplyRecordsViewController
  2. 每个类型的命名以该类型结尾。
  • ViewController:使用 ViewController 结尾。例子:ApplyRecordsViewController
  • View:使用 View 结尾。例子:分界线:boundaryView
  • NSArray:使用 s 结尾。比如商品分类数据源。categories
  • UITableViewCell:使用 Cell 结尾。比如 MyProfileCell
  • Protocol:使用 Delegate 或者 Datasource 结尾。比如 XQScanViewDelegate
  • Tool:工具类
  • 代理类:Delegate
  • Service 类:Service

类的注释

有时候我们需要为我们创建的类设置一些注释。我们可以在类的下面添加。

枚举

枚举的命名和类的命名相近。

typedef NS_ENUM(NSInteger, UIControlContentVerticalAlignment) {
    UIControlContentVerticalAlignmentCenter  = 0,
    UIControlContentVerticalAlignmentTop     = 1,
    UIControlContentVerticalAlignmentBottom  = 2,
    UIControlContentVerticalAlignmentFill    = 3,
};

  1. 全部大写,单词与单词之间用 _ 连接。
  2. 以 K 开头。后面遵循大写驼峰命名。「不带参数」
#define HOME_PAGE_DID_SCROLL @"com.xq.home.page.tableview.did.scroll"
#define KHomePageDidScroll @"com.xq.home.page.tableview.did.scroll"

  1. 宏定义中如果包含表达式或变量,表达式和变量必须用小括号括起来。
#define MY_MIN(A, B)  ((A)>(B)?(B):(A))

属性

书写规则,基本上就是 @property 之后空一格,括号,里面的 线程修饰词、内存修饰词、读写修饰词,空一格 类 对象名称 根据不同的场景选择合适的修饰符。

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, assign, readonly) BOOL loading;   
@property (nonatomic, weak) id<#delegate#> delegate;
@property (nonatomic, copy) <#returnType#> (^<#Block#>)(<#parType#>);

  1. 属性的关键字推荐按照 原子性,读写,内存管理的顺序排列
@property (nonatomic, readwrite, copy) NSString *name;
@property (nonatomic, readonly, copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;

  1. 形容词性的BOOL属性的getter应该加上is前缀
@property (assign, getter=isEditable) BOOL editable;

  1. 使用getter方法做懒加载
    实例化一个对象是需要耗费资源的,如果这个对象里的某个属性的实例化要调用很多配置和计算,就需要懒加载它,在使用它的前一刻对它进行实例化:
- (NSDateFormatter *)dateFormatter 
{
    if (!_dateFormatter) {
           _dateFormatter = [[NSDateFormatter alloc] init];
           NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
           [_dateFormatter setLocale:enUSPOSIXLocale];
           [_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];
    } 
    return _dateFormatter;
}
但是也有对这种做法的争议:getter方法可能会产生某些副作用,例如如果它修改了全局变量,可能会产生难以排查的错误。

  1. 除了init和dealloc方法,建议都使用点语法访问属性
    使用点语法的好处:
    setter:1. setter会遵守内存管理语义(strong, copy, weak)。2. 通过在内部设置断点,有助于调试bug。3. 可以过滤一些外部传入的值。4. 捕捉KVO通知。
    getter:1. 允许子类化。2. 通过在内部设置断点,有助于调试bug。3. 实现懒加载(lazy initialization)。

注意 : 1. 懒加载的属性,必须通过点语法来读取数据。因为懒加载是通过重写getter方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。
2 在init和dealloc方法里面使用点语法的后果是:因为没有绕过setter和getter,在setter和getter里面可能会有很多其他的操作。而且如果它的子类重载了它的setter和getter方法,那么就可能导致该子类调用其他的方法。

  1. 不要滥用点语法,要区分好方法调用和属性访问
view.backgroundColor = [UIColor orangeColor]; 
[UIApplication sharedApplication].delegate;  

不推荐这样写:

[view setBackgroundColor:[UIColor orangeColor]]; 
UIApplication.sharedApplication.delegate; 

  1. 尽量使用不可变对象
    建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:
  • 在头文件中,设置对象属性为readonly。
  • 在实现文件中设置为readwrite。

这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。
如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。所以当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:

在公共API中:

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSSet *friends //向外公开的不可变集合

- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName;
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;

@end
在这里,我们将friends属性设置为不可变的set。然后,提供了来增加和删除这个set里的元素的公共接口。
在实现文件里:

@interface EOCPerson ()

@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;

@end

@implementation EOCPerson {
     NSMutableSet *_internalFriends;  //实现文件里的可变集合
}

- (NSSet*)friends 
{
     return [_internalFriends copy]; //get方法返回的永远是可变set的不可变型
}

- (void)addFriend:(EOCPerson*)person 
{
    [_internalFriends addObject:person]; //在外部增加集合元素的操作
    //do something when add element
}

- (void)removeFriend:(EOCPerson*)person 
{
    [_internalFriends removeObject:person]; //在外部移除元素的操作
    //do something when remove element
}

- (id)initWithFirstName:(NSString*)firstName andLastName:(NSString*)lastName 
{

     if ((self = [super init])) {
        _firstName = firstName;
        _lastName = lastName;
        _internalFriends = [NSMutableSet new];
    }
 return self;
}


我们可以看到,在实现文件里,保存一个可变set来记录外部的增删操作。
这里最重要的代码是:

- (NSSet*)friends 
{
   return [_internalFriends copy];
}
这个是friends属性的获取方法:它将当前保存的可变set复制了一不可变的set并返回。因此,外部读取到的set都将是不可变的版本。

单例

单例适合全局管理状态或者事件的场景。一旦创建,对象的指针保存在静态区,单例对象在堆内存中分配的内存空间只有程序销毁的时候才会释放。基于这种特点,那么我们类似 UIApplication 对象,需要全局访问唯一一个对象的情况才适合单例,或者访问频次较高的情况。我们的功能模块的生命周期肯定小于 App 的生命周期,如果多个单例对象的话,势必 App 的开销会很大,糟糕的情况系统会杀死 App。如果觉得非要用单例比较好,那么注意需要在合适的场合 tearDown 掉。
单例的使用场景概括如下:

  • 控制资源的使用,通过线程同步来控制资源的并发访问。
  • 控制实例的产生,以达到节约资源的目的。
  • 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //because has rewrited allocWithZone  use NULL avoid endless loop lol.
        _sharedInstance = [[super allocWithZone:NULL] init];
    });
    
    return _sharedInstance;
}

+ (id)allocWithZone:(struct _NSZone *)zone
{
    return [TestNSObject sharedInstance];
}

+ (instancetype)alloc
{
    return [TestNSObject sharedInstance];
}

- (id)copy
{
    return self;
}

- (id)mutableCopy
{
    return self;
}

- (id)copyWithZone:(struct _NSZone *)zone
{
    return self;
}

私有变量

推荐以 _ 开头,写在 .m 文件中。例如 NSString * _somePrivateVariable

代理方法

  1. 类的实例必须作为方法的参数之一。
  2. 对于一些连续的状态的,可以加一些 will(将要)、did(已经)
  3. 以类的名称开头
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;

方法

  1. 方法与方法之间间隔一行
  2. 大量的方法尽量要以组的形式放在一起,比如生命周期函数、公有方法、私有方法、setter && getter、代理方法..
  3. 方法最后面的括号需要另起一行。遵循 Apple 的规范
  4. 对于其他场景的括号,括号不需要单独换行。比如 if 后面的括号。
  5. 如果方法参数过多过长,建议多行书写。用冒号进行对齐。
  6. 一个方法内的代码最好保持在50行以内,一般经验来看如果一个方法里面的代码行数过多,代码的阅读体验就很差(别问为什么,做过重构代码行数很长的人都有类似的心情)
  7. 一个函数只做一个事情,做到单一原则。所有的类、方法设计好后就可以类似搭积木一样实现一个系统。
  8. 对于有返回值的函数,且函数内有分支情况。确保每个分支都有返回值。
  9. 函数如果有多个参数,外部传入的参数需要检验参数的非空、数据类型的合法性,参数错误做一些措施:立即返回、断言。
  10. 多个函数如果有逻辑重复的代码,建议将重复的部分抽取出来,成为独立的函数进行调用
- (instancetype)init
{
    self = [super init];
    if (self) {
        <#statements#>
    }
    return self;
}

- (void)doHomework:(NSString *)name
            period:(NSInteger)second
            score:(NSInteger)score;

  1. 方法如果有多个参数的情况下需要注意是否需要介词和连词。很多时候在不知道如何抉择测时候思考下苹果的一些 API 的方法命名。
//good
- (instancetype)initWithAge:(NSInteger)age name:(NSString *)name;

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;


//bad
- (instancetype)initWithAge:(NSInteger)age andName:(NSString *)name;

- (void)tableView:(UITableView *)tableView :(NSIndexPath *)indexPath;

  1. .m 文件中的私有方法需要在顶部进行声明
  2. 方法组之间也有个顺序问题。
  • 在文件最顶部实现属性的声明、私有方法的声明(很多人省去这一步,问题不大,但是蛮多第三方的库都写了,看起来还是会很方便,建议书写)。
  • 在生命周期的方法里面,比如 viewDidLoad 里面只做界面的添加,而不是做界面的初始化,所有的 view 初始化建议放在 getter 里面去做。往往 view 的初始化的代码长度会比较长、且一般会有多个 view 所以 getter 和 setter 一般建议放在最下面,这样子顶部就可以很清楚的看到代码的主要逻辑。
  • 所有button、gestureRecognizer 的响应事件都放在这个区域里面,不要到处乱放。
    文件基本上就是
#import "ViewController.h"
/*ViewController*/

/*View&&Util*/

/*model*/

/*NetWork InterFace*/

/*Vender*/

@interface ViewController ()

@end

@implementation ViewController

#pragma mark - life cycle
- (void)viewWillAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.title = @"标准模版";
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

#pragma mark - public Method

#pragma mark - private method

#pragma mark - event response



#pragma mark - UITableViewDelegate

#pragma mark - UITableViewDataSource
//...(多个代理方法依次往下写)

#pragma mark - getters and setters

@end

14 .私有方法应该在实现文件中申明。

@interface ViewController ()
- (void)basicConfiguration;
@end

@implementation ViewController
- (void)basicConfiguration
{
   //Do some basic configuration
}
@end

图片资源

  1. 单个文件的命名
    文件资源的命名也需要一定的规范,形式为:功能模块名类别功能_状态@nx.png
    Setting_Button_search_selected@2x.png、Setting_Button_search_selected@3x.png
    Setting_Button_search_unselected@2x.png、Setting_Button_search_unselected@3x.png
    2、 资源的文件夹命名 最好也参考 App 按照功能模块建立对应的实体文件夹目录,最后到对应的目录下添加相应的资源文件。

注释

优秀的代码大部分是可以自描述的,我们完全可以用程代码本身来表达它到底在干什么,而不需要注释的辅助。
但并不是说一定不能写注释,有以下三种情况比较适合写注释:

  1. 公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)。
  2. 涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。
  3. 容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。
    除了上述这三种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。

最后,对于注释的内容,相对于“做了什么”,更应该说明“为什么这么做”。

  1. 对于类的注释写在当前类文件的顶部
  2. 对于属性的注释需要写在属性后面的地方。 //*<userId/
  3. 对于 .h 文件中方法的注释,一律按快捷键 command+option+/。三个快捷键解决。按需在旁边对方法进行说明解释、返回值、参数的说明和解释
  4. 对于 .m 文件中的方法的注释,在方法的旁边添加 //。
  5. 注释符和注释内容需要间隔一个空格。 例如: // fetch goods list

版本规范

采用 A.B.C 三位数字命名,比如:1.0.2,当有更新的情况下按照下面的依据

  • 属于重大内容的更新 1.0.2 -> 2.0.0
  • 属于小部分内容的更新 1.0.2 -> 1.1.1
  • 属于补丁更新 1.0.2 -> 1.0.3

改进

我们知道了平时在使用 Xcode 开发的过程中使用的系统提供的代码块所在的地址和新建控制器、模型、view等的文件模版的存放文件夹地址后,我们就可以设想下我们是否可以定制自己团队风格的控制器模版、是否可以打造和维护自己团队的高频使用的代码块?
答案是可以的。
Xcode 代码块的存放地址:~/Library/Developer/Xcode/UserData/CodeSnippets
Xcode 文件模版的存放地址:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/File Templates/

代码块的改造

我们可以将属性、控制器生命周期方法、单例构造一个对象的方法、代理方法、block、GCD、UITableView 懒加载、UITableViewCell 注册、UITableView 代理方法的实现、UICollectionVIew 懒加载、UICollectionVIewCell 注册、UICollectionView 的代理方法实现等等组织为 codesnippets

思考

  • 封装好 codesnippets 之后团队除了你 编写这个项目的人如何使用?如何知道是否有这个代码块?
    方案:先在团队内召开代码规范会议,大家都统一知道这个事情在。之后大家共同维护 codesnippets。用法见下
    属性:通过 Property_类型 开头,回车键自动补全。比如 Strong 类型,编写代码通过 Property_Strong 回车键自动补全成如下格式
@property (nonatomic, strong) <#Class#> *<#object#>;

方法:以 Method_关键词 回车键确认,自动补全。比如 Method_UIScrollViewDelegate 回车键自动补全成 如下格式

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    
}

各种常见的 Mark:以 Mark_关键词 回车确认,自动补全。比如 Method_MethodsGroup 回车键自动补全成 如下格式

#pragma mark - life cycle
#pragma mark - public Method
#pragma mark - private method
#pragma mark - event response
#pragma mark - UITableViewDelegate
#pragma mark - UITableViewDataSource
#pragma mark - getters and setters

  • 封装好 codesnippets 之后团队内如何统一?想到一个方案,可以将团队内的 codesnippets 共享到 git,团队内的其他成员再从云端拉取同步。这样的话团队内的每个成员都可以使用最新的 codesnippets 来编码。
    编写 shell 脚本。几个关键步骤:
  1. 给系统文件夹授权
  2. 在脚本所在文件夹新建存放代码块的文件夹
  3. 将系统文件夹下面的代码块复制到步骤2创建的文件夹下面
  4. 将当前的所有文件提交到 Git 仓库

文件模版的改造

我们观察系统文件模版的特点,和在 Xcode 新建文件模版对应。

image.png

所以我们新建 Custom 文件夹,将系统 Source 文件夹下面的 Cocoa Touch Class.xctemplate 复制到 Custom 文件夹下。重命名为我们需要的名字,我这里以“Power”为例

image.png

进入 PowerViewController.xctemplate/PowerViewControllerObjective-C
修改 FILEBASENAME.h 和 FILEBASENAME.m 文件内容

image.png

在替换 .h 文件内容的时候后面改为 UIViewController,不然其他开发者新建文件模版的时候出现的不是 UIViewController 而是我们的 PowerViewController

image.png

修改 TemplateInfo.plist

image.png

  • 如何使用
    商量好一个标识(“Power”)。比如我新建了单例、控制器、Model、UIView4个模版,都以为 Power 开头。
  • 如何共享
    以 shell 脚本为工具。使用脚本将 git 云端的代码模版同步到本地 Xcode 文件夹对应的位置就可以使用了。关键步骤:
  1. git clone 代码到脚本所在文件夹
  2. 进入存放 codesnippets 的文件夹将内容复制到系统存放 codesnippets 的地方
  3. 进入存放 file template 的文件夹将内容复制到系统存放 file template 的地方

./syncSnippets.sh // 同步git云端代码块和文件模版到本地
./uploadMySnippets.sh //将本地的代码块和文件模版同步到云端

补充

if语句

1。 必须列出所有分支(穷举所有的情况),而且每个分支都必须给出明确的结果。

var hintStr;
if (count < 3) {
  hintStr = "Good";
} else {
  hintStr = "";
}
不推荐这样写:
var hintStr;
if (count < 3) {
 hintStr = "Good";
}

  1. 不要使用过多的分支,要善于使用return来提前返回错误的情况
- (void)someMethod { 
  if (!goodCondition) {
    return;
  }
  //Do something
}
不推荐这样写:
- (void)someMethod { 
  if (goodCondition) {
    //Do something
  }
}

-(id)initWithDictionary:(NSDictionary*)dict error:(NSError)err{
   //方法1. 参数为nil
   if (!dict) {
     if (err) *err = [JSONModelError errorInputIsNil];
     return nil;
    }

    //方法2. 参数不是nil,但也不是字典
    if (![dict isKindOfClass:[NSDictionary class]]) {
        if (err) *err = [JSONModelError errorInvalidDataWithMessage:@"Attempt to initialize JSONModel object using initWithDictionary:error: but the dictionary parameter was not an 'NSDictionary'."];
        return nil;
    }

    //方法3. 初始化
    self = [self init];
    if (!self) {
        //初始化失败
        if (err) *err = [JSONModelError errorModelIsInvalid];
        return nil;
    }

    //方法4. 检查用户定义的模型里的属性集合是否大于传入的字典里的key集合(如果大于,则返回NO)
    if (![self __doesDictionary:dict matchModelWithKeyMapper:self.__keyMapper error:err]) {
        return nil;
    }

    //方法5. 核心方法:字典的key与模型的属性的映射
    if (![self __importDictionary:dict withKeyMapper:self.__keyMapper validation:YES error:err]) {
        return nil;
    }

    //方法6. 可以重写[self validate:err]方法并返回NO,让用户自定义错误并阻拦model的返回
    if (![self validate:err]) {
        return nil;
    }

    //方法7. 终于通过了!成功返回model
    return self;
}

可以看到,在这里,首先判断出各种错误的情况然后提前返回,把最正确的情况放到最后返回。

  1. 条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值
let nameContainsSwift = sessionName.hasPrefix("Swift")
let isCurrentYear = sessionDateCompontents.year == 2014
let isSwiftSession = nameContainsSwift && isCurrentYear
if (isSwiftSession) { 
   // Do something
}
不推荐这样写:
if ( sessionName.hasPrefix("Swift") && (sessionDateCompontents.year == 2014) ) { 
    // Do something
}

  1. 条件语句的判断应该是变量在左,常量在右
if ( count == 6) {
}
if ( object == nil) {
}
不推荐这样写:
if ( 6 == count) {
}
if ( nil == object ) {
}

for语句

  1. 不可在for循环内修改循环变量,防止for循环失去控制。
for (int index = 0; index < 10; index++){
   ...
   logicToChange(index)
}

  1. 避免使用continue和break。
    ontinue和break所描述的是“什么时候不做什么”,所以为了读懂二者所在的代码,我们需要在头脑里将他们取反。

其实最好不要让这两个东西出现,因为我们的代码只要体现出“什么时候做什么”就好了,而且通过适当的方法,是可以将这两个东西消灭掉的:
2.1 如果出现了continue,只需要把continue的条件取反即可

var filteredProducts = Array<String>()
for level in products {
    if level.hasPrefix("bad") {
        continue
    }
    filteredProducts.append(level)
}

我们可以看到,通过判断字符串里是否含有“bad”这个prefix来过滤掉一些值。其实我们是可以通过取反,来避免使用continue的:

for level in products {
    if !level.hasPrefix("bad") {
      filteredProducts.append(level)
    }
}

Switch语句

  1. 每个分支都必须用大括号括起来
switch (integer) {  
  case 1:  {
    // ...  
   }
    break;  
  case 2: {  
    // ...  
    break;  
  }  
  case 3: {
    // ...  
    break; 
  }
  default:{
    // ...  
    break; 
  }
}

  1. 使用枚举类型时,不能有default分支, 除了使用枚举类型以外,都必须有default分支
RWTLeftMenuTopItemType menuType = RWTLeftMenuTopItemMain;  
switch (menuType) {  
  case RWTLeftMenuTopItemMain: {
    // ...  
    break; 
   }
  case RWTLeftMenuTopItemShows: {
    // ...  
    break; 
  }
  case RWTLeftMenuTopItemSchedule: {
    // ...  
    break; 
  }
}

在Switch语句使用枚举类型的时候,如果使用了default分支,在将来就无法通过编译器来检查新增的枚举类型了。

函数

1. 一个函数的长度必须限制在50行以内

通常来说,在阅读一个函数的时候,如果视需要跨过很长的垂直距离会非常影响代码的阅读体验。如果需要来回滚动眼球或代码才能看全一个方法,就会很影响思维的连贯性,对阅读代码的速度造成比较大的影响。最好的情况是在不滚动眼球或代码的情况下一眼就能将该方法的全部代码映入眼帘。

2. 一个函数只做一件事(单一原则)

每个函数的职责都应该划分的很明确(就像类一样)。

dataConfiguration()
viewConfiguration()
不推荐这样写:
void dataConfiguration()
{   
   ...
   viewConfiguration()
}

3. 对于有返回值的函数(方法),每一个分支都必须有返回值

int function()
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }else{
       return defaultCount
    } 
}
不推荐这样写:
int function()
{
    if(condition1){
        return count1
    }else if(condition2){
        return count2
    }
}

4. 对输入参数的正确性和有效性进行检查,参数错误立即返回

void function(param1,param2)
{
      if(param1 is unavailable){
           return;
      }
    
      if(param2 is unavailable){
           return;
      }

     //Do some right thing
}

5. 如果在不同的函数内部有相同的功能,应该把相同的功能抽取出来单独作为另一个函数

6. 将函数内部比较复杂的逻辑提取出来作为单独的函数

一个函数内的不清晰(逻辑判断比较多,行数较多)的那片代码,往往可以被提取出去,构成一个新的函数,然后在原来的地方调用它这样你就可以使用有意义的函数名来代替注释,增加程序的可读性。

openEmailSite();
login();

writeTitle(title);
writeContent(content);
writeReceiver(receiver);
addAttachment(attachment);

send();
中间的部分稍微长一些,我们可以将它们提取出来:
void writeEmail(title, content,receiver,attachment)
{
  writeTitle(title);
  writeContent(content);
  writeReceiver(receiver);
  addAttachment(attachment); 
}
然后再看一下原来的代码:
openEmailSite();
login();
writeEmail(title, content,receiver,attachment)
send();

8. 避免使用全局变量,类成员(class member)来传递信息,尽量使用局部变量和参数。

在一个类里面,经常会有传递某些变量的情况。而如果需要传递的变量是某个全局变量或者属性的时候,有些朋友不喜欢将它们作为参数,而是在方法内部就直接访问了:

 class A {
   var x;

   func updateX() 
   {
      ...
      x = ...;
   }

   func printX() 
   {
     updateX();
     print(x);
   }
 }

我们可以看到,在printX方法里面,updateX和print方法之间并没有值的传递,乍一看我们可能不知道x从哪里来的,导致程序的可读性降低了。
而如果你使用局部变量而不是类成员来传递信息,那么这两个函数就不需要依赖于某一个类,而且更加容易理解,不易出错:

func updateX() -> String
 {
    x = ...;
    return x;
 }

 func printX() 
 {
   String x = updateX();
   print(x);
 }

CGRect函数

其实iOS内部已经提供了相应的获取CGRect各个部分的函数了,它们的可读性比较高,而且简短,推荐使用:

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 };

范型

建议在定义NSArray和NSDictionary时使用泛型,可以保证程序的安全性:

NSArray<NSString *> *testArr = [NSArray arrayWithObjects:@"Hello", @"world", nil];
NSDictionary<NSString *, NSNumber *> *dic = @{@"key":@(1), @"age":@(10)};

Block

  1. Block属性应该使用copy关键字
typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;//将block拷贝到堆中

为常用的Block类型创建typedef
如果我们需要重复创建某种block(相同参数,返回值)的变量,我们就可以通过typedef来给某一种块定义属于它自己的新类型

int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value)
{
     // Implementation
     return someInt;
}
这个Block有一个bool参数和一个int参数,并返回int类型。我们可以给它定义类型:
typedef int(^EOCSomeBlock)(BOOL flag, int value);

再次定义的时候,就可以通过简单的赋值来实现:

EOCSomeBlock block = ^(BOOL flag, int value){
     // Implementation
};

定义作为参数的Block

- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;

这里的Block有一个NSData参数,一个NSError参数并没有返回值

typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”

字面量语法

尽量使用字面量值来创建 NSString , NSDictionary , NSArray , NSNumber 这些不可变对象:

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" ];
NSNumber *shouldUseLiterals = [NSNumber numberWithBool:YES];NSNumber *buildingZIPCode = [NSNumber numberWithInteger:10018]; 

面向协议编程

如果某些功能(方法)具备可复用性,我们就需要将它们抽取出来放入一个抽象接口文件中(在iOS中,抽象接口即协议),让不同类型的对象遵循这个协议,从而拥有相同的功能。

因为协议是不依赖于某个对象的,所以通过协议,我们可以解开两个对象之间的耦合。如何理解呢?我们来看一下下面这个例子:
现在有一个需求:在一个UITableViewController里面拉取feed并展示出来。

方案一:

定义一个拉取feed的类ZOCFeedParser,这个类有一些代理方法实现feed相关功能:

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;@end 

@interface ZOCFeedParser : NSObject
@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic, strong) NSURL *url; 

- (id)initWithURL:(NSURL *)url; 
- (BOOL)start; 
- (void)stop; 
@end 

然后在ZOCTableViewController里面传入ZOCFeedParser,并遵循其代理方法,实现feed的拉取功能。

@interface ZOCTableViewController : UITableViewController<ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser; 
@end 

具体应用:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"]; 
ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL]; 
ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser]; 
feedParser.delegate = tableViewController; 

OK,现在我们实现了需求:在ZOCTableViewController里面存放了一个ZOCFeedParser对象来处理feed的拉取功能。
但这里有一个严重的耦合问题:ZOCTableViewController只能通过ZOCFeedParser对象来处理feed的拉取功能。
于是我们重新审视一下这个需求:其实我们实际上只需要ZOCTableViewController拉取feed就可以了,而具体是由哪个对象来拉取,ZOCTableViewController并不需要关心。

也就是说,我们需要提供给ZOCTableViewController的是一个更范型的对象,这个对象具备了拉取feed的功能就好了,而不应该仅仅局限于某个具体的对象(ZOCFeedParser)。所以,刚才的设计需要重新做一次修改:

方案二

首先需要在一个接口文件ZOCFeedParserProtocol.h里面定义抽象的,具有拉取feed功能的协议:

@protocol ZOCFeedParserDelegate <NSObject>
@optional
- (void)feedParserDidStart:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info; 
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didParseFeedItem:(ZOCFeedItemDTO *)item; 
- (void)feedParserDidFinish:(id<ZOCFeedParserProtocol>)parser;
- (void)feedParser:(id<ZOCFeedParserProtocol>)parser didFailWithError:(NSError *)error;@end 

@protocol ZOCFeedParserProtocol <NSObject>
@property (nonatomic, weak) id <ZOCFeedParserDelegate> delegate; 
@property (nonatomic, strong) NSURL *url;

- (BOOL)start;
- (void)stop;

@end

而原来的ZOCFeedParser仅仅是需要遵循上面这个协议就具备了拉取feed的功能:

@interface ZOCFeedParser : NSObject <ZOCFeedParserProtocol> 
- (id)initWithURL:(NSURL *)url;//仅仅需要通过传入url即可,其他事情都交给ZOCFeedParserProtocol@end 

而且,ZOCTableViewController也不直接依赖于ZOCFeedParser对象,我们只需要传给它一个遵循<ZOCFeedParserProtocol>的对象即可。

@interface ZOCTableViewController : UITableViewController <ZOCFeedParserDelegate>
- (instancetype)initWithFeedParser:(id<ZOCFeedParserProtocol>)feedParser;
@end

这样一来,ZOCTableViewController和ZOCFeedParser之间就没有直接的关系了。以后,如果我们想:

  • 给这个feed拉取器增加新的功能:仅需要修改ZOCFeedParserProtocol.h文件。
  • 更换一个feed拉取器实例:创建一个新类型来遵循ZOCFeedParserProtocol.h即可。

人已赞赏
iOS文章

iOS 获取Wi-Fi的ssid和bssid

2019-12-20 15:20:38

iOS文章

iOS 13 SceneDelegate适配

2019-12-21 9:45:44

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
有新消息 消息中心
搜索