Swift Protocols, Delegate, and Notification Design Patterns

Guilherme Teixeira de Mello
8 min readJun 13, 2023

--

In the world of Swift and iOS development, understanding the power of design patterns is crucial for building robust and flexible applications. Among the many design patterns, protocols, delegates, and notifications play pivotal roles in facilitating communication and defining interaction patterns between objects.

In this blog post, we will delve into the world of Swift protocols, delegates, and the notification design pattern. We’ll explore how these patterns can be leveraged to create modular, extensible, and maintainable code in your iOS projects.

So, let’s dive in and uncover the ins and outs of Swift protocols, delegates, and notifications.

Swift Protocols

The protocol design pattern is a widely used pattern in Swift and iOS development. It allows you to define a set of methods and properties that a class or struct must conform to. This pattern promotes loose coupling between components and enables flexible and modular code design.

Here’s an example of how you can use the protocol design pattern in Swift for iOS development:

// Define a protocol
protocol DataDelegate: AnyObject {
func didReceiveData(_ data: String)
}

// Create a class that conforms to the protocol
class DataManager {
weak var delegate: DataDelegate?

func fetchData() {
// Simulate fetching data
let data = "Some data"

// Notify the delegate that data has been received
delegate?.didReceiveData(data)
}
}

// Create another class that conforms to the protocol
class ViewController: UIViewController, DataDelegate {
let dataManager = DataManager()

override func viewDidLoad() {
super.viewDidLoad()
dataManager.delegate = self
dataManager.fetchData()
}

func didReceiveData(_ data: String) {
// Handle the received data
print("Received data: \(data)")
}
}

In this example, we have a protocol called DataDelegate that declares a method didReceiveData(_:). The DataManager class has a weak reference to an instance that conforms to the DataDelegate protocol. When fetchData() is called, it simulates fetching data and then notifies the delegate by calling the didReceiveData(_:) method.

The ViewController class conforms to the DataDelegate protocol and implements the didReceiveData(_:) method. In viewDidLoad(), it sets itself as the delegate of the DataManager instance and calls fetchData(). When the data is received, the didReceiveData(_:) method in the ViewController is invoked, allowing it to handle the received data.

By using protocols, the DataManager class and ViewController class are decoupled. The DataManager class doesn't need to know about the specific implementation of the delegate—it only needs to know that it conforms to the DataDelegate protocol. This promotes code reusability and makes it easier to swap out different delegate implementations.

This is a simplified example, but it demonstrates the basic concept of the protocol design pattern in Swift for iOS development. Protocols can be powerful tools for defining interfaces, enabling communication between different components in an application.

Swift Delegate

The delegate design pattern is another commonly used pattern in iOS development. It allows one object (the delegate) to communicate and provide functionality to another object (the delegating object). The delegating object holds a reference to the delegate and calls specific methods on it to notify or request information.

Here’s an example of how you can use the delegate design pattern in Swift for iOS development:

// Define a protocol for the delegate
protocol DataDelegate: AnyObject {
func didReceiveData(_ data: String)
}

// Create a delegating class
class DataManager {
weak var delegate: DataDelegate?

func fetchData() {
// Simulate fetching data
let data = "Some data"

// Notify the delegate that data has been received
delegate?.didReceiveData(data)
}
}

// Create a delegate-conforming class
class ViewController: UIViewController, DataDelegate {
let dataManager = DataManager()

override func viewDidLoad() {
super.viewDidLoad()
dataManager.delegate = self
dataManager.fetchData()
}

func didReceiveData(_ data: String) {
// Handle the received data
print("Received data: \(data)")
}
}

In this example, we have a protocol called DataDelegate that declares a method didReceiveData(_:). The DataManager class has a weak reference to an instance that conforms to the DataDelegate protocol. When fetchData() is called, it simulates fetching data and then notifies the delegate by calling the didReceiveData(_:) method.

The ViewController class conforms to the DataDelegate protocol and implements the didReceiveData(_:) method. In viewDidLoad(), it sets itself as the delegate of the DataManager instance and calls fetchData(). When the data is received, the didReceiveData(_:) method in the ViewController is invoked, allowing it to handle the received data.

The delegate design pattern allows for loose coupling between objects. The DataManager class doesn't need to know the specific implementation of its delegate; it only needs to know that the delegate conforms to the DataDelegate protocol. This promotes modularity and code reusability.

Delegation can be used in various scenarios in iOS development. It is commonly used for handling user interactions, such as button taps or table view cell selection. By implementing delegation, you can separate concerns and encapsulate functionality, making your code more maintainable and easier to understand.

The delegate design pattern is an effective way to establish communication and allow for extensibility between objects in iOS development. It promotes clean and modular code organization, enabling you to build robust and flexible applications.

Swift Notification

The notification design pattern in iOS involves using the NotificationCenter class to facilitate communication between different parts of an application. It allows objects to broadcast and observe notifications, enabling loosely coupled communication without direct dependencies between the sender and receiver.

Here’s an explanation of the notification design pattern in iOS:

1. Posting Notifications:

  • In the notification design pattern, the sender of the notification is responsible for posting it.
  • To post a notification, the sender creates an instance of NotificationCenter and calls the post(name:object:userInfo:) method.
  • The name parameter is a unique identifier for the notification, typically represented as a Notification.Name constant.
  • The object parameter represents the object associated with the notification (can be nil).
  • The userInfo parameter is an optional dictionary that carries additional information related to the notification.
  • For example, to post a notification named “DataReceivedNotification” with an associated object and user info:
NotificationCenter.default.post(name: Notification.Name("DataReceivedNotification"), object: self, userInfo: ["key": "value"])

2. Observing Notifications:

  • Objects interested in receiving notifications can add themselves as observers using NotificationCenter.
  • To observe notifications, an object calls the addObserver(_:selector:name:object:) method of NotificationCenter.
  • The observer specifies a selector (a method) that will be called when the notification is posted.
  • The name parameter matches the name of the notification to observe. You can use the same Notification.Name constant used during posting.
  • The object parameter can be specified to filter notifications based on the associated object.
  • For example, to observe the “DataReceivedNotification” from the above example:
NotificationCenter.default.addObserver(self, selector: #selector(handleDataReceived(_:)), name: Notification.Name("DataReceivedNotification"), object: nil)

3. Handling Notifications:

  • The observer must implement a method that matches the selector specified during observation.
  • This method takes a single parameter of type Notification, which provides information about the posted notification.
  • The method can access the userInfo dictionary to retrieve any additional data passed with the notification.
  • For example, the handler method for “DataReceivedNotification”:
@objc func handleDataReceived(_ notification: Notification) {
if let data = notification.userInfo?["key"] as? String {
// Handle the received data
print("Received data: \(data)")
}
}

4. Removing Observers:

  • It’s important to remove observers when they are no longer needed to avoid potential memory leaks.
  • To remove an observer, call the removeObserver(_:name:object:) method of NotificationCenter.
  • The observer must be the same object that was used during observation.
  • For example, to remove the observer for “DataReceivedNotification”:
NotificationCenter.default.removeObserver(self, name: Notification.Name("DataReceivedNotification"), object: nil)

The notification design pattern is useful when you have multiple objects that need to be notified about events or changes, and you want to decouple the sender and receiver. It allows for flexible communication and promotes modularity by reducing direct dependencies between objects.

Apple’s frameworks extensively use the notification design pattern. For example, NotificationCenter is used in UIKit to notify objects about system events like keyboard appearance, orientation changes, and view controller lifecycle events. By utilizing notifications, iOS developers can build more flexible and extensible applications that can respond to various events and adapt to different scenarios.

As we dive deep into this topic, there is always the question of when do we need to use Delegates and when to use Notifications, so thinking about this question, I’ve decided to expose my opinion on this topic.

When to use Delegates

  1. One-to-One Communication: Delegates are suitable when you have a one-to-one relationship between objects. One object acts as the delegate for another object, providing specific functionality or data.
  2. Tight Coupling: Delegates are appropriate when the sender and receiver have a direct relationship. The delegating object has knowledge of the delegate and communicates with it directly.
  3. Specific Response Required: Delegates are beneficial when the sender expects a specific response or action from the delegate. The delegate methods can return values or modify the behavior of the delegating object.
  4. Customization and Extensibility: Delegates are useful when you want to allow customization or extension of functionality. Delegates can provide a way to customize behavior by implementing specific delegate methods.

When to use Notifications

  1. One-to-Many Communication: Notifications are ideal when you want to broadcast information to multiple objects that are interested in the event. Any number of objects can observe and respond to the notification.
  2. Loose Coupling: Notifications promote loose coupling between objects. The sender and receiver do not need to have direct knowledge of each other. They only need to know the notification name and, optionally, the associated object.
  3. Anonymous Communication: Notifications are useful when the sender does not require a specific response from the receivers. The sender can post the notification without knowledge of the interested observers, and they can handle the notification as needed.
  4. Cross-Component Communication: Notifications are helpful when you need to communicate across different components or modules of your application, where direct object references may not be available.

Conclusion

In this blog post, we have explored three essential aspects of Swift development: protocols, delegates, and the notification design pattern. These concepts provide powerful tools to enhance the structure, organization, and communication within your iOS applications.

Protocols enable you to define a blueprint of required methods and properties, promoting code reuse, and enabling polymorphism. By adopting protocols, you can create flexible, modular components that can be easily extended and customized.

Delegates establish a close relationship between objects, allowing for one-to-one communication and customization. By delegating responsibilities to separate objects, you can decouple components, improve modularity, and create highly customizable interactions.

Notifications, on the other hand, provide a loosely coupled communication mechanism, enabling one-to-many communication across different components. With notifications, you can broadcast information and allow multiple objects to observe and respond to events, promoting flexibility and extensibility.

By mastering protocols, delegates, and the notification design pattern, you gain a comprehensive toolkit for designing elegant and efficient iOS applications. Understanding when and how to leverage these patterns can greatly improve your code’s organization, maintainability, and extensibility.

As you continue your journey in Swift development, remember to carefully consider the requirements and context of your application when choosing between protocols, delegates, or notifications. Each pattern has its strengths and best use cases, and by selecting the appropriate pattern, you can create code that is both elegant and efficient.

So, go forth and leverage the power of Swift protocols, delegates, and notifications to create exceptional iOS applications that are modular, flexible, and robust. Happy coding!

--

--