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
。 采用这种方法的好处是,编译器在尝试创建重复名称时会警告您。
命名通知时,请包含对象名称和有意义的更改描述。 使用辅助动词Will和Did来指示在更改之前还是之后发布通知。 如果不确定,请查看现有名称,只需键入Notification.Name
并浏览自动完成结果。
注意Swift 4和Swift 4.2的区别
Swift 4中的Cocoa Touch使用全局NSNotification.Name
命名空间来声明通知名称常量:
// UIApplicationextension NSNotification.Name {public static let UIApplicationDidChangeStatusBarOrientation : NSNotification.Name
}
在Swift 4.2中,常量嵌套在类型中:
扩展UIApplication {公共类let didChangeStatusBarOrientationNotification : NSNotification.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
参数。
另一个有用的做法是使用通知名称作为方法名称: URLContainerDidAddURL
– urlContainerDidAddURL(_:)
。 这个小技巧将帮助您快速搜索响应通知的方法。
当我们不再对观察通知感兴趣时,可以从NotificationCenter
删除观察者。
func removeObserver(_ observer : Any,
name aName:func removeObserver(_ observer : Any,
NSNotification.Name
name aName:?,
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/didSet
( subscribe(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 {
//更新用户界面
}
}}
通知传递
当使用NotificationCenter
的post(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!=无
}覆盖func 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