Swift中的通知(NSNotification)

在Swift中,可以通过以下几种方式在应用程序中的实体之间进行通信:通知(NSNotification),键值观察(KVO),委托等。在系列文章中,我将分享如何为工作选择合适的工具以及一些最佳做法。 这是关于通知的。

通常将通知与代表进行比较。 您可以在我的文章 Swift中的Delegate中 找到更多信息

Notification对象是包含与更改或事件有关的信息的容器。 通知使用NotificationCenter的集中实例进行广播。

name标识的Notification对象可以包含对发送通知的object引用,以及userInfo词典中的一些任意信息。

NotificationCenter注册观察者并传递通知。 对象使用NotificationCenter实例发布观察通知。 每个应用程序(进程)都有一个默认的通知中心,可通过NotificationCenter.default属性访问。

请注意,在macOS上, DistributedNotificationCenter 可用于在进程之间进行通信。

出于演示目的,我创建了URLContainer示例类,该示例类存储URL对象数组。

   URLContainer {      私人(设置)的var网址:[ URL ] = [] func add(_ url: URL ){ 
urls.append(url)
}
}

创建通知

要创建新通知,我们需要一个名称。 名称使用嵌套的NSNotification.Name类型。

引擎盖下的名称实际上是字符串,因此,最好使用唯一的名称。 最佳做法是为名称声明一个常量。 我更喜欢使用常量名称作为字符串,这与Cocoa / Cocoa Touch中使用的便利相同。

  // URLContainerextension Notification.Name {static let URLContainerDidAddURL 
= NSNotification.Name (“ URLContainerDidAddURL ”)}

URLContainerDidAddULR是发生更改时发布的通知的名称。 使用Notification.Name扩展名Notification.Name 。 采用这种方法的好处是,编译器在尝试创建重复名称时会警告您。

命名通知时,请包含对象名称和有意义的更改描述。 使用辅助动词WillDid来指示在更改之前还是之后发布通知。 如果不确定,请查看现有名称,只需键入Notification.Name并浏览自动完成结果。

注意Swift 4和Swift 4.2的区别

Swift 4中的Cocoa Touch使用全局NSNotification.Name命名空间来声明通知名称常量:

  // UIApplicationextension NSNotification.Name {public static let UIApplicationDidChangeStatusBarOrientationNSNotification.Name 
}

在Swift 4.2中,常量嵌套在类型中:

 扩展UIApplication {公共类let didChangeStatusBarOrientationNotificationNSNotification.Name 
}

字符串值保持不变– "UIApplicationDidChangeStatusBarOrientationNotification" 。 但是,我们引用它的方式有所变化:

  // Swift 4 
Notification.Name.UIApplicationDidChangeStatusBarOrientation //完整
.UIApplicationDidChangeStatusBarOrientation //简短// Swift 4.2
UIApplication.didChangeStatusBarOrientationNotification

在Swift 4.2中,您必须使用Type.constantName语法。 这使它更加结构化,但是我们放宽了简短的语法,并且无法对Notification.Name类型的所有通知进行内部检查。

发布通知

发布通知的最便捷方法是使用NotificationCenter便捷方法。

 func post( name aName: NSNotification.Name, 
object anObject: Any?,
userInfo aUserInfo: [AnyHashable : Any]? = nil)

您很少需要自己创建Notification对象。 而是向通知中心提供名称,发布对象和用户信息字典。

  class URLContainer {private(set)var urls:[URL] = [] func add(_ url:URL){ 
urls.append(url)
NotificationCenter.default.post(
名称: 。 URLContainerDidAddURL
对象: 自我
userInfo:[ URLContainer.urlKey :url])
静态让urlKey =“ URL
}

URLContainer通知何时添加新的URL。 userInfo词典在"URL"键下包含新的URL。 优良作法是为userInfo字典键声明常量。

通常, object参数是发布通知的对象。 在特殊情况下,它可能nil 。 例如系统通知,例如时钟或时区更改。 最佳做法是确保发布通知的对象始终是传递self 。 这样,通知将仅通过对象方法之一发布,而不是外部代码。

使用选择器观察通知

观察通知的一种方法是将观察者及其选择器添加到NotificationCenter

 func addObserver(_ observer : Any, 
selector aSelector: Selector,
name aName: NSNotification.Name?,
object anObject: Any?)

NotificationCenter需要NotificationCenter的名称以及观察者希望接收其通知的对象。

这种方法类似于您在UI控件中看到的目标操作模式。

 类控制器{func subscription(用于容器:URLContainer){ 
NotificationCenter.default.addObserver( self
选择器: #selector(urlContainerDidChange(_ :))
名称: 。 URLContainerDidAddURL
对象: 容器
} @objc
func urlContainerDidAddURL(_通知:通知){
让url = notification.userInfo?[URLContainer.urlKey]
打印(URL)
}}

在此示例中, Controller对象使用urlContainerDidAddURL(_:)方法响应URLContainerDidAddURL通知。

观察者方法必须有一个参数( Notification的实例)。 没有参数的方法仍然有效。 但这是一个副作用,文档明确要求遵循签名。

即使您对通知对象本身不感兴趣,但对事件的事实不感兴趣, Notification实例也可以作为参数来帮助分离和搜索代码中的通知处理方法。 因此,最好在通知处理方法中保留Notification参数。

另一个有用的做法是使用通知名称作为方法名称: URLContainerDidAddURLurlContainerDidAddURL(_:) 。 这个小技巧将帮助您快速搜索响应通知的方法。

当我们不再对观察通知感兴趣时,可以从NotificationCenter删除观察者。

 func removeObserver(_ observer : Any, 
name aName:
func removeObserver(_ observer : Any,
name aName:
NSNotification.Name ?,
object anObject: Any?)
?,
object anObject: Any?)

为此,您需要观察者对象,通知的名称以及观察者接收其通知的对象。

Controller {func取消订阅(来自容器:URLContainer){ 
NotificationCenter.default.removeObserver( self
名称: 。 URLContainerDidAddURL
对象: 容器
}}

在这里, Observer对象停止响应URLContainerDidAddURL通知。

请注意,在iOS 9和macOS 10.11之前,我们必须通过在释放对象之前调用此方法来删除观察者。

使用闭包观察通知

NotificationCenter提供了关闭API来观察通知。

 func addObserver(forName name : NSNotification.Name?, 
object obj: Any?,
queue : OperationQueue?,
using block: @escaping (Notification) -> Void) -> NSObjectProtocol

与以前的方法一样, NotificationCenter期望NotificationCenter的名称以及观察者希望接收其通知的对象。 但是这一次,闭包对通知做出反应,并且从该方法返回了充当观察者的对象。

 类控制器{私有var观察者:AnyObject?  func subscription(用于容器:URLContainer){ 
守卫观察者==无其他{返回}
观察者= NotificationCenter.default.addObserver(
forName :。 URLContainerDidAddURL
对象: 容器
队列:无) { let url中的通知= notification.userInfo?[URLContainer.urlKey]
打印(URL) }
} func unsubscribe(){
如果让观察者=观察者{
NotificationCenter.default.removeObserver(观察者)
self.observer =无
}
} deinit {
退订()
}}

在此示例中,使用闭包完成对URLContainerDidAddURL响应,并且Controller对象存储观察者实例。

在释放removeObserver(_:name:object:)之前, 必须通过调用removeObserver(_:)removeObserver(_:name:object:) 删除观察者。

通知中心将保留关闭,直到观察者注册被删除。 当然,关闭是在逃避。 这可能会产生强大的参考周期。

 观察者= NotificationCenter.default.addObserver( 
forName :。 URLContainerDidAddURL
对象: 容器
队列:无) {[弱自我]通知 守卫let`self` = self else {
返回
} self.processNotification(notification) }
}

在我的示例中,观察者已被deinit方法删除,但具有强大的引用周期,它将永远不会发生。 我使用[weak self]打破强参考周期。

在闭包中具有弱引用的一般经验法则是在闭包例程之前捕获强引用。 这很重要,因为弱引用可能随时变为nil 。 这可能导致不确定的行为和意外的副作用。 为了方便起见,我使用下一个模式:

 守卫let`self` = self else { 
返回
}

多个订阅将多次触发响应的选择器。 但是,当取消订阅时,所有观察者选择器都会被删除。

  NotificationCenter.default.addObserver(self, 
选择器: #selector(urlContainerDidChange(_ :))
名称: URLContainerDidAddURL,
object:container)NotificationCenter.default.addObserver(self,
选择器: #selector(urlContainerDidChange(_ :))
名称: URLContainerDidAddURL,
object:container)container.add(URL(string:“ https://developer.apple.com ”)!)NotificationCenter.default.removeObserver(自己,
名称:.URLContainerDidAddURL,对象:container)container.add(URL(string:“ https://itunesconnect.apple.com ”)!)
输出:
https://developer.apple.com
https://developer.apple.com

在此示例中,第一个URL将两次触发urlContainerDidChange(_:) ,而第二个URL将不会触发。

可能不是我们所期望的。 通常,我们需要先取消订阅先前的对象,然后再订阅新的对象。 当对象存储为属性时,这特别简单。

  class Controller {var container:URLContainer?  { 
willSet {
如果让容器=容器{
退订 (来自: container
}
} didSet {
如果让容器=容器{
订阅 (用于: container
}
}
}

willSet/didSet (属性观察者)与帮助程序的willSet/didSetsubscribe(for:)/unsubscribe(from:)方法一起使用。

因此,另一个好的做法是创建用于订阅和取消订阅通知的单独方法。

使用闭合时,我们需要格外小心。 因为如果我们覆盖以前的观察者,则永远无法删除它。 但是实际上控制起来可能更简单。

 专用var观察者:AnyObject?func subscription(用于容器:URLContainer){ 
后卫 观察者==其他,{
assertionFailure (“ 添加新的观察者之前,必须先删除该观察者 ”)
返回
}观察者= NotificationCenter.default.addObserver(
forName:.URLContainerDidAddURL,
对象:容器,
队列:无){在let url中的通知= notification.userInfo?[URLContainer.urlKey]
打印(URL)}
}

在这里,我们验证在添加新观察者之前是否删除了先前的观察者。 断言将帮助我们在测试期间捕获这些错误。

NotificationCenter有趣的功能之一是能够订阅来自某个对象的所有通知。 对于所有对象的特定通知; 或基本上是通知中心中的所有通知。

  //来自容器的任何通知 
让observer1 = NotificationCenter.default.addObserver(
forName:无
对象:容器,
队列:无){
} //来自所有对象的URLContainerDidChange通知
让observer2 = NotificationCenter.default.addObserver(
forName:.URLContainerDidChange,
对象:nil
队列:无){
} //来自localNotificationCenter中所有对象的所有通知
让observer3 = localNotificationCenter.addObserver(
forName:无
对象:nil
队列:无){
}

您需要做的只是将nil作为参数之一。 同样适用于基于选择器的订阅。 当您没有对发布通知的对象的引用,所有通知都可以通过一种关闭/方法来处理或在应用程序的某些部分使用本地通知中心时,此功能将非常有用。

取消订阅通知同样适用。

  //从所有对象退订URLContainerDidChange的观察者 
NotificationCenter.default.removeObserver(观察者,
名称:.URLContainerDidChange,对象:nil)//从容器对象退订观察者
NotificationCenter.default.removeObserver(观察者,
名称:无,对象:容器)//从通知中心删除观察者
NotificationCenter.default.removeObserver(观察者)

明智地使用此方法。 通常,最好明确枚举您对观察和特定对象感兴趣的所有通知。 否则,您可能会遭受不确定的行为和副作用。

删除观察者时,请确保仅删除您明确订阅的通知。 特别是如果您使用继承,并且子类/超类可以订阅通知。

当观察到可能在辅助线程上发生的通知时,在我们期望的线程上进行响应很重要。 一个示例是在响应可能在后台发生的模型更改时更新UI。

通知中心在发布通知的线程上传递通知。 重定向通知是可能的,并在文档中进行了描述。 但是实现相当复杂并且在几个方面受到限制。

使用闭包观察通知时,我们可以为闭包指定OperationQueue

 func addObserver(forName name: NSNotification.Name?, 
object obj: Any?,
queue : OperationQueue ?,
using block: @escaping (Notification) -> Void) -> NSObjectProtocol

DispatchQueue与选择器一起使用是解决复杂问题的另一个简单解决方案。

 类控制器{@objc 
func urlContainerDidAddURL(_通知:通知){
DispatchQueue.main.async {
//更新用户界面
}
}}

通知传递

当使用NotificationCenterpost(name:object:userInfo:) ,通知将同步分发。 通知中心使用队列( NotificationQueue ),该队列以先进先出(FIFO)顺序维护通知。

  container.add(URL(string:“ https://apple.com ”)!) 
container.add(URL(string:“ https://developer.apple.com ”)!)
container.add(URL(string:“ https://itunesconnect.apple.com ”)!) 输出:
https://apple.com
https://developer.apple.com
https://itunesconnect.apple.com

在此示例中,通知同步且按顺序出现。

这也意味着,在发布对象可以恢复其线程之前,它必须等待通知中心将通知分发给所有观察者。

从该堆栈跟踪中可以看到, urlContainerDidAddURL(_:)是同步执行的。

但是…放置在队列中的通知可以延迟,直到当前结束通过运行循环或运行循环空闲为止。 可以使用发布样式( NotificationQueue.PostingStyle )更改通知传递,并且可以异步执行。

 公共枚举PostingStyle:UInt { 
///通知已发布在当前通知标注或计时器的末尾。
空闲时
///当运行循环空闲时,将发布通知。
尽快案例
///合并后立即发布通知。
现在的情况
}

观察模式用于各种体系结构中。 通知通常用作模型-视图-控制器(MVC)体系结构中的通信协议。

通知和UIViewController

 类ViewController:UIViewController { 

让容器= URLContainer()
私人var观察者:AnyObject? 覆盖func viewDidLoad(){
super.viewDidLoad()
观察者= NotificationCenter.default.addObserver(
forName:.URLContainerDidAddURL,
对象:容器,
队列: OperationQueue.main
{ [弱自我]通知
守卫let`self` = self else {
返回
} self.updateUI()
}
} deinit {
如果让观察者=观察者{
NotificationCenter.default.removeObserver(观察者)
}
}
}

在此示例中, ViewController UI更新为对URLContainerDidAddURL通知的响应。 由于必须在主队列上更新UI,因此在添加观察者时使用OperationQueue.main 。 对self指称是弱的,在封闭内部转化为强。

还有一些事情要考虑。 处理通知可能是昂贵的操作。 更新用户界面,格式化值等。也许您需要从服务器下载图像。 为了获得最佳性能,最好避免不必要的工作。

一种选择是订阅外观,并在视图消失时取消订阅。 这将删除不必要的更新。 但是,您仍然需要在外观上更新UI。 我更喜欢跟踪所需的更新:

 私人var needsUpdateViewOnAppearance = true 
私人var isVisible :布尔{
返回isViewLoaded && view.window!=无
}覆盖f​​unc viewDidLoad(){
super.viewDidLoad()
观察者= NotificationCenter.default.addObserver(
forName:.URLContainerDidAddURL,
对象:容器,
队列:OperationQueue.main)
{[弱自我]通知
守卫let`self` = self else {
返回
}如果self.isVisible {
self.updateUI ()
}
其他{
self.needsUpdateViewOnAppearance = true
}
}
}覆写func viewWillAppear(_动画:Bool){
super.viewWillAppear(动画)

如果需要UpdateViewOnAppearance {
updateUI ()
needsUpdateViewOnAppearance =
}
}

在此示例中,需要needsUpdateViewOnAppearance属性指示何时必须在外观上更新UI(在viewWillAppear(_:) )。 发生URLContainerDidAddURL通知时,我们检查视图是否可见(通过检查视图是否在窗口中)。 如果视图不可见,则设置needsUpdateViewOnAppearance属性。

要做更多的工作,但是性能提升可能是巨大的。

最后说明

通知易于使用且功能强大。 它在模型-视图-控制器体系结构中运行良好,可用于响应系统事件。

拥有权利的同时也被赋予了重大的责任。

通知可能看起来像是解决复杂问题的简单解决方案。 但是过度使用通知可能会导致应用程序中对象之间的复杂依赖关系,有时甚至会导致单独的模块。

在考虑添加观察者时,请检查是否对发布通知的对象有引用。 如果该对象不可访问,则与其观察每个通知,不如通过调整体系结构来解决问题。

在构造数据流时,请考虑依赖项注入和委派作为通知的替代方法。

在许多情况下,通知似乎是解决问题的唯一方法,这是更复杂的体系结构问题的征兆。 通知必须是选择的最佳解决方案。


您可以在Apple开发人员网站的“通知编程主题”下找到文档通知。


感谢您的阅读。 如果您喜欢这篇文章,请关注我以后的文章。

相关主题:在Swift中进行代理。

向作者显示更多❤️与👏’s

希望下次见you