委托、通知传值的用法与区别

低耦合性是良好程序的特性。低耦合性程序可读性和可维护性比较好。Cocoa中的委托、通知功能可以使低耦合性更易实现,下面结合demo说明如何使用委托、通知进行传值,及委托与通知的区别。

1. 委托传值

委托传值在反向传值中使用。使用委托可以让委托和委托对象之间的关系变得清晰,特别是在委托的方法必须实现时。

委托传值步骤如下:

1.1 在ChildViewController.h声明协议,协议内方法默认必须实现。如果想选择实现,在方法前用@optional标志出来。

#import <UIKit/UIKit.h>

@protocol ChildVCDelegate <NSObject>

- (void)didReceiveText:(NSString *)string;

@optional
- (void)receiveTextFailedWithError:(NSError *)error;

@end

1.2 在ChildViewController.h接口部分创建一个ChildVCDelegate类型的实例变量。此时的特性应该使用weak,否则会造成循环引用。

#import <UIKit/UIKit.h>

@protocol ChildVCDelegate <NSObject>

- (void)didReceiveText:(NSString *)string;

@optional
- (void)receiveTextFailedWithError:(NSError *)error;

@end

@interface ChildViewController : UIViewController

@property (weak, nonatomic) id<ChildVCDelegate> delegate;

@end

1.3 在RootViewController.m中,使你的类遵守ChildViewController.h里声明的ChildVCDelegate协议。

#import "ViewController.h"
#import "ChildViewController.h"

@interface ViewController () <ChildVCDelegate>

@end

1.4 在RootViewController.m实现协议方法,将ChildViewController的代理委托给当前控制器。

@implementation ViewController

// 1
ChildViewController *childVC = [[ChildViewController alloc] init];
childVC.delegate = self;

- (void)didReceiveText:(NSString *)string
{
    
}

注释1后的代码需要添加到跳转到ChildViewController的方法内。如果使用纯代码编程,添加到presentViewController: animated: completion:showViewController: animated:方法前;如果使用storyboardsegue跳转,添加到 prepareForSegue: sender:方法内,此时初始化视图控制器应该使用SecViewController *secVC =segue.destinationViewController;

1.5 在ChildViewController.m实现部分,调用代理方法。为防止运行时出现问题,调用方法前要先判断代理是否实现了调用的方法。

// 在某方法内
if ([self.delegate respondsToSelector:@selector(didReceiveText:)])
    {
        [self.delegate didReceiveText:@"pro648"];
    }

2. 通知传值

NSNotificationCenter对象(简称通知中心)提供了广播信息的机制,NSNotificationCenter对象实质上是一个通知分发表。对象使用addObserver: selector: name: object:addObserverForName: object: queue: usingBlock:方法向通知中心注册以接收通知,每次调用上面的方法都会指定一组通知。因此,对象可以通过多次调用这些方法注册为不同通知的观察者。

每一个运行的Cocoa程序都有一个默认通知中心,一般不需要自己创建。NSNotificationCenter对象只能在单个进程中传递通知。如果需要向其他进程发送通知,或从其他进程接收通知,请使用NSDistributedNotificationCenter

2.1 添加观察者

要想接收通知,先要在通知中心注册观察者,注册时声明想要观察通知的名称。如果你是在为iPhone应用程序的视图控制器添加观察者,最好写在viewDidLoad方法中,这样可以确保视图控制器加载完成时只创建唯一一个观察者用以接收通知。添加观察者方法如下:

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(didReceiveText:)
                                                 name:@"DidReceiveNotification"
                                               object:nil];

观察者对象self是当前视图控制器,selector指明当视图控制器接收到通知时调用的方法,这个方法必须为无返回类型、带有一个参数。如下所示:

- (void)didReceiveText:(NSNotification *)notification

如果需要获取与通知一起发送的用户信息,可以从NSNotification对象中提取,如下:

- (void)didReceiveText:(NSNotification *)notification
{
    NSDictionary *userInfo = [notification userInfo];
    NSString *receivedText = [userInfo objectForKey:@"YOUR_KEY"];
    ...
}

2.2 发送通知

发送通知的方法很简单,如下所示:

    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:@"YOUR_OBJECT" forKey:@"YOUR_KEY"];
    
    [[NSNotificationCenter defaultCenter] postNotificationName:@"DidReceiveNotification"
                                                        object:self
                                                      userInfo:userInfo];

通知名称一般为字符串常量,object可以是任何想要和通知一起发送的对象,但一般为selfnil,如果需要发送额外信息,可以使用可选的userInfo。如果不需要发送额外信息,可以直接把userInfo设置为nil,或使用postNotificationName: object:方法。

2.3 移除观察者

从OS X 10.11和iOS 9.0开始,NSNotificationCenter将不再向已被释放掉的观察者发送通知,通知中心对观察者是零引用( zeroing weak reference)。因此,下一次通知中心想要向观察者发送通知时,会检测到观察者已不存在并为我们移除观察者,也就是不再需要手动移除观察者。需要注意的是,如果使用addObserverForName: object: queue: usingBlock:方法添加的观察者,或需要支持iOS 8 或更低版本,依旧需要移除观察者,移除方法如下:

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:@"DidReceiveNotification"
                                                  object:nil];
}

如果要移除所有观察者,可以直接使用removeObserver:方法。

3. 创建demo

这个demo整体思路是:有三个视图控制器,第一个视图控制器上有两个UILabel,一个名称为下一页UIButton,点击下一页按钮进入第二个视图控制器;第二个视图控制器上有一个UILabel,一个UITextField,两个UIButton,在UITextField输入文本后,点击上一页UITextField的内容使用委托传值到第一个视图控制器并在UILabel显示,点击下一页进入第三个视图控制器;第三个视图控制器有一个UITextField和一个上一页按钮,在UITextField输入文本后点击上一页按钮,使用通知传值到前两个视图控制器,并显示到UILabel中。

如下面gif所示:

Delegation&Notification.gif

这里提供一个demo模版,在这个模板上添加代码进行传值练习。

模板名称:Delegation&Notification模板
下载地址:https://github.com/pro648/BasicDemos-iOS

4. 使用委托传值

4.1 在SecondViewController.h接口前面声明协议,用来传值。

#import <UIKit/UIKit.h>

@protocol SendTextDelegate <NSObject>

- (void)sendText:(NSString *)string;

@end

@interface SecondViewController : UIViewController

@end

4.2 在SecondViewController.h中定义一个代理属性。

@interface SecondViewController : UIViewController

@property (weak, nonatomic) id<SendTextDelegate> delegate;

@end

4.3 在SecondViewController.m实现文件中,调用代理方法。这里在点击上一页按钮回到首页时调用代理方法,把self.textField的内容传给代理。传值前可以先判断代理是否实现了协议的方法,防止运行时出现问题。更新后的代码如下:

- (IBAction)backToVC:(UIButton *)sender
{
    // 判断是否实现了协议方法
    if ([self.delegate respondsToSelector:@selector(sendText:)])
    {
        // 代理实现了协议方法,传送TextField内文本给代理
        [self.delegate sendText:self.textField.text];
    }else
    {
        NSLog(@"代理没有实现协议方法,%d, %s",__LINE__, __PRETTY_FUNCTION__);
    }
    
    // 返回ViewController
    [self.navigationController popViewControllerAnimated:YES];
}

4.4 进入ViewController.m文件,声明遵守SendTextDelegate协议。在跳转到SecondViewController的方法中设置SecondViewController的代理为当前控制器。

@interface ViewController () <SendTextDelegate>

- (void)goToSecondVC:(UIButton *)sender
{
    // 跳转到SecondViewController
    SecondViewController *secVC = [[SecondViewController alloc] init];
    
    // 设置secVC的代理为当前控制器
    secVC.delegate = self;
    
    [self.navigationController pushViewController:secVC animated:YES];
}

4.5 在ViewController.m实现代理方法,并把传来的值显示到self.deleLabel中。

- (void)sendText:(NSString *)string
{
    self.deleLabel.text = string;
}

5. 使用通知传值

5.1 添加观察者

ViewController.mSecondViewController.mviewDidLoad方法中添加观察者,name使用全局变量,接收到通知后,执行被调用的方法,把通知附带的字符串显示在notiLabel上。更新后的代码如下:

// ViewController.m
extern NSString *NotificationFromThirdVC;

@implementation ViewController

- (void)viewDidLoad {
    ...
    // 添加观察者
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(didReceiveNotificationMessage:)
                                                 name:NotificationFromThirdVC
                                               object:nil];
}

- (void)didReceiveNotificationMessage:(NSNotification *)notification
{
    if ([[notification name] isEqualToString:NotificationFromThirdVC])
    {
        // 把通知传送的字符串显示到notiLabel
        NSDictionary *dict = [notification userInfo];
        NSString *string = [dict objectForKey:@"TextField"];
        self.notiLabel.text = string;
    }
}

// SecondViewController.m中的代码与ViewController.m中的一样,你可以自己写。如果遇到问题,可以在文章尾部下载源码查看。

SecondViewController.m 中使用addObserverForName:object:queue:usingBlock:方法注册观察者。代码如下:

- (void)viewDidLoad
{
    ...
        // 添加观察者
    [[NSNotificationCenter defaultCenter] addObserverForName:NotificationFromThirdVC
                                                      object:nil
                                                       queue:nil
                                                  usingBlock:^(NSNotification * _Nonnull note) {
          if ([note.name isEqualToString:NotificationFromThirdVC]) {
              // 把通知传送的字符串显示到notiLabel。
              NSDictionary *userInfo = [note userInfo];
              self.notiLabel.text = [userInfo valueForKey:@"TextField"];
          }
                                                  }];
}

5.2 发送通知

首先在ThirdViewController.m实现部分前先声明通知名称为全局变量。

NSString *NotificationFromThirdVC = @"NotificationFromThirdVCTextField";

ThirdViewController.m实现部分,在点击回到上一页按钮时发送通知,把UITextField中的字符串做为额外信息发送,更新后代码如下:

- (IBAction)backToSecVC:(UIButton *)sender
{
    // 发送通知
    NSString *string = self.textField.text;
    NSDictionary *userInfo = [NSDictionary dictionaryWithObject:string forKey:@"TextField"];
    [[NSNotificationCenter defaultCenter] postNotificationName:NotificationFromThirdVC
                                                        object:nil
                                                      userInfo:userInfo];
    
    // 返回SecondViewController
    [self.navigationController popViewControllerAnimated:YES];
}

5.3 移除观察者

ViewController.m中添加观察者使用的是addObserver: selector: name: object:方法,模拟器是iOS 11,且不计划支持iOS 8或更低版本,ViewController.m中添加的观察者不需要移除。

SecondViewController.m中添加观察者使用的是addObserverForName:object:forQueue:usingBlock:方法,必须手动移除观察者。代码如下:

- (void)dealloc {
    // 移除观察者。
    [[NSNotificationCenter defaultCenter] removeObserver:self
                                                    name:NotificationFromThirdVC
                                                  object:nil];
}

使用通知中心传递信息时,一定要先实例化观察者,再发送通知。例如:TabBar上有VC1VC2两个视图控制器,运行后先进入VC1,如果直接在VC1上发送通知,VC2上将不能接收到通知,因为此时VC2还没有运行,VC2的观察者还没有在通知中心注册,所以需要进入VC1后再点击进入VC2,之后再返回VC1发送通知,此时VC2就可以接收到通知。

现在,运行demo,如下所示:

Delegation&Notification.gif

总结

初看,通知是一种没有缺点的方式来减少类之间的依赖,你甚至不需要向你的类添加一个委托实例变量。现在再来看一下通知的缺点,当你发送通知时,通知中心会同步向所有在通知中心注册的观察者发送信息,直到所有观察者调用他们的注册方法后,发起通知的代码才会再次获得控制。值得注意的是,当你向多个观察者发送通知并且发送通知的代码需要等待完成某些操作时,只有当所有观察者方法被调用并执行完毕时,发送通知的代码才会再次获得控制(观察者方法以一些未指定的顺序一个接一个地调用)。为解决这个问题,一种方法是在不同线程上有额外通知中心,同时使用异步通知,NSNotificationQueue允许调用立即返回。这样在大多数情况下会额外增加代码的复杂性。另一种简便方法是使用performSelector: withObject: afterDelay:延迟处理通知。

- (void)didReceiveNotificationMessage:(NSNotification *)notification
{
    if ([[notification name] isEqualToString:NotificationFromThirdVC])
    {
        // 把通知传送的字符串显示到notiLabel
        NSDictionary *dict = [notification userInfo];
        NSString *string = [dict objectForKey:@"TextField"];
        
        // 延迟处理
        [self performSelector:@selector(DO_YOUR_REAL_WORK) withObject:string afterDelay:0.3];
    }
}

这样可以使发布通知的代码更快获得控制。此时观察者方法在同一线程执行。

通知是将信息传播到你无法接触到的多个对象的一种方法,因此,它可以用在视图控制器间传递数据,但一般来说,不要这样做。当你发送一个通知,你不知道哪一个对象会对此做出反应,如果遇到错误将难以追踪,别人维护你的代码也会变的更加困难。

NSUserDefaults是用来永久保存用户偏好设置,以便app下次启动时使用。任何保存在此位置的数据如没有明确删除会永远保存在这里。所以最好不要使用NSUserDefaults传值。

始终使用代理将信息传回其他控制器,内容视图控制器应该永远不需要知道源视图控制器的类或不是它创建的视图控制器。另外,如果你想获取对象属性的变化,最好使用Key Value Observing

在任何情况下,都不应该让视图控制器发送通知或委托消息。在多数情况下,视图控制器应该更改模型,然后模型通知观察者或委托它已被更改。

文件名称:Delegation&Notification
源码地址:https://github.com/pro648/BasicDemos-iOS

参考资料:

  1. Delegation or Notification
  2. Unregistering NSNotificationCenter Observers in iOS 9
  3. How iOS View Controllers communicate with each other
  4. NSNotification & NSNotificationCenter

欢迎更多指正:https://github.com/pro648/tips/wiki

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

推荐阅读更多精彩内容