A typical Swift application is composed of dozens and dozens of objects, working together to make your application tick. To get the job done, these objects need the ability to talk to each other. In this tutorial, we take a look at three common patterns that enable objects to communicate with one another. We also discuss when to use which pattern and, more importantly, when to avoid a particular pattern.
Object References
On iOS, a view is typically owned and managed by a view controller. This is textbook Model-View-Controller. The view controller keeps a reference to the view it manages and talks to it directly.
Even though there's nothing wrong with this approach, it's important to understand that it introduces tight coupling. The implementation of the view controller is tightly coupled to that of the view. The opposite isn't true, though. The view is unaware of the view controller and, as a result, its implementation isn't coupled and doesn't depend on that of the view controller.
Keeping a reference to an object is the most direct way of communication. Because it introduces coupling between objects, it cannot, or should not, be used in every context.
Delegation
At first glance, delegation appears to be similar to the first approach. The key difference is that the objects are loosely coupled. The UITableViewDelegate
protocol is a fine example of delegation.
Like any other view, a table view is in charge of responding to user interaction. Even though a table view can detect user interaction, it doesn't know how to interpret the touch events. This seeming disadvantage is what makes table views reusable components.
A table view informs its delegate about the touch events it detects. It's the responsibility of the table view's delegate to handle table view interactions. A table view is loosely coupled to its delegate through the UITableViewDelegate
protocol. The delegate object is also loosely coupled to the table view. All it needs to do is conform to the UITableViewDelegate
protocol by implementing the methods defined by the protocol.
Any object conforming to the UITableViewDelegate
protocol can act as the table view's delegate. The table view doesn't care and doesn't need to know which object acts as the table view's delegate as long as it conforms to the UITableViewDelegate
protocol. As the name implies, the table view delegates tasks to the delegate object. Delegation is a very common design pattern in Cocoa applications and frameworks.
Notifications
Ideally, delegation is used to enable unidirectional communication between two objects. What if an object has a message that is of interest to two, three, or dozens of objects? For Cocoa applications, the recommended approach is posting a notification through a notification center.
The NotificationCenter
class, defined in the Foundation framework, provides an interface for broadcasting messages within a Cocoa application. To make this possible, two elements need to be in place:
- an object posting the notification
- one or more objects interested in the notification
An object needs to tell a notification center that it's interested in receiving notifications with a particular name. It can optionally specify which objects they would like to receive notifications from. What does that look like in code?
let defaultCenter = NotificationCenter.default
defaultCenter.addObserver(self, selector: #selector(synchronizationDidFinish(_:)), name: Notification.Name.SynchronizationDidFinish, object: nil)
The object that broadcasts the notification uses the same notification center to post a notification with a particular name. It can optionally include additional information in the form of a dictionary.
let defaultCenter = NotificationCenter.default
defaultCenter.post(name: Notification.Name.SynchronizationDidFinish, object: self, userInfo: [ "success" : true ])
If an object is no longer interested in receiving notifications with a particular name, it simply removes itself as an observer.
NotificationCenter.default.removeObserver(self)
What About Key-Value Observing?
Key-Value Observing, or KVO for short, is a solution that enables objects to observe properties of other objects. KVO is an important aspect of Cocoa programming. It's heavily used by bindings on macOS, for example. That said, it's important to understand that KVO is a passive form of communication between objects.
When To Use What
Object References
Object references make sense if both objects have a clear or logical relationship with one another. A view controller, for example, keeps a reference to the view it manages. It's important to understand that the owner takes responsibility for the lifetime of the object it owns. This isn't true for delegation.
Delegation
Developers new to Cocoa are often confused by delegation and notifications. There's no need to be confused, though. Delegation should only be used if one object needs to talk to another object. It's a one-to-one relationship and the communication is often unidirectional, from the delegating object to its delegate.
Delegation works best with protocols. A delegate object, the object being talked to, can be any object as long as it conforms to the delegate protocol. Remember how a table view talks to its delegate. It doesn't know which object it delegates tasks to. It only knows that it conforms to the UITableViewDelegate
protocol. As a result, delegation promotes a loosely coupled object graph.
Notifications
Notifications are used if an object wants to notify other objects of an event. The sender of the notification doesn't care which objects subscribe to the notification or how they handle the broadcast. Even though this may seem the best solution since objects are seemingly uncoupled, it introduces a subtle form of coupling.
One Solution to Rule Them All
Great. I'm going to stick with notifications ... for everything. It offers the most flexibility. Well ... not exactly. It's true that notifications are great for communication between objects that are unaware of each other.
An application that relies heavily on notifications quickly becomes a nightmare to maintain and debug. My advice is to use notifications if you have no other option. Notifications aren't bad, but they shouldn't be overused either. They are no cure-all.
Closures as an Alternative to Delegation
Even though I like using delegation in Cocoa applications, it can sometimes become a bit overwhelming. Large applications can end up with dozens of delegate protocols, with some of them defining only a single method.
An alternative approach is to use closures (or blocks in Objective-C). I only recommend this alternative as a substitute for compact delegate protocols. Let's look at an example to better understand how this works.
Imagine an application that manages a list of items. The user can add items to the list by tapping a button in the navigation bar. Each item is an instance of the Item
structure.
struct Item {
var title: String
var content: String
}
To add an item, the user taps a button in the navigation bar. This action instantiates an instance of the AddItemViewController
class. The class declares a didAddItem
property of type DidAddItemHandler?
, a type alias for a closure that accepts an Item
instance as an argument. This closure is invoked when the user creates and saves a new item.
class AddItemViewController: UIViewController {
typealias DidAddItemHandler = (Item) -> ()
var didAddItem: DidAddItemHandler?
private var item: Item?
@IBAction func save(_ sender: AnyObject) {
if let item = item, let didAddItem = didAddItem {
didAddItem(item)
}
}
}
The list of items is managed by an instance of the ItemsViewController
class. This class implements the addItem(_:)
action, which is linked to the button in the navigation bar. Tapping the button instantiates the AddItemViewController
instance.
class ItemsViewController: UIViewController {
var items = [Item]()
@IBAction func addItem(_ sender: AnyObject) {
// Initialize View Controller
let viewController = AddItemViewController()
// Configure View Controller
viewController.didAddItem = { (item) in
// Add Item to List
self.items.append(item)
}
present(viewController, animated: true)
}
}
Before presenting the add item view controller, we set its didAddItem
property. The closure accepts an Item
instance as its only argument and that enables the items view controller to add the new item to the array of items it manages. Not only removes this pattern the overhead of creating and implementing a delegate protocol, it also keeps related code together.
Singletons and Object References
There's nothing wrong with object references, but it's important to keep track which objects know about which objects. When object references are combined with singletons, you need to be extra careful to avoid getting yourself onto a slippery slope. If singletons keep references to other singletons ... well ... I'm sure you know what I mean. Singletons aren't bad, but you need to be careful when you use the singleton pattern.
What's Next?
Object references, delegation, and notifications are very common design patterns on iOS, tvOS, macOS, and watchOS. The Cocoa frameworks make heavy use of these patterns and it's important to understand how they work and when to use them.