The Observer Pattern in Swift

Beyza Sığınmış
6 min readMar 30, 2023

In software development, observing changes in state is a common pattern that is used to implement features such as event-driven programming, reactive programming, and Model-View-Controller (MVC) architecture. In this article, we will discuss how to implement the observation pattern in Swift using the code examples provided.

Observation Pattern

The observation pattern is a design pattern that allows an object (the observer) to watch for changes in another object (the subject) and take action when the subject changes. The observer object registers with the subject object to receive notifications when a specific event occurs. When the event occurs, the subject object sends a notification to all registered observers, and each observer takes action based on the notification.

The code provided demonstrates how to implement the observation pattern in Swift using the example of a post liking feature in a social media application. The following are the key components of the observation pattern in the code:

PostLikeChangeType: This enum defines the types of changes that can occur in the post liking feature, i.e., like or unlike. It also defines the postId associated with each type of change.

public enum PostLikeChangeType {
case like(postId: Int)
case unlike(postId: Int)

public var value: Int {
switch self {
case .like(let postId):
return postId
case .unlike(let postId):
return postId
}
}
}

PostStatusResultHandler and PostStatusOnChangeObserver: These typealiases define the completion handlers that are used to handle the post status change result and the change type observation.

public typealias PostStatusResultHandler = (Result<EmptyResponse, Error>) -> Void
public typealias PostStatusOnChangeObserver = (_ type: PostLikeChangeType) -> ()

PostLikeHandlerInterface and PostLikeHandlerInteractorInterface: The interface for the post like handler and the post like handler interactor is defined by these protocols. These interfaces define the methods that handle the liking and unliking of a post and the addition of a post like handler observer.

public protocol PostLikeHandlerInterface {
func postLike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler)
func postUnlike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler)
func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver)
}

public protocol PostLikeHandlerInteractorInterface {
func postLike(with postId: Int, completion: @escaping PostStatusResultHandler)
func postUnlike(with postId: Int, completion: @escaping PostStatusResultHandler)
}

PostLikedResponse: This structure defines the response object for the post liking feature.

public struct PostLikedResponse: Codable {
public let postIds: [Int]
}

PostLikeHandlerInteractor: This class implements the PostLikeHandlerInteractorInterface protocol, which defines the methods for liking and unliking a post. These methods are implemented to handle the post liking feature’s business logic.

final class PostLikeHandlerInteractor {
init() {}
}

extension PostLikeHandlerInteractor: PostLikeHandlerInteractorInterface {
func postLike(with postId: Int, completion: @escaping PostStatusResultHandler) {
// like
}

func postUnlike(with postId: Int, completion: @escaping PostStatusResultHandler) {
// unlike
}
}

PostLikeHandler: This class implements the PostLikeHandlerInterface protocol, which defines the methods for adding a post like handler observer and liking and unliking a post. The PostLikeHandler class also maintains a list of observers and notifies all registered observers when a post like status changes.

func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver) {
let id = UUID()

observers[id] = .init(
observer: observer,
onChangeObserver: { [weak self, weak observer] type in
guard observer != nil else {
self?.observers.removeValue(forKey: id)
return
}
completion(type)
}
)
}

The first parameter, observer, is the object that will observe the changes. This parameter is marked with the AnyObject type, which means that any class instance can be passed in as the observer. The second parameter, completion, is a closure that takes in a PostStatus parameter and returns nothing. This closure will be called whenever there is a change in the post’s status.

The method generates a unique identifier for the observer using UUID(). This identifier will be used to keep track of the observer.

The observer is added to a dictionary called observers. The key for the dictionary is the unique identifier generated in step 3, and the value is an instance of a struct that contains two properties:
observer: A weak reference to the object passed in as the observer parameter. This is done to avoid creating a strong reference cycle between the observer and the object being observed.
onChangeObserver: A closure that takes in a PostStatus parameter and returns nothing. This closure is created using a capture list that includes weak references to self and observer. This is done to avoid creating a strong reference cycle between the closure and the object being observed.

Whenever there is a change in the post’s status, the closure passed in as the completion parameter is called with the updated PostStatus value.

If the observer object is deallocated (i.e., its reference count drops to zero), its entry in the observers dictionary is removed to avoid attempting to call a closure on a deallocated object.

private func notifyObservers(_ observer: AnyObject, type: PostLikeChangeType) {
let filtered = observers.filter ({ $0.value.observer !== observer })
filtered.forEach({ $0.value.onChangeObserver(type) })
}

This private function is responsible for notifying all the registered observers that a change has occurred in the post liking feature. The function takes two arguments: an observer object and a PostLikeChangeType enum value that specifies the type of change that has occurred.

Before notifying the observers, the function filters out the observer object passed as an argument to prevent unnecessary notifications. This is because the observer object already knows about the change since it is the one that triggered it. By filtering out the observer, we avoid redundant notifications and improve performance.

Once the filtered list of observers is obtained, the function iterates over it and calls the onChangeObserver method of each observer, passing the PostLikeChangeType enum value as an argument. This allows each observer to react to the change in its own way, depending on its implementation.

final class PostLikeHandler: PostLikeHandlerInterface {
struct LikeObserverArgument {
weak var observer: AnyObject?
var onChangeObserver: PostStatusOnChangeObserver
}

static let shared: PostLikeHandlerInterface = PostLikeHandler()
private var observers: [UUID: LikeObserverArgument] = [:]
private let interactor: PostLikeHandlerInteractorInterface

init(interactor: PostLikeHandlerInteractorInterface = PostLikeHandlerInteractor()) {
self.interactor = interactor
}

private func notifyObservers(_ observer: AnyObject, type: PostLikeChangeType) {
let filtered = observers.filter ({ $0.value.observer !== observer })
filtered.forEach({ $0.value.onChangeObserver(type) })
}

func addPostLikeHandlerObserver(_ observer: AnyObject, completion: @escaping PostStatusOnChangeObserver) {
let id = UUID()

observers[id] = .init(
observer: observer,
onChangeObserver: { [weak self, weak observer] type in
guard observer != nil else {
self?.observers.removeValue(forKey: id)
return
}
completion(type)
}
)
}
}

extension PostLikeHandler {
func postLike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler) {
interactor.postLike(with: postId) { result in
self.notifyObservers(observer, type: .like(postId: postId))
}
}

func postUnlike(_ observer: AnyObject, with postId: Int, completion: @escaping PostStatusResultHandler) {
interactor.postLike(with: postId) { result in
self.notifyObservers(observer, type: .like(postId: postId))
}
}
}

PostListPresenter: This class uses the PostLikeHandler class to observe post like status changes and update the related post UI.

final class PostListPresenter {
private var likeHandler: PostLikeHandlerInterface

init(likeHandler: PostLikeHandlerInterface = PostLikeHandler.shared) {
self.likeHandler = likeHandler
}

func viewDidLoad() {
addPostLikeHandlerObserver()
}

private func addPostLikeHandlerObserver() {
likeHandler.addPostLikeHandlerObserver(self) { type in
switch type {
case .like(let postId):
// - update related post ui
case .unlike(let postId):
// - update related post ui
}
}
}

private func likeButtonTapped(with postId: Int, isLike: Bool) {
if isLike {
likeHandler.postLike(self, with: postId) { result in
// - update related post ui
}
} else {
likeHandler.postUnlike(self, with: postId) { result in
// - update related post ui
}
}
}
}

In conclusion, the observation pattern is a powerful design pattern that is widely used in software development to observe changes in state and take action when a change occurs.

This pattern is particularly useful when there is a one-to-many relationship between classes and broadcasting changes is necessary.

By using the observation pattern and implementing dependency injection, we can create a testable code base that avoids code duplication and memory leaks when applying changes in the id observed by different classes.

Thank you for taking the time to read. If you have any feedback or would like to get in touch, please feel free to reach out. ✨🤘🏻

References

https://www.swiftbysundell.com/articles/observers-in-swift-part-2/

--

--