In my post Why do we need delegates, I explained one major developer use of delegates: moving data from a destination view controller back to the controller that presented it or pushed it onto the stack. This is not the only use of delegates. Apple uses it often in APIs. In this theoretical lesson we’ll discuss what they doing under-the-hood and why. We’ll dissect the entire process, so you will understand what you are doing in your code.
The Basic Problem
Let’s start with a simple class, which if you want you can copy into a playground to try:
class SimpleClass{ var array:[String] = ["A","B","C","D"] func printArray(){ for string in array { if string.characters.count != 0{ print(string) }else{ print("Empty String") } } } }
We use this class this way:
let simpleClass = SimpleClass() simpleClass.printArray()
This class prints an array. When a developer writes this you have access to the methods and functions. If you wanted to re-write this to be more customizable, you could just change the source. You can assign different values to the array.
However, Apple (and sometimes developers) don’t want to you messing with their code. Apple will suspend your developer license for doing that to an API. Often Apple writes the code so you have no access to things like array
at all. Suppose our code changes to this:
class SimpleClass{ func printArray(){ let array:[String] = ["A","B","C","D"] for string in array { if string.characters.count != 0{ print(string) }else{ print("Empty String") } } } }
Here’s the problem: You want to format string
and set the values in array
. You just can’t since you have no access to them.
Adding a Protocol
What Apple does for these customization situations is write protocols for the class. You’ll find two different names of this protocol. For getting data, you’ll find a protocol called a data source. For changing data or reacting to events, you’ll find a delegate.
Adding a Data Source
A data source is a function that defines the data in a class. You write the function to return the data you want to use. For example, we could add the following protocol for array
protocol SimpleClassDataSource{ func array() -> [String] }
We could then change our code to this:
class SimpleClass{ var dataSource:SimpleClassDataSource! = nil func printArray(){ let array:[String] = dataSource.array() for string in array { if string.characters.count != 0{ print(string) }else{ print("Empty String") } } } }
Line 2 creates a property dataSource
which has an optional value of SimpleClassDataSource
as its type. In line 4, the code calls dataSource
function array
to get the data for the array.
Of course, there is no data yet. That’s in the class that will use SimpleClass
and adopts the SimpleClassDataSource
protocol. Suppose we had this class:
class PrintingDoughnuts{ }
We could use our simple class to print the doughnuts. First, add a constant simpleClass
, and an initializer to run it.
class PrintingDoughnuts{ let simpleClass = SimpleClass() init() { simpleClass.printArray() } }
If we try running this code, with PrintingDoughnuts()
we get an error:
fatal error: unexpectedly found nil while unwrapping an Optional value
The protocol is set to nil
, and thus crashes. We still need to assign the data we want. First, we adopt the protocol,
class PrintingDoughnuts:SimpleClassDataSource{ let simpleClass = SimpleClass() init() { simpleClass.printArray() } }
Then we tell simpleClass
to find our implementation of array
inside PrintingDoughnuts
, by assigning self
to the data source.
class PrintingDoughnuts:SimpleClassDataSource{ let simpleClass = SimpleClass() init() { simpleClass.dataSource = self simpleClass.printArray() } }
Finally, we implement our version of array
class PrintingDoughnuts:SimpleClassDataSource{ let simpleClass = SimpleClass() init() { simpleClass.dataSource = self simpleClass.printArray() } func array() -> [String] { return ["Boston Cream","Cruller","Old Fashioned","","Frosted"] } }
I just assigned a literal array here, but I could have just as easily called an external XML File to download data, or calculated some values. I can do anything here that returns a string array. That’s the power of a data source: While the class does not change I can do anything I want with it.
If I go and run this code by PrintingDoughnuts()
, I get this output:
Boston Cream Cruller Old Fashioned Empty String Frosted
You can think of data sources setting properties otherwise inaccessible by coding, because someone made it impossible to get at the property directly.
Adding Delegates
While data sources are for properties, delegates are for methods. If you want to do something because of a change in your values or with your data you find those in a delegate. The developer of SimpleClass
for example might use a delegate to change string
and to react to a blank string in the array. The protocol for that might look like this:
protocol SimpleClassDelegate{ func format(string:String)->String func handleEmptyString() }
The SimpleClass
would use it like this:
class SimpleClass{ var dataSource:SimpleClassDataSource! = nil var delegate:SimpleClassDelegate! = nil func printArray(){ let array:[String] = dataSource.array() for string in array { if string.characters.count != 0{ print(delegate.format(string: string)) }else{ delegate.handleEmptyString() } } } }
Line two creates an optional property called delegate
with type SimpleClassDelegate
and sets it to nil
. Lines 8 and 10 use the methods of delegate
. Again we don’t know what those methods are yet. That happens in the class that adopts the protocol.
Just like the data source, we’d implement the adoption of the protocol like this:
class PrintingDoughnuts:SimpleClassDataSource, SimpleClassDelegate{ let simpleClass = SimpleClass() init() { simpleClass.dataSource = self simpleClass.delegate = self simpleClass.printArray() } // data source func array() -> [String] { return ["Boston Cream","Cruller","Old Fashioned","","Frosted"] } // delegates for self func format(string: String) -> String { return "Yummy " + string + " Doughnut" } func handleEmptyString() { print("Waaah!! No Doughnut!") } }
Like the data source, adopt the protocol in line 1. Tell delegate
in line 6 that the delegate methods can be found in this class. Lines 13 through 19 are those delegate methods.
Our output changes:
Yummy Boston Cream Doughnut Yummy Cruller Doughnut Yummy Old Fashioned Doughnut Waaah!! No Doughnut! Yummy Frosted Doughnut
External Implementation of a Protocol
One more thing about line 5 and 6. We set dataSource
and delegate
to self
. In most cases, this is what you will do, because you want to write the required methods in the same class as you use them. However, this is not always the case. If you will use the same methods for multiple classes you may make a separate class like this:
class DoughnutDelegateMethods:SimpleClassDelegate{ // delegates func format(string: String) -> String { return "Very Yummy " + string + " Doughnut" } func handleEmptyString() { print("**Waaah!! No Doughnut!**") } }
We’ve adopted the protocol, and implement the required methods in this separate class with a few changes to tell the difference.
We can change line 6 of PrintingDoughnuts
to this:
simpleClass.delegate = DoughnutDelegateMethods
Now the delegate is reading the methods from the DoughnutDelegateMethods
class:
Very Yummy Boston Cream Doughnut Very Yummy Cruller Doughnut Very Yummy Old Fashioned Doughnut **Waaah!! No Doughnut!** Very Yummy Frosted Doughnut
We can now use the same delegate method in another class for Firecakes Doughnuts, an artisan doughnut shop in Chicago:
class PrintingFirecakes:SimpleClassDataSource{ let simpleClass = SimpleClass() init() { simpleClass.dataSource = self simpleClass.delegate = DoughnutDelegateMethods() simpleClass.printArray() } // data source func array() -> [String] { return ["Triple Valrhona Chocolate Cake","Maple Glazed Pineapple & Bacon","Butterscotch Praline","","Malted Milk Ball"] } }
This time we only adopted the data source.We only need to adopt the delegate when writing the delegate methods. For our delegate we told the system to find it in the class DoughnutDelegateMethods
. Since they are not in this class we don’t adopt them. Running the PrintingFirecakes()
in a playground gets us a list of these doughnuts too:
Very Yummy Triple Valrhona Chocolate Cake Doughnut Very Yummy Maple Glazed Pineapple & Bacon Doughnut Very Yummy Butterscotch Praline Doughnut **Waaah!! No Doughnut!** Very Yummy Malted Milk Ball Doughnut
How Apple’s API’s Use This
With that look under the hood of API delegates and data sources, we can look at what you need to do for any API that has delegates. Some have required delegates, such as the UITableView
, UIImagePickerController
and CLCoreLocationManager
classes. In Swift, UITableview's
delegate and data source are so important, Apple bundled them into the Swift version of the class, while you do have to adopt them in Objective-C.
In a non working example lets look at CLCoreLocationManager
. For an API delegate or data source we have three things to do
- Adopt the delegate
- Set the location of the delegate’s method implementation, usually
self
to say “within this class” - Implement the delegate methods
Here’s a fragment of code to show what it would look like. By now you should understand what Apple is doing. All of the methods that return values from the GPS are delegate methods found in CLLocationManagerDelegate.
When one of those events occurs, that delegate method gets called. If you want to do something at that event you implement that delegate method.
/* // // This shoud really be run on a device, not the playground // commented out as a non working example, which is missing functional parts import MapKit // #1- adopt the delegate class WhereAmI:NSObject,CLLocationManagerDelegate{ let myLocationManager = CLLocationManager() override init(){ super.init() // #2 - set the delegate implementation to this class myLocationManager.delegate = self } //#3 - Use delegate functions func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let myLocation = locations.first print ("my latitude = \(myLocation?.coordinate.latitude)") print ("my longitude = \(myLocation?.coordinate.longitude)") } } WhereAmI() */
The Whole Code
This was an under the hood explanation of something developers use often. You still might want to play with the code I wrote. You can cut and paste the code below into an Apple playground, or click here to run the code in the IBM swift sandbox.
//: //: An example of API use of delegates and data sources //: (c)Steven Lipton (makeapppie.com) July 2016 //: See http://makeapppie.com for explanation import UIKit // //: A data source protocol // protocol SimpleClassDataSource{ func array() -> [String] } // //: A delegate protocol // protocol SimpleClassDelegate{ func format(string:String)->String func handleEmptyString() } // //:A simple class that uses the protocols // class SimpleClass{ var dataSource:SimpleClassDataSource! = nil var delegate:SimpleClassDelegate! = nil func printArray(){ let array:[String] = dataSource.array() for string in array { if string.characters.count != 0{ print(delegate.format(string: string)) }else{ delegate.handleEmptyString() } } } } // //: The first example class that uses SimpleClass // class PrintingDoughnuts:SimpleClassDataSource, SimpleClassDelegate{ let simpleClass = SimpleClass() init() { //If we want to implement the protocol in this class, we use self simpleClass.dataSource = self simpleClass.delegate = self //If we want to implement the protocol in an external class //simpleClass.delegate = DoughnutDelegateMethods() simpleClass.printArray() } // data source implementation func array() -> [String] { return ["Boston Cream","Cruller","Old Fashioned","","Frosted"] } // delegate implementations func format(string: String) -> String { return "Yummy " + string + " Doughnut" } func handleEmptyString() { print("Waaah!! No Doughnut!") } } // //: An external implementaion of the delegate methods, used //: when sharing the implementaion between classes // class DoughnutDelegateMethods:SimpleClassDelegate{ // delegates func format(string: String) -> String { return "Very Yummy " + string + " Doughnut" } func handleEmptyString() { print("**Waaah!! No Doughnut!**") } } // //: An eaxmple of a second method sharing the delegate implementation // class PrintingFirecakes:SimpleClassDataSource{ let simpleClass = SimpleClass() init() { simpleClass.dataSource = self simpleClass.delegate = DoughnutDelegateMethods() simpleClass.printArray() } // data source func array() -> [String] { return ["Triple Valrhona Chocolate Cake","Maple Glazed Pineapple & Bacon","Butterscotch Praline","","Malted Milk Ball"] } } let doughnutList = PrintingDoughnuts() let fireCakeList = PrintingFirecakes() /* // //: An example of CLLocationManager using delegates //: This shoud really be run on a device, not the playground //: commented out as a non working example, which is missing functional parts // import MapKit //: #1- adopt the delegate class WhereAmI:NSObject,CLLocationManagerDelegate{ let myLocationManager = CLLocationManager() override init(){ super.init() //: #2 - set the delegate implementation to this class myLocationManager.delegate = self } //: #3 - implement delegate functions func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { let myLocation = locations.first print ("my latitude = \(myLocation?.coordinate.latitude)") print ("my longitude = \(myLocation?.coordinate.longitude)") } } let whereAmI = WhereAmI() */
Leave a Reply