《禅与Objective-C 编程艺术》读书笔记

前言

《禅与Objective-C 编程艺术》虽然内容不是很多,但是却又很多好用的Tips。可以将其作为OC的编码规范来使用。虽然OC逐渐在被Swift所替代,但是很多项目短时间是不会全部转向Swift的,而且本文有很多内容也是独立于编程语言的。总得来说是一本很不错的书,除了这本书外还有一本书叫《Effective Objective-C 2.0: 52 Specific Ways to Improve Your iOS and OS X Programs》,这两本书内容都差不多,后者比前者讲的更详细一些,对于初级程序猿来说会受益匪浅。

条件语句

语句体总是使用大括号来包围避免错误。

推荐

if (!error) {
   return success;
}

不推荐

if (!error)
return success;
或者
if (!error) return success;

不要使用尤达表达式(尤达表达式:使用常量去和变量比较)

推荐

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

不推荐

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

nil和BOOL检查使用感叹号来判断

因为nil是解释到NO所以没必要在条件语句里面把它和其他值比较。同时,不要直接把它和YES比较,因为YES的定义是1BOOL是8位的,实际上是char类型。

推荐

if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...
    ```
    不推荐
    
    ```
if (someObject == YES) { ... // Wrong
if (myRawValue == YES) { ... // Never do this.
if ([someObject boolValue] == NO) { ...

不要嵌套if语句,多个return是OK的

推荐

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

不推荐

- (void)someMethod {
  if ([someOther boolValue]) {
    //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
}

三元运算符?:,应该只用在它能让代码更加清晰的地方

推荐

result = a > b ? x : y;

result = object ? : [self createObject]; // 第二个参数返回和条件语句中已经检查的对象一致时

不推荐

result = a > b ? x = c > d ? c : d : y;

result = object ? object : [self createObject]; // 第二个参数返回和条件语句中已经检查的对象一致时

当方法返回一个错误参数引用的时候,检查返回值,而不是错误的变量

推荐

NSError *error = nil;
if (![self trySomethingWithError:&error]) {
    // Handle Error
}

Case语句

括号在Case语句中是非必要的,但当一个Case包含多行语句时,要加上括号

switch (condition) {
    case 1:
        // ...
        break;
    case 2: {
        // ...
        // Multi-line example using braces
        break;
       }
    case 3:
        // ...
        break;
    default: 
        // ...
        break;
}

当switch语句里使用可枚举的变量的时候,default是不必要的,没有default有利于错误的排查

switch (menuType) {
    case ZOCEnumNone:
        // ...
        break;
    case ZOCEnumValue1:
        // ...
        break;
    case ZOCEnumValue2:
        // ...
        break;
}

关于遍历

多用快速遍历(for in)或者block的方式遍历,少用常规for循环

Enumerated Types枚举类型推荐使用NS_ENUM()

用枚举表示状态,选项,状态码,可读性和可用性会大大增加

命名规则

尽量遵守Apple命名约定,推荐使用长的,描述性的方法和变量名

推荐

UIButton *settingsButton;

不推荐

UIButton *setBut; // 方法和变量尽量不要带set get 前缀

Constants常量

多用类型常量,少用#define预处理指令,有利于错误检测

常量命名:

常量如果局限于某个实现文件(.m)中时,使用前缀k。如果常量在类之外可见,则使用类名作为前缀。常量应该用static声明,并且不要使用#define,除非他就是明确作为一个宏。

推荐

static const NSTimeInterval kFadeOutAnimationDuration = 0.4; // 只在.m 可见

static const NSTimeInterval ZOCSignInViewControllerFadeOutAnimationDuration = 0.4; // 类外可见

static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";

不推荐

static const NSTimeInterval fadeOutTime = 0.4;

 #define CompanyName @"Apple Inc.

类型常量如果在类外可见,则应该在.h文件中这样声明extern NSString *const ZOCCacheControllerDidClearCacheNotification;,在.m中这样声明NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";

方法命名在方法类型(-/+符号)后应该有一个空格,方法段之间也该有一个空格,参数名称之前总应有一个描述性关键词,尽量不使用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;

不推荐

- (void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height;  // Never do this.

字面量/语法糖,多使用字面量来创建不可变的实例对象。

推荐

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

美化代码

  • 缩进使用4个空格

  • 方法的大括号和其他的大括号(if/else/switch/while 等) 总是在同一行开始,在新起一行结束。

    推荐:

    if (user.isHappy) {
        //Do something
    }
    else {
        //Do something else
    }
    
    

    不推荐:

    if (user.isHappy)
    {
      //Do something
    } else {
      //Do something else
    }
    
  • 方法之间应该要有一个空行来帮助代码看起来清晰且有组织。 方法内的空格应该用来分离功能,但是通常不同的功能应该用新的方法来定义。

  • 优先使用 auto-synthesis。但是如果必要的话, @synthesize and @dynamic

  • 在实现文件中的声明应该新起一行。

  • 应该总是让冒号对齐。有一些方法签名可能超过三个冒号,用冒号对齐可以让代码更具有可读性。即使有代码块存在,也应该用冒号对齐方法。

    推荐:

    [UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];
    
  • 长行代码可以在第二行以一个间隔(2个空格)延续

    self.productsRequest = [[SKProductsRequest alloc]
      initWithProductIdentifiers:productIdentifiers];
    

代码组织

利用代码块

代码块如果在闭合的圆括号内的话,会返回最后语句的值。

NSURL *url = ({
    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
    [NSURL URLWithString:urlString];
});

Pragma

使用 #Pragma Mark进行代码分组:

  • 不通功能组的实现
  • protocols的实现
  • 对父类方法的重写
- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

#pragma mark - View Lifecycle (View 的生命周期)

- (void)viewDidLoad { /* ... */ }
- (void)viewWillAppear:(BOOL)animated { /* ... */ }
- (void)didReceiveMemoryWarning { /* ... */ }

#pragma mark - Custom Accessors (自定义访问器)

- (void)setCustomProperty:(id)value { /* ... */ }
- (id)customProperty { /* ... */ }

#pragma mark - IBActions  

- (IBAction)submitData:(id)sender { /* ... */ }

#pragma mark - Public

- (void)publicMethod { /* ... */ }

#pragma mark - Private

- (void)zoc_privateMethod { /* ... */ }

#pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }

#pragma mark - ZOCSuperclass

// ... 重载来自 ZOCSuperclass 的方法

#pragma mark - NSObject

- (NSString *)description { /* ... */ }

或者

image

忽略警告:

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[myObj performSelector:mySelector withObject:name];

#pragma clang diagnostic pop
- (NSInteger)giveMeFive
{
    NSString *foo;
    #pragma unused (foo)

    return 5;
}

明确警告与错误标识

  • 手动置为错误#error Whoa, buddy, you need to check for zero here!
  • 手动警告#warning Dude, don't compare floating point numbers like this!

注释

  • .h 头文件中声明该类的作用
  • 接口方法应该有注释
  • 如果变量或方法名不明确的应该加上注释

接口与API设计

  • 类加上前缀,避免命名空间冲突
  • 提供designated initializer
  • 尽量创建不可变对象,不要把可变对象属性公开,而是提供相关方法。
  • 属性如果有读写权限设定,需要用readonly声明
  • 方法命名要清晰,尽量不使用缩略词
  • 私有方法可以加上前缀,如- (void)p_privateMethod,但不要用下划线开头;

类的头文件(.h)中尽量少引入其他头文件,避免互相引用,降低耦合,减少编译时间

.h中要引入类时,只需要声明有这个类,具体细节不用暴露,使用向前声明(forward declaring)该类,即:@class XXXX
在.m实现文件中使用该类时需要知道所有接口袭击,即需要import,#import XXXX

类名,应加上三个大写字母作为前缀(两个字母为Apple类保留,WTF😢),为了减少没有命名空间导致的问题

Initializer和dealloc初始化,将dealloc方法写在实现文件最前面,init方法放在dealloc之后。

使用ARC的话,dealloc方法中不用调用super dealloc,dealloc中只应释放引用,取消KVO的订阅或Notification,不做其他事情。

Designated和secondary Initializer

designated初始化方法提供所有的参数,一个类应该有且只有一个,secondary初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用Designated初始化方法的初始化方法。

eg:

@implementation ZOCEvent
 
 // designated初始化方法,一个类应该有且只有一个
- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
                     location:(CLLocation *)location
{
    self = [super init];
    if (self) {
        _title    = title;
        _date     = date;
        _location = location;
    }
    return self;
}
 
 // 一下为secondary初始化方法
- (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

在类继承中调用任何 designated 初始化方法都是合法的,而且应该保证 所有的 designated initializer 在类继承中是是从祖先(通常是 NSObject)到你的类向下调用的。 实际上这意味着第一个执行的初始化代码是最远的祖先,然后从顶向下的类继承,所有类都有机会执行他们特定的初始化代码。这样,你在你做你的特定的初始化工作前,所有你从超类继承的东西是不可用的状态。即使它的状态不明确,所有 Apple 的框架的 Framework 是保证遵守这个约定的,而且你的类也应该这样做。

定义一个新类的时候有三个不同方式:

  • 不需要重载任何初始化函数
  • 重载designated initializer
  • 定义一个新的designated initializer

第一个方案是最简单的:你不需要增加类的任何初始化逻辑,只需要依照父类的designated initializer。 当你希望提供额外的初始化逻辑的时候你可以重载 designated initializer。你只需要重载你的直接的超类的 designated initializer 并且确认你的实现调用了超类的方法。

在你希望提供你自己的初始化函数的时候,你应该遵守这三个步骤来保证正确的性:

  • 定义你的 designated initializer,确保调用了直接超类的 designated initializer
  • 重载直接超类的 designated initializer。调用你的新的 designated initializer.
  • 为新的 designated initializer 写文档

通过宏来指定,正确实现例子:

@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
 
- (instancetype)initWithNews:(ZOCNews *)news
{
    // call to the immediate superclass's designated initializer
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _news = news;
    }
    return self;
}
 
// Override the immediate superclass's designated initializer (重载直接父类的  designated initializer)
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call the new designated initializer
    return [self initWithNews:nil];
}
 
@end

注意:你应该永远不从 designated initializer 里面调用一个 secondary initializer (如果secondary initializer 遵守约定,它会调用 designated initializer)。如果这样,调用很可能会调用一个子类重写的 init 方法并且陷入无限递归之中。

secondary initializer 是一种方便提供默认值、行为到 designated initializer 的 方法。也就是说,你不应该强制很多初始化操作在这样的方法里面,并且你应该一直假设这个方法不会得到调用。我们保证的是唯一被调用的方法是 designated initializer。 这意味着你的 designated initializer 总是应该调用其他的 secondary initializer 或者你 self 的 designated initializer。有时候,因为错误,可能打成了 super,这样会导致不符合上面提及的初始化顺序(在这个特别的例子里面,是跳过当前类的初始化)

一个相关的返回类型可以明确的规定用instancetype关键字作为返回类型

初始化模式

类簇(class cluster):一个在共有的抽象超类下设置一组私有子类的架构->抽象工厂设计模式,比如NSNumber,NSArray

特点:这个模式的精妙的地方在于,调用者可以完全不管子类,事实上,这可以用在设计一个库,可以用来交换实际的返回的类,而不用去管相关的细节,因为它们都遵从抽象超类的方法。

class cluster 的想法很简单,你经常有一个抽象类在初始化期间处理信息,经常作为一个构造器里面的参数或者环境中读取,来完成特定的逻辑并且实例化子类。这个"public facing" 应该知晓它的子类而且返回适合的私有子类。

单例:如果可能,请尽量避免使用单例而是依赖注入,使用单例请使用线程安全模式来创建共享实例dispatch_once()注意单例的滥用

单例模式应该运用于类及类的接口趋向于作为单例来使用的情况

属性,属性应该尽可能描述性的命名,避免缩写,而且是首字母小写的驼峰命名,应该总是使用settergetter方法访问属性,除了initdealloc方法。

eg:NSString *text,不推荐NSString* textNSString * text

属性getter/setter的好处:

  • 使用 setter 会遵守定义的内存管理语义(strong, weak, copy etc...)。举个例子,copy 每个时候你用 setter 并且传送数据的时候,它会复制数据而不用额外的操作
  • KVO 通知(willChangeValueForKey, didChangeValueForKey) 会被自动执行
  • 更容易debug:你可以设置一个断点在属性声明上并且断点会在每次 getter / setter 方法调用的时候执行,或者你可以在自己的自定义 setter/getter 设置断点。
  • 允许在一个单独的地方为设置值添加额外的逻辑。

getter特点:

  • 它是对未来的变化有扩展能力的(比如,属性是自动生成的)
  • 它允许子类化
  • 更简单的debug(比如,允许拿出一个断点在 getter 方法里面,并且看谁访问了特别的 getter
  • 它让意图更加清晰和明确:通过访问 ivar _anIvar 你可以明确的访问 self->_anIvar.这可能导致问题
    。在 block 里面访问 ivar (你捕捉并且 retain 了 self 即使你没有明确的看到 self 关键词)
  • 它自动产生KVO 通知
  • 在消息发送的时候增加的开销是微不足道的。

注意:永远不能在init以及其他初始化函数里面和dealloc方法中用getter和setter方法,应该直接访问实例变量。
一个子类可以重载getter和setter并且尝试调用其他方法,访问属性他们可能没有完全初始化。

点符号:属性有关时尽量使用点符号,区分属性的访问还是方法调用

推荐

view.backgroundColor = [UIColor orangeColor];

[UIApplication sharedApplication].delegate;

不推荐

[view setBackgroundColor:[UIColor orangeColor]];

UIApplication.sharedApplication.delegate;

属性定义:属性参数按照原子性读写内存管理顺序排列,例如@property (nonatomic, readwrite, copy) NSString *name;

除非特别情况,原子性请使用nonatomic,iOS 中atomic带来的锁特别影响性能。

NSString,NSArray,NSDictionary等可变对象和block请使用copy关键字。block最早在栈中创建,通过Copy拷贝到堆里。可变对象参考深拷贝与浅拷贝

对于getter公有,setter私有的公开属性,需要在.h设置为readonly,并在类扩展中重新定义通用的属性为readwrite

@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object
@end
  
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object
@end

私有属性:应该在实现文件的类扩展(class extensions,没有名字的categories)中声明。

方法

参数断言:最好使用NSParameterAssert()来断言条件是否成立或是抛出一个异常。

私有方法:永远不要在私有方法前加上_前缀,此前缀被Apple保留。

相等性:当我们要自己实现相等性的时候,需要实现isEqualhash方法。

如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。自行百度。

分类(Categories),在方法名加上前缀和下划线

当一个类中逻辑方法很多时,可以将代码按照逻辑划分到不同的分类当中。将私有方法放到private分类中是一个好的实践。如果不想暴露实现细节则在class_continuation分类(没有名字的分类)中声明。

注意:

  • 分类中的方法应该加上自己小写的前缀和下划线。比如- (void)abc_myCategoryMethod;,这样可以避免与其他的Category中的方法重名。
  • 不要在分类中重载系统方法,可能会导致莫名其妙的问题。
  • 最好把所有属性都定义在主接口里。尽量不要在分类中声明属性,虽然可以在分类中添加属性,但是这样会使调试变得困难,推荐使用存取方法。

协议(Protocol),实现抽象接口,利于代码复用

在 Objective-C 的世界里面经常错过的一个东西是抽象接口。接口(interface)这个词通常指一个类的 .h 文件,但是它在 Java 程序员眼里有另外的含义:
一系列不依赖具体实现的方法的定义。

当实现一个 protocol 你总应该坚持里氏替换原则
这个原则是:你应该可以取代任意接口(也就是Objective-C里的"protocol")实现,而不用改变客户端或者相关实现。

可以参考代码

通过协议提供匿名对象

  • 协议可以提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。
  • 使用匿名对象来隐藏类型名称(或类名)
  • 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可以使用匿名对象来表示

通知(Notification)

当你定义你自己的 NSNotification 的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。
你应该在公开的接口文件中将其声明为 extern 的, 并且在对应的实现文件里面定义。

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

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

对象的通讯

Block

当block作为异步接口的时候,尽量使用一个单独的block作为接口的最后一个参数。把需要提供的数据和错误信息整合到一个单独的block中,比分别提供要好

  • 通常这成功处理和失败处理会共享一些代码(比如让一个进度条或者提示消失);
  • Apple 也是这样做的,与平台一致能够带来一些潜在的好处;
  • block 通常会有多行代码,如果不作为最后一个参数放在后面的话,会打破调用点;
  • 使用多个 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的一点知识

  • block 是在栈上创建的
  • block 可以复制到堆上
  • block会捕获栈上的变量(或指针),将其复制为自己私有的const(变量)。
  • (如果在Block中修改Block块外的)栈上的变量和指针,那么这些变量和指针必须用__block关键字申明(译者注:否则就会跟上面的情况一样只是捕获他们的瞬时值)。

如果 block 没有在其他地方被保持,那么它会随着栈生存并且当栈帧(stack frame)返回的时候消失。
仅存在于栈上时,block对对象访问的内存管理和生命周期没有任何影响。

如果 block 需要在栈帧返回的时候存在,它们需要明确地被复制到堆上,这样,block 会像其他 Cocoa 对象一样增加引用计数。
当它们被复制的时候,它会带着它们的捕获作用域一起,retain 他们所有引用的对象。

如果一个 block引用了一个栈变量或指针,那么这个block初始化的时候会拥有这个变量或指针的const副本
所以(被捕获之后再在栈中改变这个变量或指针的值)是不起作用的。

当一个 block 被复制后,__block 声明的栈变量的引用被复制到了堆里,复制完成之后,
无论是栈上的block还是刚刚产生在堆上的block(栈上block的副本)都会引用该变量在堆上的副本。

 ...
 CGFloat blockInt = 10;
 void (^playblock)(void) = ^{
     NSLog(@"blockInt = %zd", blockInt);
 };
 blockInt ++;
 playblock();
 ...

 //结果为:blockInt = 10

self的循环引用

当使用代码块和异步分发的时候,要注意避免引用循环。 总是使用 weak 来引用对象,避免引用循环。
此外,把持有 block 的属性设置为 nil (比如 self.completionBlock = nil) 是一个好的实践。它会打破 block 捕获的作用域带来的引用循环。

// 多语句是需要在block里强引用一次
__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];

委托和数据源

委托模式 是 Apple 的框架里面使用广泛的模式,同时它是四人帮的书“设计模式”中的重要模式之一。
委托代理模式是单向的,消息的发送方(委托方)需要知道接收方(代理方)是谁,反过来就没有必要了。
对象之间耦合较松,发送方仅需知道它的代理方是否遵守相关 protocol 即可。

本质上,委托代理模式仅需要代理方提供一些回调方法,即代理方需要实现一系列空返回值的方法。

然而Apple并没有遵守改原则,比如TableView的Delegate和DataSource

为了分离概念,我们应该这样做:

委托模式(delegate pattern):事件发生的时候,委托者需要通知代理者。
数据源模式(datasource pattern): 委托者需要从数据源对象拉取数据。

@class ZOCSignUpViewController;

@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end


@interface ZOCSignUpViewController : UIViewController

@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;

@end

代理方法必须以调用者(即委托者)作为第一个参数,就像上面的例子一样。否则代理者无法区分不同的委托者实例。

继承

有时候你可能需要重载代理方法。考虑有两个 UIViewController 子类的情况:UIViewControllerA 和 UIViewControllerB,有下面的类继承关系。

UIViewControllerA 遵从 UITableViewDelegate 并且实现了 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath.

你可能会想要在 UIViewControllerB 中提供一个不同的实现,这个实现可能是这样子的:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

但如果超类没有实现该方法时,此时调用[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]方法,将使用 NSObject 的实现。
根据消息机制,会出现crash。

这时可以换一种实现方式:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
   CGFloat retVal = 0;
   if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
       retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
   }
   return retVal + 10.0f;
}

多重委托

多重委托是一个非常基础的概念,但是,大多数开发者对此非常不熟悉而使用 NSNotifications。多重委托实现方式有多重,可参考LBDelegateMatrioska,可自行百度。

使用NSCache来构建缓存,而非NSDictionary

NSCache提供优雅的自动删减功能,而且线程安全,不会像Dictionary一样拷贝Key.NSCache可与NSPurgeableData搭配使用,实现数据的自动清除功能。

Aspect Oriented Programming (AOP,面向切面编程)

Aspect Oriented Programming (AOP,面向切面编程) 在 Objective-C 社区内没有那么有名,但是 AOP 在运行时可以有巨大威力。 但是因为没有事实上的标准,Apple 也没有开箱即用的提供,也显得不重要,开发者都不怎么考虑它。

通常 AOP 被用来实现横向切面。统计与日志就是一个完美的例子。

我们使用运行时的特性来增加切面:

  • 在类的特定方法调用前运行特定的代码
  • 在类的特定方法调用后运行特定的代码
  • 增加代码来替代原来的类的方法的实现

可参考Aspects


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容