It usually doesn’t take beginning macOS/iOS developers long to discover
NotificationCenter
and see it as the solution to every single problem of
passing data around to different controllers. And NotificationCenter
is
great, but it has some downsides. Notably, it is very easy to introduce retain
cycles (and memory leaks) unless you are very careful to track and free the
listener when the object is released. This has bitten me on several occasions.
In general, excessive use of NotificationCenter
ends up creating a difficult
to maintain app where it is not entirely clear what is responding to what and
where.
But the thing is, a lot times you don’t need NotificationCenter
. You really
only need it when you need to pass data to views or controllers that are not in
the same hierarchy as your current one.
That’s right. Just like my previous posts about using the responder chain, most
controllers and views in Cocoa and CocoaTouch already exist in hierarchies. And
those hierarchies can be traversed and data passed to children directly without
NotificationCenter
.
Calling Descendents Directly
Let’s say you have a NSViewController
subclass, and you want to notify all
descendents that adhere to a certain protocol that something changed.
Start by defining a Protocol. Swift is big on Protocols.
protocol DoesSomething {
public func doSomething()
}
Now, we define a second protocol, and use protocol extensions to basically create a mixin that we can use anywhere.
import Foundation
import Cocoa
protocol FindsChildren {
func findChild<T>(type: T.Type) -> T?;
func findChild<T>(type: T.Type, parent: NSViewController) -> T?;
func findChild<T>(type: T.Type, parent: NSView) -> T?;
func findAllChidrenOf<T>(type: T.Type, parent: NSViewController) -> [T];
func findAllChidrenOf<T>(type: T.Type, parent: NSView) -> [T];
}
extension FindsChildren {
func findChild<T>(type: T.Type) -> T? {
if self is NSView {
return findChild(type: type, parent: self as! NSView)
} else if self is NSViewController {
return findChild(type: type, parent: self as! NSViewController)
}
return nil
}
func findChild<T>(type: T.Type, parent: NSViewController) -> T? {
for child in parent.children {
if child is T {
return child as? T
} else if let c = findChild(type: type, parent: child) {
return c
}
}
return nil
}
func findChild<T>(type: T.Type, parent: NSView) -> T? {
for child in parent.subviews {
if child is T {
return child as? T
} else if let c = findChild(type: type, parent: child) {
return c
}
}
return nil
}
func findAllChidrenOf<T>(type: T.Type, parent: NSViewController) -> [T] {
var r: [T] = []
for child in parent.children {
if child is T {
r.append(child as! T)
} else {
let c = findAllChidrenOf(type: type, parent: child)
if c.count > 0 {
r.append(contentsOf: c)
}
}
}
return r
}
func findAllChidrenOf<T>(type: T.Type, parent: NSView) -> [T] {
var r: [T] = []
for child in parent.subviews {
if child is T {
r.append(child as! T)
} else {
let c = findAllChidrenOf(type: type, parent: child)
if c.count > 0 {
r.append(contentsOf: c)
}
}
}
return r
}
}
The above code is for macOS, but converting it to iOS is as easy as changing all
references to NSView
to UIView
.
So now, our controller can inherit FindsChildren
, giving us access to the find
methods we created above. At this point, we simply call findAllChildrenOf()
with the protocol we created above, and call the method on them. Because we’re
using generic types in Swift, the find methods return the type that was passed
in as an argument.
class ViewController: NSViewController, FindsChildren {
override public func viewDidLoad() {
let children = findAllChildrenOf(type: DoesSomething, parent: self)
for child in children {
child.doSomething()
}
}
}
Because the methods in the FindsChildren
are recursive, they will find all
children that descend from self
, no matter how deep in the hierarchy they may
be. And, like always, you can either use protocols to find any child that
conforms to the protocol, or you can specify a specific class to be called.
Calling Parents Directly
Calling parents directly is even more straightforward, because parents are automatically in the responder chain. So the method in my previous post about using the responder chain will work here as well.