美文网首页
iOS代码规范

iOS代码规范

作者: 梵生 | 来源:发表于2019-05-21 14:20 被阅读0次
    1. if 语句
      if 语句体应该总是被大括号包围。
    if (!error) {
        return success;
    }
    

    拿常量去和变量做对比。
    尽量避免使用YES或NO对空做判断,尽量使用感叹号作为运算符。

    if ([myValue isEqual:@42]) { ...
    if (someObject) { ...
    if (![someObject boolValue]) { ...
    if (!someObject) { ...
    

    不要嵌套 if 语句。使用多个 return 可以避免增加循环的复杂度,并提高代码的可读性。

    - (void)someMethod {
        if (![someOther boolValue]) {
            return;
        }
    
        // Do something important
    }
    

    当有一个复杂的 if 子句的时候,你应该把它们提取出来赋给一个 BOOL 变量。

    BOOL nameContainsSwift  = [sessionName containsString:@"Swift"];
    BOOL isCurrentYear      = [sessionDateCompontents year] == 2014;
    BOOL isSwiftSession     = nameContainsSwift && isCurrentYear;
    
    if (isSwiftSession) {
        // Do something very cool
    }
    
    1. 三元运算符
      一个条件语句的所有的变量应该是已经被求值了的。
      当三元运算符的第二个参数(if 分支)返回和条件语句中已经检查的对象一样的对象的时候,使用 ? : 写法
    result = a > b ? x : y;
    result = object ? : [self createObject];
    
    1. case语句
      当一个 case 包含了多行语句的时候,需要加上括号。break需要放到括号外面。
    switch (condition) {
        case 1:
            // ...
            break;
        case 2: {
            // ...
            // Multi-line example using braces
            }
            break;
        case 3:
            // ...
            break;
        default:
            // ...
            break;
    }
    

    当在 switch 语句里面使用一个可枚举的变量的时候,default 是不必要的。这样如果新的值加入到 enum,程序员会马上收到一个 warning 通知。

    switch (menuType) {
        case ZOCEnumNone:
            // ...
            break;
        case ZOCEnumValue1:
            // ...
            break;
        case ZOCEnumValue2:
            // ...
            break;
    }
    
    1. 枚举类型
      建议写法
    typedef enum : NSUInteger {
        MyEnumValueA,
        MyEnumValueB,
        MyEnumValueC,
    } MyEnum;
    
    1. 命名规范
      推荐使用长的、描述性的方法和变量名,变量名跟在 * 后面。
    UIButton *settingsButton;
    

    常量应该以驼峰法命名,并以相关类名作为前缀。不推荐使用#define。

    static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4;
    static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
    static const CGFloat ZOCImageThumbnailHeight = 50.0f;
    

    常量应该在头文件中以这样的形式暴露给外部,并在实现文件中为它赋值:

    extern NSString *const ZOCCacheControllerDidClearCacheNotification;
    

    使用内联函数,而不是 define 来实现一些常用的简单判断方法。

    static inline BOOL IsEmpty(id thing) {
        return thing == nil || [thing isEqual:[NSNull null]]
        || ([thing respondsToSelector:@selector(length)]
            && [(NSData *)thing length] == 0)
        || ([thing respondsToSelector:@selector(count)]
            && [(NSArray *)thing count] == 0);
    }
    

    方法名与方法类型 (-/+ 符号)之间应该以空格间隔。方法段之间也应该以空格间隔。参数前应该总是有一个描述性的关键词。
    尽可能少用 "and" 这个词。它不应该用来阐明有多个参数。

    - (void)setExampleText:(NSString *)text image:(UIImage *)image;
    - (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
    - (id)viewWithTag:(NSInteger)tag;
    - (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
    

    使用字面值来创建不可变的 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;
    

    类名应该以三个大写字母作为前缀(双字母前缀为 Apple 的类预留)。
    当你创建一个子类的时候,你应该把说明性的部分放在前缀和父类名的在中间。

    举个例子:如果你有一个 ZOCNetworkClient 类,子类的名字会是ZOCTwitterNetworkClient (注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之间); 按照这个约定, 一个UIViewController 的子类会是 ZOCTimelineViewController.
    
    1. 初始化规范
      Objective-C 有指定初始化方法(designated initializer)和间接(secondary initializer)初始化方法的观念。designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化的初始化方法。
    @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
    

    initWithTitle:date:location: 就是 designated 初始化方法,另外的两个是 secondary 初始化方法。因为它们仅仅是调用类实现的 designated 初始化方法

    一个类应该有且只有一个 designated 初始化方法,其他的初始化方法应该调用这个 designated 的初始化方法。
    在你希望提供你自己的初始化函数的时候,你应该遵守这三个步骤来保证获得正确的行为:
    a.定义你的 designated initializer,确保调用了直接超类的 designated initializer。
    b.重写直接超类的 designated initializer。调用你的新的 designated initializer。
    c.为新的 designated initializer 写文档。
    d.禁用其他的初始化方法,使用编译器指令标记为 UNAVAILABLE。

    
    @interface ZOCNewsViewController : UIViewController
    
    - (instancetype)initWithNews:(ZOCNews *)news ZOC_DESIGNATED_INITIALIZER;
    - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
    - (instancetype)init ZOC_UNAVAILABLE_INSTEAD(initWithNews:);
    
    @end
    
    @implementation ZOCNewsViewController
    
    - (id)initWithNews:(ZOCNews *)news {
        // call to the immediate superclass's designated initializer (调用直接超类的 designated initializer)
        self = [super initWithNibName:nil bundle:nil];
        if (self) {
            _news = news;
        }
        return self;
    }
    
    // Override the immediate superclass's designated initializer (重写直接父类的  designated initializer)
    - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
        // call the new designated initializer
        return [self initWithNews:nil];
    }
    
    @end
    
    1. 类簇
      类簇(Class cluster) 是 Apple 对抽象工厂设计模式的称呼。
      class cluster 的想法很简单: 使用信息进行(类的)初始化处理期间,会使用一个抽象类(通常作为初始化方法的参数或者判定环境的可用性参数)来完成特定的逻辑或者实例化一个具体的子类。
    @implementation ZOCKintsugiPhotoViewController
    
    - (id)initWithPhotos:(NSArray *)photos {
        if ([self isMemberOfClass:ZOCKintsugiPhotoViewController.class]) {
            self = nil;
    
            if ([UIDevice isPad]) {
                self = [[ZOCKintsugiPhotoViewController_iPad alloc] initWithPhotos:photos];
            }
            else {
                self = [[ZOCKintsugiPhotoViewController_iPhone alloc] initWithPhotos:photos];
            }
            return self;
        }
        return [super initWithNibName:nil bundle:nil];
    }
    
    @end
    
    1. 单例
      请使用一个线程安全的模式来创建共享的实例。对于 GCD,用 dispatch_once() 函数就可以。
      记得把其他的初始化方法标记为NS_UNAVAILABLE,以防止该类还可以创建实例对象。
    + (instancetype)sharedInstance {
        static id sharedInstance = nil;
        static dispatch_once_t onceToken = 0;
        dispatch_once(&onceToken, ^{
            sharedInstance = [[self alloc] init];
        });
        return sharedInstance;
    }
    
    1. 属性
      属性应该尽可能描述性地命名,避免缩写,并且是小写字母开头的驼峰命名。
      你应该总是使用 setter 和 getter 方法访问属性,除了 init 和 dealloc 方法。
      当使用 setter getter 方法的时候尽量使用点符号。应该总是用点符号来访问以及设置属性。

    永远不要在 init 方法(以及其他初始化方法)里面用 getter 和 setter 方法,你应当直接访问实例变量。这样做是为了防止有子类时,出现这样的情况:它的子类最终重写了其 setter 或者 getter 方法,因此导致该子类去调用其他的方法、访问那些处于不稳定状态,或者称为没有初始化完成的属性或者 ivar 。记住一个对象仅仅在 init 返回的时候,才会被认为是达到了初始化完成的状态。

    @property (nonatomic, readwrite, copy) NSString *name;
    view.backgroundColor = [UIColor orangeColor];
    [UIApplication sharedApplication].delegate;
    

    属性的参数应该按照下面的顺序排列: 原子性,读写 和 内存管理。
    为了完成一个共有的 getter 和一个私有的 setter,你应该声明公开的属性为 readonly 并且在类扩展中重新定义通用的属性为 readwrite 的。

    // .h文件中
    @interface MyClass : NSObject
    @property (nonatomic, readonly, strong) NSObject *object;
    @end
    // .m文件中
    @interface MyClass ()
    @property (nonatomic, readwrite, strong) NSObject *object;
    @end
    
    @implementation MyClass
    // Do Something cool
    @end
    

    描述BOOL属性的词如果是形容词,那么setter不应该带is前缀,但它对应的 getter 访问器应该带上这个前缀,如:

    @property (assign, getter=isEditable) BOOL editable;
    

    任何可以用一个可变的对象设置的((比如 NSString,NSArray,NSURLRequest))属性的内存管理类型必须是 copy 的。copy 属性会让 array 的 setter 方法为 array = [mutableArray copy], [mutableArray copy] 返回的是不可变的 NSArray 实例,就保证了正确性。
    你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本。

    /* .h */
    @property (nonatomic, readonly) NSArray *elements
    
    /* .m */
    - (NSArray *)elements {
        return [self.mutableElements copy];
    }
    
    1. 懒加载
      当实例化一个对象需要耗费很多资源,或者配置一次就要调用很多配置相关的方法而你又不想弄乱这些方法时,我们需要重写 getter 方法以延迟实例化,而不是在 init 方法里给对象分配内存。
    - (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"]; // 毫秒是SSS,而非SSSSS
        }
        return _dateFormatter;
    }
    
    1. 方法
    • 参数校验
      你的方法可能要求一些参数来满足特定的条件(比如不能为nil)。
      如果是阻塞类的条件要求(比如条件不满足说明程序异常,无法正常加载),在这种情况下最好使用 NSParameterAssert() 来断言条件是否成立,以方便在开发阶段快速定位问题。
      如果是异常类的条件要求(比如用户输入的条件不满足,但产品设计上有异常流程处理),在这种情况下,应在方法顶部优先做参数校验,参数校验后面才是正常的方法逻辑处理。不要把参数校验放到方法中间或方法内部的局部变量初始化之后。
    • 私有方法
      永远不要在你的私有方法前加上 _ 前缀。这个前缀是 Apple 保留的。不要冒重写苹果的私有方法的险。应使用业务前缀“business_”。
    • 方法拆分
      一个方法应只做一件事,判断方法就是,看方法名是否只有一个动词。
      超过200行的代码,应考虑是否需要拆分为多个方法。
    • 返回值类型
      有明确操作对象且操作对象是在方法内部生成的,应返回此操作对象,而不是返回void。
      例如- (UIViewController *)pushToLoginViewController
    1. 相等性
      当你要实现相等性的时候记住这个约定:你需要同时实现isEqual 和 hash方法。如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。一定要注意 hash 方法不能返回一个常量。
      这个约定当对象被存储在集合中(如 NSDictionary 和 NSSet 在底层使用 hash 表数据的数据结构)的时候,用来查找这些对象的。
      你总是应该用 isEqualTo<#class-name-without-prefix#>: 这样的格式实现一个相等性检查方法。如果你这样做,会优先调用这个方法来避免上面的类型检查。
    @implementation ZOCPerson
    
    - (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;
    }
    
    - (NSUInteger)hash {
        return [self.name hash] ^ [self.birthday hash];
    }
    
    1. Categories
      我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)zoc_myCategoryMethod。
    @interface NSDate (ZOCTimeExtensions)
    - (NSString *)zoc_timeAgoShort;
    @end
    
    1. Protocols
      此处不强制要求,如需了解可查阅查考文献。
    2. NSNotification
      当你定义你自己的 NSNotification 的时候你应该把你的通知的名字定义为一个字符串常量。用类名前缀作为这个通知名字的前缀,用一个 Did/Will 这样的动词来描述行为,以及用 "Notifications" 作为后缀。
    // Foo.h
    extern NSString * const ZOCFooDidBecomeBarNotification
    
    // Foo.m
    NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";
    
    1. 代码美化
      方法的大括号和其他的大括号(if/else/switch/while 等) 总是在同一行开始,在新起一行结束。
      控制语句 (if-else, for, switch) 的括号前后总是要有一个空格。
    if (user.isHappy) {
        // Do something
    }
    else {
        // Do something else
    }
    

    应该总是让冒号对齐。

    [UIView animateWithDuration:1.0
                     animations:^{
                         // something
                     }
                     completion:^(BOOL finished) {
                         // something
                     }];
    
    1. 代码组织
      我们建议使用 #pragma mark - 来分离:
    • 不同功能组的方法
    • protocols 的实现
    • 对父类方法的重写

    使用 #error 手动标记一个错误,使用 #warning 手动标记一个警告。

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

    使用 // TODO: 手动标记未完成的任务

    - (NSInteger)giveMeFive {
        // TODO: Give me five
        return 0;
    }
    
    1. 字符串文档
      每个接口、类别以及协议应辅以注释,以描述它的目的及与整个项目的关系。更多的例子可以看 Google 代码风格指南中的 File and Declaration Comments
      字符串文档应该描述函数的调用符号和语义,而不是它如何实现。
      文本应该用一个动词 ("return") 来描述函数的作用。

    一个函数必须有一个字符串文档,除非它符合下面的所有条件:

    • 非公开
    • 很短
    • 显而易见

    短文档适用于单行的文件,包括注释斜杠。它适合简短的函数,特别是(但不仅仅是)非 public 的 API:

    // Return a user-readable form of a Frobnozz, html-escaped.
    

    如果描述超过一行,应改用长字符串文档:

    • 以/**开始
    • 换行写一句总结的话,以?或者!或者.结尾。
    • 空一行
    • 在与第一行对齐的位置开始写剩下的注释
    • 最后用*/结束。
    /**
     This comment serves to demonstrate the format of a docstring.
    
     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.
    */
    
    1. 注释
      代码本身应该尽可能就像文档一样表示意图,只需要很少的注释。
      一个类的文档应该只在 .h 文件里用 Doxygen/AppleDoc 的语法书写。
    /**
     *  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;
    
    1. block
      使用 block 定义异步接口:
    - (void)downloadObjectsAtPath:(NSString *)path
                       completion:(void(^)(NSArray *objects, NSError *error))completion;
    

    当你定义一个类似上面的接口的时候,尽量使用一个单独的 block 作为接口的最后一个参数。把需要提供的数据和错误信息整合到一个单独 block 中,比分别提供成功和失败的 block 要好。

    以下是你应该这样做的原因:

    • 通常这成功处理和失败处理会共享一些代码(比如让一个进度条或者提示消失);
    • Apple 也是这样做的,与平台一致能够带来一些潜在的好处;
    • block 通常会有多行代码,如果不作为最后一个参数放在后面的话,会打破调用点;
    • 使用多个 block 作为参数可能会让调用看起来显得很笨拙,并且增加了复杂性。

    需要额外注意的是,如果方法中有block参数,在方法实现内部,任何return之前,都要调用block回调,否则会给调用方造成未知错误。在block使用之前,需要判断block是否为nil,否则外界传入nil,会造成崩溃。

    看上面的方法,完成处理的 block 的参数很常见:第一个参数是调用者希望获取的数据,第二个是错误相关的信息。这里需要遵循以下两点:

    • 若 objects 不为 nil,则 error 必须为 nil
    • 若 objects 为 nil,则 error 必须不为 nil

    因为调用者更关心的是实际的数据,就像这样:

    - (void)downloadObjectsAtPath:(NSString *)path
                       completion:(void(^)(NSArray *objects, NSError *error))completion {
        if (objects) {
            // do something with the data
        }
        else {
            // some error occurred, 'error' variable should not be nil by contract
        }
    }
    

    此外,Apple 提供的一些同步接口在成功状态下向 error 参数(如果非 NULL) 写入了垃圾值,所以检查 error 的值可能出现问题。

    避免循环引用
    当 block 不是作为一个 property 的时候使用,可以直接在 block 里面使用关键词 self。

    [self executeBlock:^(NSData *data, NSError *error) {
        [self doSomethingWithData:data];
    }];
    

    当 block 被声明为一个 property 的时候,需要在 block 外定义一个 __weak 的 引用到 self,并且在 block 里面使用这个弱引用。

    __weak typeof(self) weakSelf = self;
    dispatch_block_t block = ^{
        [weakSelf doSomething]; // weakSelf != nil
        // preemption, weakSelf turned nil
        [weakSelf doSomethingElse]; // weakSelf == nil
    };
    

    当 block 作为属性和并发执行有关。当涉及异步的服务的时候,block 可以在之后被执行,并且不会发生关于 self 是否存在的问题。这时候需要在 block 外定义一个 __weak 的 引用到 self,并在在 block 内部通过这个弱引用定义一个 __strong 的引用。

    __weak typeof(self) weakSelf = self;
    myObj.myBlock = ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
          [strongSelf doSomething]; // strongSelf != nil
          // preemption, strongSelf still not nil(抢占的时候,strongSelf 还是非 nil 的)
          [strongSelf doSomethingElse]; // strongSelf != nil
        }
        else {
            // Probably nothing...
            return;
        }
    };
    

    参考文献:
    禅与 Objective-C 编程艺术
    Google 开源项目风格指南

    相关文章

      网友评论

          本文标题:iOS代码规范

          本文链接:https://www.haomeiwen.com/subject/bwyhzqtx.html