Why you shouldn’t use delegates in Swift

The delegate pattern is okay. But there’s a better way.

You’re building an iOS app. You have your nice, SOLID architecture in place. You have a model, a networking layer, a UI layer, and maybe some helpers in between. The textbook way you would pass data around those layers would be trough delegation, a really useful and common pattern in iOS development.

Delegation simply means that you pass yourself to someone else, when you want that someone to notify you of changes, so you canreact to them. For instance, if a ViewController talks to a network service, and wants to get notified when that service is done with some request, it would make itself the network service’s delegate. The network service would then call the delegate methods when it’s done.

protocol NetworkServiceDelegate {
    func didCompleteRequest(result: String)
}

class NetworkService {
    var delegate: NetworkServiceDelegate?
    
    func fetchDataFromUrl(url: String) {
        API.request(.GET, url) { result in
            delegate?.didCompleteRequest(result)
        }
    }
}

class MyViewController: UIViewController, NetworkServiceDelegate {
    
    let networkService = NetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.delegate = self
    }
    
    func didCompleteRequest(result: String) {
        print("I got \(result) from the server!")
    }
}

Passing delegate references is fine. There’s nothing functionally wrong with it. But there’s a better way, and I’ll tell you why it’s better.


Using callbacks for delegation

Callbacks are similar in function to the delegate pattern. They do the same thing: letting other objects know when something happened, and passing data around.

What differentiates them from the delegate pattern, is that instead of passing a reference to yourself, you are passing a function. Functions are first class citizens in Swift, so there’s no reason why you wouldn’t have a property that is a function!

class MyClass {
  var myFunction: (String)->() = { text in
    print(text)
  }
}

MyClass now has a myFunction property that it can call and anyone can set (since properties are internal by default in Swift). This is the basic idea of using callbacks instead of delegation. Here’s the same example as before but with callbacks instead of a delegate:

class NetworkService {
    
    var onComplete: ((result: String)->())? //an optional function
    
    func fetchDataFromUrl(url: String) {
        API.request(.GET, url) { result in
            onComplete?(result: result) 
//                    ^ optional unwrapping for functions!
        }
    }
}
class MyViewController: UIViewController {
    
    let networkService = NetworkService()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        networkService.onComplete = { result in
            print("I got \(result) from the server!")
        }
    }
}

Another great way to use callbacks is when you want to get notified data has been changed. You can do this by calling the callback in a property observer:

var onUsernamesChanged: ([String]->())?
    
var loadedUsernames = [String]() {
    didSet {
        onUsernamesChanged?(loadedUsernames)
    }
}

Quick note about callbacks: Much like delegates should be weak properties to stop retain cycles, you should catch self as a weak variable inside the closure.


So why are callbacks better?

1. Decoupling

Delegates lead to pretty decoupled code. It doesn’t matter to the NetworkService who its delegate is, as long as they implement the protocol. However, the delegate has to implement the protocol, and if you’re using Swift instead of @objc protocols, the delegate has to implement every method in the protocol. (since there’s no optional protocol conformance)

When using callbacks, on the other hand, the NetworkService doesn’t even need to have a delegate object to call methods on, nor does it know anything about who’s implementing those methods. All it cares about is when to call those methods. Also, not all of the methods need to be implemented.

2. Multiple delegation

What if you want to notify a *ViewController *when a request finishes, but maybe also a some sort of logger class, and some sort of analytics class.

With delegates, you would have to have an array of delegates, or three different delegate properties that might even have different protocols! (I’ll be the first to admit I’ve done this)

With callbacks, however, you could define an array of functions (I love Swift) and call each of those when something’s done. There’s no need to have a bunch of different objects and protocols risking retain cycles and writing boilerplate code.

3. Clearer separation of concerns

The way I see the difference between delegates and callbacks is that with delegates, the NetworkService is telling the delegate “Hey, I’ve changed.” With callbacks, the delegate is observing the NetworkService.

In reality, the difference is minimal, but thinking in the latter way helps prevent anti-patterns often found withdelegation, like making the NetworkService transform results for presentation, which should not be its job!

4. Easier testing!

Ever felt like your codebase is twice as big with unit tests, because you have to mock every protocol, including all of the delegates in your app?

With callbacks, not only do you not have to mock any delegates, but it lets use use whatever callback you want in each test!

In one test, you might test if the callback gets called, then in another you might test if it’s called with the right results. And none of those require a complicated mocked delegate with someFuncDidGetCalled booleans and similar properties.


I personally think callbacks lead to clearer code and tests, and are a much Swiftier (and better) way to pass data around in your app. I hope you’ve learnt something new today, and good luck out there!