开发技巧分类

iOS 代码规范

2018-03-20  本文已影响456人  3efefee71c8c

iOS规范:仅适用于iOS的代码规范(使用Objective-C语言)。

一.代码应该简洁易懂,逻辑清晰

二.面向变化编程,而不是面向需求编程。

三:先保证程序的正确性,防止过度工程

1.先把眼前的问题解决掉,解决好,再考虑将来的扩展问题。
2.先写出可用的代码,反复推敲,再考虑是否需要重用的问题。
3.先写出可用,简单,明显没有bug的代码,再考虑测试的问题。

四.IOS 规范


变量

1. 变量名必须使用驼峰格式

类,协议使用大驼峰:

CKBaseViewController.h
<HeaderViewDelegate>

对象等局部变量使用小驼峰:

NSString *personName = @"";
NSUInteger totalCount = 0;

2. 变量的名称必须同时包含功能与类型

UIButton *addBtn //添加按钮
UILabel *nameLbl //名字标签
NSString *addressStr//地址字符串

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

类型 后缀
UIViewController VC
UIView View
UILabel label
NSDictionary Dict
NSString str
... ...

常量

1. 常量以相关类名作为前缀

推荐这样写:

static const NSString LCStrName = ckStr ;

不推荐这样写:

static const NSString strName = ckStr;

2. 建议使用类型常量,不建议使用#define预处理命令

define定义:
系统在编译时候,就将其全部替换,而不会对其变量进行类型等属性检查,相对不是很安全,可能存在潜在的问题,而没有发现,宏定义的其他缺点可自行百度。

类型常量
在头文件 .h文件中
extern NSString *const CKStringConstant;
在实现文件 .m文件中
NSString *const CKStringConstant = @"VALUE";


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

字面量语法

尽量使用字面量值来创建 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];

属性

1. 属性的命名使用小驼峰

推荐这样写:

@property (nonatomic, readwrite, strong) UIButton *confirmButton;

2. 属性的关键字推荐按照 原子性,读写,内存管理的顺序排列

@property (nonatomic, readwrite, copy) NSString *name;
@property (nonatomic, readonly, copy) NSString *gender;
@property (nonatomic, readwrite, strong) UIView *headerView;

3. Block属性应该使用copy关键字

推荐这样写:

typedef void (^ErrorCodeBlock) (id errorCode,NSString *message);
@property (nonatomic, readwrite, copy) ErrorCodeBlock errorBlock;

4. 使用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方法可能会产生某些副作用,例如如果它修改了全局变量,可能会产生难以排查的错误。

5. 除了init和dealloc方法,建议都使用点语法访问属性

使用点语法的好处:

setter:

setter会遵守内存管理语义(strong, copy, weak)。
通过在内部设置断点,有助于调试bug。
可以过滤一些外部传入的值。
捕捉KVO通知。

getter:

允许子类化。
通过在内部设置断点,有助于调试bug。
实现懒加载(lazy initialization)。

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

6. 尽量使用不可变对象

建议尽量把对外公布出来的属性设置为只读,在实现文件内部设为读写。具体做法是:

这样一来,在外部就只能读取该数据,而不能修改它,使得这个类的实例所持有的数据更加安全。而且,对于集合类的对象,更应该仔细考虑是否可以将其设为可变的。

如果在公开部分只能设置其为只读属性,那么就在非公开部分存储一个可变型。所以当在外部获取这个属性时,获取的只是内部可变型的一个不可变版本,例如:

在公共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;
}

方法

1. 方法名中不应使用and,而且签名要与对应的参数名保持高度一致

推荐这样写:

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

不推荐这样写:

- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;

2. 方法实现时,如果参数过长,则令每个参数占用一行,以冒号对齐。

- (void)doSomethingWith:(NSString *)theFoo
                   rect:(CGRect)theRect
               interval:(CGFloat)theInterval
{
   //Implementation
}

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

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

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

4. 方法名用小写字母开头的单词组合而成

- (NSString *)descriptionWithLocale:(id)locale;

5. 方法名前缀

- (void)refreshHeaderViewWithCount:(NSUInteger)count;
- (void)updateDataSourceWithViewModel:(ViewModel*)viewModel;


1. 类的名称应该以三个大写字母为前缀;创建子类的时候,应该把代表子类特点的部分放在前缀和父类名的中间

推荐这样写:

//父类
ZOCSalesListViewController

//子类
ZOCDaySalesListViewController
ZOCMonthSalesListViewController

2. initializer && dealloc

推荐:

2.1 dealloc方法里面应该直接访问实例变量,不应该用点语法访问
2.2 init方法的写法:
- (instancetype)init 
{ 
    self = [super init]; // call the designated initializer 
    if (self) { 
        // Custom initialization 
    } 
    return self; 
}
2.3 指定初始化方法

指定初始化方法(designated initializer)是提供所有的(最多的)参数的初始化方法,间接初始化方法(secondary initializer)有一个或部分参数的初始化方法。

注意事项1:间接初始化方法必须调用指定初始化方法。

@implementation ZOCEvent 

//指定初始化方法
- (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date 
location:(CLLocation *)location
{ 
    self = [super init]; 
      if (self) {
         _title = title; 
         _date = date; 
         _location = location; 
      } 
    return self; 
} 

//间接初始化方法
-  (instancetype)initWithTitle:(NSString *)title date:(NSDate *)date
{ 
    return [self initWithTitle:title date:date location:nil];
}

//间接初始化方法
-  (instancetype)initWithTitle:(NSString *)title 
{ 
    return [self initWithTitle:title date:[NSDate date] location:nil];
}
 @end

注意事项2:如果直接父类有指定初始化方法,则必须调用其指定初始化方法

- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 
{
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 
    if (self) {
    }
    return self; 
}

注意事项3:如果想在当前类自定义一个新的全能初始化方法,则需要如下几个步骤

例:

@implementation ZOCNewsViewController

//新的指定初始化方法
- (id)initWithNews:(ZOCNews *)news 
{
    self = [super initWithNibName:nil bundle:nil]; 
    if (self) {
        _news = news;
    }
    return self;
} 

// 重载父类的初始化方法
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil{
    return [self initWithNews:nil]; 
}
@end

在这里,重载父类的初始化方法并在内部调用新定义的指定初始化方法的原因是你不能确定调用者调用的就一定是你定义的这个新的指定初始化方法,而不是原来从父类继承来的指定初始化方法。
假设你没有重载父类的指定初始化方法,而调用者却恰恰调用了父类的初始化方法。那么调用者可能永远都调用不到你自己定义的新指定初始化方法了。
而如果你成功定义了一个新的指定初始化方法并能保证调用者一定能调用它,你最好要在文档中明确写出哪一个才是你定义的新初始化方法。或者你也可以使用编译器指令attribute((objc_designated_initializer))来标记它。

3. 所有返回类对象和实例对象的方法都应该使用instancetype

将instancetype关键字作为返回值的时候,可以让编译器进行类型检查,同时适用于子类的检查,这样就保证了返回类型的正确性(一定为当前的类对象或实例对象)

推荐这样写:

@interface ZOCPerson
+ (instancetype)personWithName:(NSString *)name; 
@end

不推荐这样写:

@interface ZOCPerson
+ (id)personWithName:(NSString *)name; 
@end

4. 在类的头文件中尽量少引用其他头文件

有时,类A需要将类B的实例变量作为它公共API的属性。这个时候,我们不应该引入类B的头文件,而应该使用向前声明(forward declaring)使用class关键字,并且在A的实现文件引用B的头文件。

5. 类的布局

#pragma mark - Life Cycle Methods
- (instancetype)init
- (void)dealloc

- (void)viewWillAppear:(BOOL)animated
- (void)viewDidAppear:(BOOL)animated
- (void)viewWillDisappear:(BOOL)animated
- (void)viewDidDisappear:(BOOL)animated

#pragma mark - Override Methods

#pragma mark - Intial Methods

#pragma mark - Network Methods

#pragma mark - Target Methods

#pragma mark - Public Methods

#pragma mark - Private Methods

#pragma mark - UITableViewDataSource  
#pragma mark - UITableViewDelegate  

#pragma mark - Lazy Loads

#pragma mark - NSCopying  

#pragma mark - NSObject  Methods

分类

1. 分类添加的方法需要添加前缀和下划线

推荐这样写:

@interface NSDate (ZOCTimeExtensions)
 - (NSString *)zoc_timeAgoShort;
@end

不推荐这样写:

@interface NSDate (ZOCTimeExtensions) 
- (NSString *)timeAgoShort;
@end
2. 把类的实现代码分散到便于管理的多个分类中

一个类可能会有很多公共方法,而且这些方法往往可以用某种特有的逻辑来分组。我们可以利用Objecctive-C的分类机制,将类的这些方法按一定的逻辑划入几个分区中。
例:
先看一个没有使用无分类的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

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

/* Friendship methods */
- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

/* Work methods */
- (void)performDaysWork;
- (void)takeVacationFromWork;

/* Play methods */
- (void)goToTheCinema;
- (void)goToSportsGame;

@end

分类之后:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, strong, readonly) NSArray *friends;

- (id)initWithFirstName:(NSString*)firstName

andLastName:(NSString*)lastName;

@end

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end

@interface EOCPerson (Work)

- (void)performDaysWork;
- (void)takeVacationFromWork;

@end

@interface EOCPerson (Play)

- (void)goToTheCinema;
- (void)goToSportsGame;

@end

其中,FriendShip分类的实现代码可以这么写:

// EOCPerson+Friendship.h
#import "EOCPerson.h"

@interface EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person;
- (void)removeFriend:(EOCPerson*)person;
- (BOOL)isFriendsWith:(EOCPerson*)person;

@end

// EOCPerson+Friendship.m
#import "EOCPerson+Friendship.h"

@implementation EOCPerson (Friendship)

- (void)addFriend:(EOCPerson*)person 
{
   /* ... */
}

- (void)removeFriend:(EOCPerson*)person 
{
   /* ... */
}

- (BOOL)isFriendsWith:(EOCPerson*)person 
{
   /* ... */
}

@end

单例

1. 单例不能作为容器对象来使用

单例对象不应该暴露出任何属性,也就是说它不能作为让外部存放对象的容器。它应该是一个处理某些特定任务的工具,比如在iOS中的GPS和加速度传感器。我们只能从他们那里得到一些特定的数据。

2. 使用dispatch_once来生成单例

推荐这样写:

+ (instancetype)sharedInstance 
{ 
 static id sharedInstance = nil; 
 static dispatch_once_t onceToken = 0;
       dispatch_once(&onceToken, ^{ 
  sharedInstance = [[self alloc] init];
  }); 
 return sharedInstance; 
}

不推荐这样写:

+ (instancetype)sharedInstance 
{ 
 static id sharedInstance; 
 @synchronized(self) { 
 if (sharedInstance == nil) {  sharedInstance = [[MyClass alloc] init]; 
 } } 
 return sharedInstance; 
}

相等性的判断

判断两个person类是否相等的合理做法:
-  (BOOL)isEqual:(id)object 
{

     if (self == object) {  
            return YES; //判断内存地址
    } 

    if (![object isKindOfClass:[ZOCPerson class]]) { 
     return NO; //是否为当前类或派生类 
     } 

     return [self isEqualToPerson:(ZOCPerson *)object]; 

}

//自定义的判断相等性的方法
-  (BOOL)isEqualToPerson:(Person *)person 
{ 
        if (!person) {  
              return NO;
        } 
        BOOL namesMatch = (!self.name && !person.name) || [self.name isEqualToString:person.name]; 
        BOOL birthdaysMatch = (!self.birthday && !person.birthday) || [self.birthday isEqualToDate:person.birthday]; 
        return haveEqualNames && haveEqualBirthdays; 
}

方法文档

一个函数(方法)必须有一个字符串文档来解释,除非它:

非公开,私有函数。
很短。
显而易见。

而其余的,包括公开接口,重要的方法,分类,以及协议,都应该伴随文档(注释):

以/开始
第二行识总结性的语句
第三行永远是空行
在与第二行开头对齐的位置写剩下的注释。

建议这样写:

/This comment serves to demonstrate the format of a doc string.

Note that the summary line is always at most one line long, and after the opening block comment,
and each line of text is preceded by a single space.
*/

看一个指定初始化方法的注释:

/ 
  *  Designated initializer. *
  *  @param store The store for CRUD operations.
  *  @param searchService The search service used to query the store. 
  *  @return A ZOCCRUDOperationsStore object.
  */ 
- (instancetype)initWithOperationsStore:(id<ZOCGenericStoreProtocol>)store searchService:(id<ZOCGenericSearchServiceProtocol>)searchService;

多用队列,少用同步锁来避免资源抢夺

多个线程执行同一份代码时,很可能会造成数据不同步。建议使用GCD来为代码加锁的方式解决这个问题。

方案一:使用串行同步队列来将读写操作都安排到同一个队列里:

_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);

//读取字符串
- (NSString*)someString 
{

         __block NSString *localSomeString;
         dispatch_sync(_syncQueue, ^{
            localSomeString = _someString;
        });
         return localSomeString;

}

//设置字符串
- (void)setSomeString:(NSString*)someString 
{

     dispatch_sync(_syncQueue, ^{
        _someString = someString;
    });
}

这样一来,读写操作都在串行队列进行,就不容易出错。

但是,还有一种方法可以让性能更高:

方案二:将写操作放入栅栏快中,让他们单独执行;将读取操作并发执行。

_syncQueue = dispatch_queue_create("com.custom.queue", DISPATCH_QUEUE_CONCURRENT);

//读取字符串
- (NSString*)someString 
{

     __block NSString *localSomeString;
     dispatch_sync(_syncQueue, ^{
        localSomeString = _someString;
    });
     return localSomeString;
}//设置字符串
- (void)setSomeString:(NSString*)someString 
{

     dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });

}

显然,数据的正确性主要取决于写入操作,那么只要保证写入时,线程是安全的,那么即便读取操作是并发的,也可以保证数据是同步的。
这里的dispatch_barrier_async方法使得操作放在了同步队列里“有序进行”,保证了写入操作的任务是在串行队列里。


NSArray& NSMutableArray

1. addObject之前要非空判断。

2. 取下标的时候要判断是否越界。

3. 取第一个元素或最后一个元素的时候使用firtstObject和lastObject


NSNotification

1. 通知的名称

建议将通知的名字作为常量,保存在一个专门的类中:

// Const.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Const.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

2. 通知的移除

通知必须要在对象销毁之前移除掉。


其他

1. Xcode工程文件的物理路径要和逻辑路径保持一致。

2. 忽略没有使用变量的编译警告

对于某些暂时不用,以后可能用到的临时变量,为了避免警告,我们可以使用如下方法将这个警告消除:

- (NSInteger)giveMeFive 
{ 
 NSString *foo; 
 #pragma unused (foo) 
 return 5; 
}

3. 手动标明警告和错误

手动明确一个错误:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor 
{ 
 #error Whoa, buddy, you need to check for zero here! 
 return (dividend / divisor); 
}

手动明确一个警告:

- (float)divide:(float)dividend by:(float)divisor 
{ 
 #warning Dude, don't compare floating point numbers like this! 
     if (divisor != 0.0) { 
        return (dividend / divisor); 
     } else {  
        return NAN; 
 } 
}

参考文献:

上一篇下一篇

猜你喜欢

热点阅读