https://medium.cobeisfresh.com/why-you-shouldn-t-use-delegates-in-swift-7ef808a7f16b
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!
1 2 3 4 5 6 7 8 9 10 |
class MyClass { var myFunction: (String, Int)->() = { phrase, numOfTimes in for i in 0 ..< numOfTimes { print(phrase) } } } let a = MyClass() a.myFunction("Ha dooo ken!", 3) |
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:
1 2 3 4 5 6 7 8 |
class NetworkService { var onComplete: ((String)->())? //an optional function func fetchDataFromUrl(url:String) { sleep(2) onComplete?(" yay! data completed ") } } |
1) we declare a class called NetworkService
2) We create a function type, called onComplete, that takes in a parameter of String type and returns Void. We make it into an optional so that we can take advantage of Optional Chaining, where we can query the reference. If its valid, great. If its nil, it would return nil and will not crash.
3) We create a function called fetchDataFromUrl that simulates getting data from a server by using sleep. After 2 seconds, granted something came back, we call our callback function property. However! Note here that our onComplete is defaulted to nil. Hence if we call onComplete in fetchDataFromUrl, it will query the nil and get nil back. Nothing will happen. In order for something to happen, we need to implement the onComplete optional function. You can implement onComplete in NetworkService initializer or externally since our onComplete is public.
a) Hence, usually, we will instantiate an object of our class. We have a reference to that object called service.
b) We then declare the callback definition for onComplete.
c) Finally, we call fetchDataFromUrl. After it runs through the function implementation, it calls the onComplete function as defined in b)
1 2 3 4 5 6 7 8 9 |
let service = NetworkService() // a // b service.onComplete = { result in print("I got [\(result)] from the server!") } // c service.fetchDataFromUrl(url: "http://www.google.com") |
Another way to use callbacks – Data has changed!
1) as always, declare your callback. This time, we’re declaring the function type as having a parameter of “an array of String”, and returns void. Let’s call this property onUsernamesChanged
2) implement an init for our class, and define the definition for our callback. Since our callback’s function type has a parameter of an array of String, we use names as an identifier for that parameter.
3) Then, we simply use it in didSet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class AnotherWay { var onUsernamesChanged: ( ([String])->() )? // 1) // 2) init() { print("init - define callback definition to our callback property") onUsernamesChanged = { names in // where names is array of String print("calling callback onUsernamesChanged's definition") for i in 0 ..< names.count { print("\(i) - \(names[i])") } } } // 3) var loadedUsernames = [String]() { didSet { print("didSet - property loadedUsernames did set to new String array") onUsernamesChanged?(loadedUsernames) } } } |
Thus, another great way to use callbacks is when you want to get notified data has been changed.
1 2 |
let way = AnotherWay() way.loadedUsernames = ["Ricky", "Kevin", "Dean"] |
Full Source Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import Foundation print("------ callback basic example --------") class MyClass { var myFunction: (String, Int)->() = { phrase, numOfTimes in for i in 0 ..< numOfTimes { print(phrase) } } } let a = MyClass() a.myFunction("Ha dooo ken!", 3) class NetworkService { var onComplete: ((String)->())? //an optional function func fetchDataFromUrl(url:String) { print("calling fetchDataFromUrl") sleep(2) onComplete?(" yay! data completed ") } } print("\n------ call back usage #1 --------\n") let service = NetworkService() service.onComplete = { result in print("calling callback onComplete's definition") print("I got [\(result)] from the server!") } service.fetchDataFromUrl(url: "http://www.google.com") class AnotherWay { var onUsernamesChanged: ( ([String])->() )? // takes in an array of String init() { print("init - define callback definition to our callback property") onUsernamesChanged = { names in // where names is array of String print("calling callback onUsernamesChanged's definition") for i in 0 ..< names.count { print("\(i) - \(names[i])") } } } // array of Strings var loadedUsernames = [String]() { didSet { print("didSet - property loadedUsernames did set to new String array") onUsernamesChanged?(loadedUsernames) } } } print("\n------ call back usage #2 --------\n") let way = AnotherWay() way.loadedUsernames = ["Ricky", "Kevin", "Dean"] |
So why are callbacks better?
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.
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.
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 with delegation, like making the NetworkService transform results for presentation, which should not be its job!
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.