Mega Bundle SALE is ON! Get ALL of our amazing iOS app codebases at 95% OFF discount 🔥

Developing an iOS application is all about sending and receiving data between two or more objects. We are using all of these great design patterns (Swift delegates, notifications, closures, KVO) in our iOS Starter Kits to build highly modularized code. In this article we are taking a closer look at each of these popular architectural patterns, discussing their pros and cons as well as showcasing code examples for each of them.

swift delegates

The different Swift strategies of passing data between different classes are called Communication Patterns. In general, communication in software development can be grouped into three categories:

  • synchronous
  • asynchronous
  • two-way messaging

In iOS/MacOS development, the most popular Swift communication patterns are:

  • Swift Delegates
  • Notifications
  • Completion handlers
  • KVO

In this article, we are looking at these Swift messaging patterns and showing, with simple examples, how they can be implemented efficiently and robustly.

Even a single function that is returning data is some kind of messaging pattern: for a given input, we would like to get some output.

Let’s decompose the following method:

class SimpleCalculator {
    func addition(lhs: Int, rhs: Int) -> Int {
        return lhs + rhs
    }
}
  • SimpleCalculator is our base object
  • lhs and rhs of type Int is our Input Message
  • the returned value of type Int is our Output Message

Simple as that, but when we start to dive into asynchronous programming stuff, it is starting to get much more complicated.

Swift Delegates

iOS developers are extremely familiar with using delegates in a Swift codebase. In fact, almost every Apple framework is using delegates. Even the very first object that is responsible for doing a lot of stuff is a delegate: UIApplicationDelegate.

public protocol UIApplicationDelegate : NSObjectProtocol {

    @available(iOS 2.0, *)
    optional public func applicationDidFinishLaunching(_ application: UIApplication)

    @available(iOS 6.0, *)
    optional public func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool
...
}

iOS, as the operating system of the iPhone, is deciding when to launch your app and it informs your app that is has just started. To achieve that, it is using this delegate to communicate that your app changes the state. For example, when an app is launched on iOS, the delegate method named  `func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool` is being called.

But the best way to learn stuff is doing that by examples, so let’s build the app that has a big blue bubble on the screen and when the users tap on the bubble, it’s falling down.

class BubbleView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    private func setup() {
        self.isUserInteractionEnabled = true
        let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(BubbleView.didTapIntoButton))
        self.addGestureRecognizer(tapGestureRecognizer)
    }
    
    @objc func didTapIntoButton(_ sender: UITapGestureRecognizer) {
    }
}

So we have a class named BubbleView in here. As you can see, it inherits from UIView, so it denotes a UI view on the screen. Let’s define the delegate, by writing it as a protocol:

protocol BubbleViewDelegate: class {
    func userDidTap(into bubbleView: BubbleView)
}

So Apple recommends naming the delegate methods with didDo prefix, as in general Swift delegates inform other objects about the event that just happened. Secondly, Apple’s recommendation is to pass in the sender object within the delegate method. That’s why our method is called userDidTap and as an argument, this method takes the BubbleView object.

Lastly, we need to amend our BubbleView implementation with two extra lines of code:

class BubbleView: UIView {
    { ... }
    weak var delegate: BubbleViewDelegate?
    
    @objc func didTapIntoButton(_ sender: UITapGestureRecognizer) {
        delegate?.userDidTap(into: self)
    }
}

And then with only a few lines of code, we can use our delegate inside the ContainerViewController. which is the main view controller that’s managing the entire screen.

class ContainerViewController: UIViewController {
    
    lazy var bubbleView: BubbleView = {
        let bubbleView = BubbleView(frame: CGRect(x: 80, y: 0, width: 160, height: 160))
        bubbleView.backgroundColor = .blue
        bubbleView.layer.cornerRadius = 80
        bubbleView.delegate = self
        return bubbleView
    }()
    override func loadView() {
        super.loadView()
        view.addSubview(bubbleView)
    }
}
extension ContainerViewController: BubbleViewDelegate {
    func userDidTap(into bubbleView: BubbleView) {
        let currentBounds = view.bounds
        UIView.animate(withDuration: 1.5) {
            var frame = bubbleView.frame
            frame.origin.y = currentBounds.height
            bubbleView.frame = frame
        }
    }
}

And the final effect is visible here:

swift delegate example

Advanced use

Delegates can be used in more complex situations. For example, we can leverage Swift delegates if we would like to add the possibility to specify the thread on which our delegate method will be invoked. To achieve that, we can add a few simple lines of code:

class BubbleView: UIView {
    private var delegateQueue: DispatchQueue = .main
    private weak var delegate: BubbleViewDelegate?
    
    func setDelegate(_ delegate: BubbleViewDelegate?, queue: DispatchQueue? = nil) {
        assert(self.delegate == nil, "Delegate was already set.")
        self.delegate = delegate
        queue.map { delegateQueue = $0 }
    }
    
    @objc func didTapIntoButton(_ sender: UITapGestureRecognizer) {
        delegateQueue.async { [weak self] in
            guard let self = self else { return }
            self.delegate?.userDidTap(into: self)
        }
    }

Caveats of using Swift delegates

  • A given delegate can be overridden by some other object that would like to get these messages from the BubbleView object. It only supports 1 to 1 Messaging basically.
  • If we would like to inform more then one object about that some event happened in this BubbleView object, it’s not possible while using delegates.
  • It’s also easy to forget that delegate needs to be defined as a weak reference and it can lead to the strong reference cycle and then to serious memory leaks, that will eventually cause your app to crash. (also know as retain cycles)

Advantages of Swift Delegation pattern

  • Easy to implement
  • Easy to maintain and add a new functionality
  • Easy to understand the 1-1 connection between two objects, while keeping them separate from each other

Swift NSNotification and NSNotificationCenter

NSNotifications are also frequently used by iOS and MacOS developers. This code architectural pattern is used for many to many communication between objects, solving one of the limitations of delegates. This messaging pattern is also widely used inside Apple frameworks, so you might be familiar with it already.

For example, when you would like to be informed when the keyboard will be shown on the screen, you can register for the notification named `UIResponder.keyboardWillShowNotification`, and similarly for catching the moment when the keyboard will be hidden `keyboardWillHideNotification`.

Let’s get started with a code example in Swift. Making a URL request is one of the most popular tasks that we do while creating iOS applications. So let’s create a simple APIClient object.

class APIClient {
    let urlSession = URLSession(configuration: .default)    
    func perform(_ urlRequest: URLRequest,
                 completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        let urlSessionTask =
            urlSession
                .dataTask(with: urlRequest) { [weak self] (data, response, error) in
                    completion(data, response, error)
        }
        urlSessionTask?.resume()
    }
}

Our APIClient implementation is very simplistic, for learning reasons. We can now perform HTTP requests and get the results back in the completion closure.

But as developers we love logs. So we can add `prints` or even the debugPrint methods into this client, but this solution is not so clean and can make our simple method very hard to read and understand. Instead of that, we can send a notification that something happened.

Sending Notifications

So let’s modify our APIClient a little:

class APIClient {
    let urlSession = URLSession(configuration: .default)
    let notificationCenter = NotificationCenter.default
    
    func perform(_ urlRequest: URLRequest,
                 completion: @escaping (Data?, URLResponse?, Error?) -> Void) {
        var urlSessionTask: URLSessionTask?
        urlSessionTask =
            urlSession
                .dataTask(with: urlRequest) { [weak self] (data, response, error) in
                    completion(data, response, error)
                    self?.notificationCenter
                        .post(.makeURLSessionTaskNotification(urlSessionTask,
                                                              urlSession: self?.urlSession,
                                                              forName: .urlSessionTaskDidStart))
        }
        urlSessionTask?.resume()
        notificationCenter
            .post(.makeURLSessionTaskNotification(urlSessionTask,
                                                  urlSession: urlSession,
                                                  forName: .urlSessionTaskDidStart))
    }
}

So let’s decompose the components here, line by line:

  • NotificationCenter.default – here we are using the default notification center, provided by Apple’s NotificationCenter object to send and receive notifications. If you are creating your own framework or maybe you would like to unit test this object, you should create and inject your own NotificationCenter instance.
  • var urlSessionTask: URLSessionTask? – here we are storing the reference to the urlSessionTask. We need to make it a variable because it will be used inside the closure method that is creating this object.
  • .makeURLSessionTaskNotification(urlSessionTask, urlSession: urlSession, forName: .urlSessionTaskDidStart)) – a simple factory method that allows us to create the notification broadcasting that the URLSessionTask did start.
  • .makeURLSessionTaskNotification(urlSessionTask, urlSession: urlSession, forName: .urlSessionTaskDidStart) – a second notification that is scheduled when task is completed.

Here’s how to build your first custom notification:

extension Notification.Name {
    static let urlSessionTaskDidStart = Notification.Name("didStartURLSessionTask")
    static let urlSessionTaskDidComplete = Notification.Name("didStartURLSessionTask")
}

extension Notification {
    static func makeURLSessionTaskNotification(_ urlSessionTask: URLSessionTask?,
                                               urlSession: URLSession?,
                                               forName name: Notification.Name) -> Notification {
        guard let urlSessionTask = urlSessionTask else { fatalError("URLSessionTask was empty.") }
        return Notification(name: name, object: urlSession, userInfo: [URLSessionTask.urlSessionTaskKey: urlSessionTask])
    }
}

extension URLSessionTask {
    static let urlSessionTaskKey = "URLSessionTask.urlSessionTaskKey"
}

Receiving Notifications

So let’s register for listening to our notifications.

notificationCenter
   .addObserver(forName: .urlSessionTaskDidStart,
                object: nil,
                queue: nil) { [weak self] (notification) in
                    self?.handleURLSessionTaskDidStart(notification)
       }
notificationCenter
   .addObserver(forName: .urlSessionTaskDidComplete,
                object: nil,
                queue: nil) { [weak self] (notification) in
                     self?.handleURLSessionTaskDidComplete(notification)

We have exactly two options for registering for observing the notifications:

  •  func addObserver(_ observer: Any, selector aSelector: Selector, name aName: NSNotification.Name?, object anObject: Any?)
  •  func addObserver(forName name: NSNotification.Name?, object obj: Any?, queue: OperationQueue?, using block: @escaping (Notification) -> Void) -> NSObjectProtocol

In the first example, we need to define the selector (obj-c) that should be invoked when the notification is posted, and we also need to pass the observer object (self) which will be stored in some notification center observers repository.

The second method gives us a more Swifty way to observe given events, the most meaningful difference being that we as the observers are responsible for saving the notification token (NSObjectProtocol object), and use it when we would like to unregister from notifications.

So URLRequestsObserver object can look like that:

class URLRequestsObserver {
    let notificationCenter = NotificationCenter.default
    private var tokens: [Any] = []
    init() {
        registerForNotifications()
    }
    deinit {
        tokens.forEach(notificationCenter.removeObserver)
    }
    private func registerForNotifications() {
        notificationCenter
            .addObserver(forName: .urlSessionTaskDidStart,
                         object: nil,
                         queue: nil) { [weak self] (notification) in
                            self?.handleURLSessionTaskDidStart(notification)
        }
        notificationCenter
            .addObserver(forName: .urlSessionTaskDidComplete,
                         object: nil,
                         queue: nil) { [weak self] (notification) in
                            self?.handleURLSessionTaskDidComplete(notification)
        }
    }
    private func handleURLSessionTaskDidStart(_ notification: Notification) {
        guard let urlSesisonTask = notification.userInfo?[URLSessionTask.urlSessionTaskKey] as? URLSessionTask else { return }
        print("URL session task did start: \(urlSesisonTask)")
    }
    private func handleURLSessionTaskDidComplete(_ notification: Notification) {
        guard let urlSesisonTask = notification.userInfo?[URLSessionTask.urlSessionTaskKey] as? URLSessionTask else { return }
        print("URL session task did complete: \(urlSesisonTask)")
    }
}

As with all the design patterns, there are pros and cons for Swift Notification pattern as well.

Drawbacks of using NotificationCenter

  • Observer objects need to store the NSObjectProtocols tokens
  • It’s not obvious when to use NotificationCenter.default and when to create your own NotificationCenter object
  • It’s easy to forget to unregister for notifications when the object is deallocated which can lead our app to crash
  • The announcers and listeners are not tight together via strongly typing, which can introduce bugs.
  • The notifiers and listeners are not aware of each other, so cleaning up dead code becomes more challenging (you might end up with notifications that are not listened by any observer)

Benefits of using NSNotifications

  • Multiple objects can be informed about that some event occurred – Many to Many Communication
  • We can easily define the thread on which we would like to be informed about a given event
  • We don’t need access to the object that is sending the notification for registering for notifications, so this is highly convenient for communication between two objects that are in completely different contexts (e.g. two different tabs, networking layer and profile view controller, etc.)

Swift Completion Handlers

In Swift, completion handlers are closures, as stated in Apple’s documentation: 

Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages.

To simplify our mental model, closures can be treated as functions. These functions can act as input arguments and output arguments.

Let’s check out a trivial example:

let voidClosure = { () -> Void in
    print("This is void  closure")
}
voidClosure()
# This is void closure

The example above shows that simple closures (Void -> Void) can be used without any argument. Let’s see something more advanced which is a closure that takes data as an argument:

var dataClosure = { (data: Data) -> Void in
    print("This is closure that takes: \(data)")
}
dataClosure(Data([0x0a]))

And next closure is a closure which takes data as an argument and returns an optional String:

var dataClosure = { (data: Data) -> String? in
    print("This is closure that takes: \(data) and return String")
    return String(data: data, encoding: .utf8)
}
let resultString = dataClosure(Data([0x5A, 0x21]))

That being said, let’s check how we can use closures in our production codebase.

class APIClient {
    enum APIClientError: Swift.Error {
        case invalidStatusCode
    }
    {...}
    func preformLogin(using urlRequest: URLRequest, completionHandler: @escaping (Bool, Error?) -> Void) {
        perform(urlRequest) { (data, urlResponse, error) in
            if let error = error {
                completionHandler(false, error)
                return
            }
            if let urlResponse = urlResponse as? HTTPURLResponse,
                (200..<300).contains(urlResponse.statusCode) {
                completionHandler(true, nil)
            } else {
                completionHandler(false, APIClientError.invalidStatusCode)
            }
        }
    }
}

The method func preformLogin(using urlRequest: URLRequest, completionHandler: @escaping (Bool, Error?) -> Void)  takes as arguments urlRequest and completionHandler closure which is marked as @escaping closure, which means that it can be invoked outside of the scope of the method that is taking it as an argument.

So even though in performing the request we already use the closure, here we are doing more advanced stuff. Inside the completion closure that is invoked when urlRequest is performed, we added some logic that is checking if our response is a success and then we can invoke our completionHandler with the correct result. What is interesting here, as an example, our completionHandler takes a couple as arguments: (Bool, and Error?), Bool is not optional because it represents whether the user should be logged in or not and error is optional, because when the user is logged in successfully, we wouldn’t like to propagate any error.

So let’s see how to use this closure based solution:

let apiClient = APIClient()
apiClient.preformLogin(using: URLRequest(url: URL(string: "https://api.github.com/v3/users")!)) { (isLoggedIn, error) in
    switch (isLoggedIn, error) {
    case (true, _):
        print("user was logged")
    case (false, let error?):
        print("error occured while login user: \(error)")
    default:
        print("Unknown stuff happend")
    }
}

Once again, let’s define the downside of using completion handlers:

  • Inside the closures we have to use [weak self] reference, otherwise, we might introduce memory leaks
  • We cannot assume that our completion handler will be invoked on main or on a background thread
  • Using the closure is always a 1-1 solution
  • When we are passing the closure as a completion handler we cannot be sure that will be invoked only once

Advantages of using Completion Handlers in Swift

  • They are extremely easy to use
  • There are memory efficient, in that blocks don’t live in memory once they are called (as it happens to delegates, for instance)
  • Our code is easier to read and understand
  • Handlers can be very powerful, they can take any object(s) as arguments, and can return Void or even return some value
  • Using the closure is always a 1-1 solution and it’s stongly typed, enforcing correct API use at build time.

KVO in Swift

KVO stands for Key-Value Observing and it’s a quite an advanced and powerful mechanism. This pattern gives the possibility to observe changes (before and after change occurred) of any value that the given object contains. There are a few constraints that need to be fulfilled to use the KVO pattern in Swift.

Usually, it’s better to avoid this design pattern, since its syntax is heavy and it can create lots of bugs for an inexperienced developer. Only use it if you can’t achieve the messaging pattern with the other communication strategies presented in this article. If you are interested in that follow our blog or you can check out Apple’s documentation.

Conclusion

Apple gives us a lot of ways that we can leverage to pass data messages between two or more objects in Swift. As described in this Swift tutorial, they are very simple and can be easily implemented and used by iOS developers. Let us know in the comments which one is your favorite: Swift Delegates, KVO, Completion Handlers or Swift NSNotification?

Categories: Swift programming

Leave a Reply

Your email address will not be published. Required fields are marked *

Shopping Cart