In the first tutorial in this series we introduced classes. In the second we did some more advanced manipulations of classes. In both, we discussed the idea of inheritance, which has one final possibility: a class whose only purpose is to define other classes. This is known as an abstract class.
Abstract classes are mere skeletons of their child classes. The abstract class gives some structure, but no function. The subclasses flesh out the differences between classes for different functionality.
In these lessons we’ve been using pizzas. All pizzas have a size, a crust and toppings. We might want to know the area of almost any pizza. How different types of pizzas use those properties and method changes dramatically. I charted out a few types of pizza. A flat round pizza is different from a rectangular pizza and a flatbread pizza. All of those are different from pan pizzas, where we aren’t interested in area, but volume. All of those are different from Bowl pizzas. Yet, they all have toppings, size and crust. To make subclasses of our abstract pizza, we change how we define and use those three properties.
Lets look at all these pizzas in an example in the playground. If you have followed along with the previous lessons, we will be starting from scratch, though the code will look familiar. For those picking up this lesson, as long as you understand the concepts from the first two lessons you do not have to catch up in coding.
In Xcode, make a new playground named AbstractPizzaPlayground. Clear out what’s in the playground and add the following code.
import UIKit class AbstractPizza { //our Abstract pizza Class let pizzaPi = 3.14 var size:Double! = nil var crust:AnyObject! = nil var topping:[String]! = nil func area() -> Double! { return nil } }
This is a simple class with the properties size
, crust
and topping
. It has one constant pizzaPi
for pi. It has one method for area
. While that alone is not remarkable, everything is optional and set to nil
. This is an empty and useless class by itself. We made sure anything using this class knows how empty it is by making everything optional. You don’t have to make it optional, but it is a good practice. In Apple’s APIs like UIKit
, you find a lot of things that are optional that don’t need to be. This is why – so you can tell easily if the class is empty and needing some populating.
We have an empty function area
. The method area
will return nil
if you don’t implement it. In any subclass, you have to override the parent class’ implementation of the method to get it to work correctly for that subclass.
There’s one more common practice in abstract classes in Swift and in Objective-C. Change the code to the following:
class AbstractPizza { //our Abstract pizza Class let pizzaPi = 3.14 var size:Double! = nil var crust:AnyObject! = nil var topping:[String]! = nil init(){ //use an empty method to initialize values pizzaDidLoad() } func area() -> Double! { return nil } func pizzaDidLoad(){ //empty method for initialization } }
We initialized the class manually by calling a method pizzaDidLoad
, which here is empty. I named this like a very familiar method in UIKIt
intentionally: viewDidLoad
. All of the life cycle methods of a view controller get defined like this as empty methods. This is a Objective-C throwback, but still handy. Unlike Swift, in Objective-C you cannot initialize with a value when declaring a property. This is how you initialize. Upon initialization of a view controller, the code in the initializer will run the viewDidload
method. If empty, it does nothing. If not, it does the preliminary steps for setting up the view controller by initializing some values. Its counterpart viewWillAppear
does the same further along the presentation of a view controller.
Subclassing the Abstract Class: FlatPizza
Let’s add a subclass from the abstract class. Under the AbstractPizza
code, add a flat pizza class like this:
// FlatPizza -- demonstrating the basic subclassing of a abstract class class FlatPizza:AbstractPizza{ //flat round pizza //define area override func area() -> Double! { let radius = size / 2.0 return radius * radius * pizzaPi } //set default values for properties override func pizzaDidLoad() { size = 10.0 crust = "White" topping = ["Pizza Sauce","Cheese"] } }
In FlatPizza
, we inherited all the properties and methods from AbstractPizza
. First we implemented the area method. Secondly, we set some default values in pizzaDidLoad
. While this was required in Objective-C, It is not necessary in Swift, since we could do the same by initializing a variable or in a initializer. It still comes in handy when you don’t want to override init
.
Test our code. Add this to the bottom of the playground and see the results:
Using Optional Chaining: PanPizza
Next let’s add a Pan Pizza:
//PanPizza -- demonstrate adding a property and methods with optional chaining class PanPizza:AbstractPizza{ var depth = 0.0 override func area() -> Double! { if let currentSize = size{ let radius = currentSize / 2.0 return radius * radius * pizzaPi }else{ return nil } } func volume() -> Double!{ if let area = area() { return area * depth }else{ return nil } } }
This time we did not initialize the properties with values. Instead, we use optional chaining to check if we have a value. Optional chaining assigns a constant to an optional value as a conditional statement. If the optional is nil
, the statement is false. If the statement is non-nil
, the value gets assigned to the constant. In the volume
function, we check if area
is nil
, if not we use the constant area to compute the volume from our added property for the depth of a pizza. If nil
, we return nil
.
The area
method uses optional chaining to check the value of size
. When using abstract methods, use optionals for the return values, so you can use checks such as optional chaining to decide if the method ever got written. Test out our pan pizza at the bottom of the playground:
Overriding Properties … or Not: RectPizza
Let’s try overriding a property. We might have a pizza that is a rectangle. For that, we will need a size that has a length and a width, not a diameter. Add the following class to the playground:
class RectPizza:AbstractPizza{ struct PizzaDimension{ var height:Double = 0.0 var width:Double = 0.0 } var size:PizzaDimension = PizzaDimention() }
We make a struct PizzaDimension
to allow us to have both a height and width, then try to change the type of size to the new type. This should work — but it doesn’t. We get an error:
Property 'size' with Type 'RectPizza.PizzaDimension' Cannot override a property with type 'Double'
You cannot override the type of an inherited property. The easiest solution is make a new one. Change the class to this:
// Demonstring overriding properties, or not... class RectPizza:AbstractPizza{ struct PizzaDimension{ var height:Double = 0.0 var width:Double = 0.0 } //this does not work -- we cant overrride type of a property //var size:PizzaDimension = PizzaDimension() //added property var sizeRect = PizzaDimension() override func area() -> Double! { return sizeRect.height * sizeRect.width } override func pizzaDidLoad() { sizeRect.height = 10.0 sizeRect.width = 12.0 crust = "Wheat" topping = ["Pizza Sauce","Cheese","Mushroom"] } }
We added the property, then overrode the pizzaDidLoad
and area
. methods. Test this with
let rectanglePizza = RectPizza() rectanglePizza.area()
and we get this.
This is the simplest way of changing a property — make a new one. There is a more complex way in Swift, called generics, but that is a lesson for another time.
Subclassing a Subclass: FlatBreadPizza
Flatbread pizzas are getting popular. These are a circle of dough flattened in one direction, to make a long rectangular shape with rounded ends, as you can see in the illustration. To find the area of these pizzas, we can think of them as a circle and a smaller rectangle added together. To make the class for this pizza, we do not use the abstract class, but subclass
RectPizza
, since every property we need is there already. Add this code:
class FlatBreadPizza:RectPizza{ override func area() -> Double!{ let rect = (sizeRect.height - sizeRect.width) * sizeRect.width //area with round parts removed to make a rectangle let radius = sizeRect.width / 2.0 let circle = radius * radius * pizzaPi // the circle that remains return rect + circle } }
We can subclass any class, and often we may start with the abstract class as the base class, but subclass the child classes much like we did here. You don’t have to make everything from the abstract class.
AnyObject in Class Methods: BowlPizza
Another pizza is the bowl pizza or known by its inventors as the Pizza Pot Pie. Essentially you put all the pizza toppings in a bowl, put the pizza dough over the top of the bowl and bake this pizza. When done, you turn the bowl upside down and remove the bowl. We don’t need an area here. Like a pan pizza, we need a volume. We can estimate the volume by the half the volume of a sphere, which is a bowl. The formula is
2/3*pi*r^3
for half a sphere. If you’ve followed along, adding a BowlPizza
class is pretty easy at this point. We want a class that looks like this:
class BowlPizza:AbstractPizza{ func volume() -> Double!{ if let bowlSize = size { let radius = bowlSize / 2.0 return radius * radius * radius * pizzaPi * (2.0/3.0) //volume of half a sphere } else { return nil } } }
That was simple enough. We have not added a class method to our abstract class. Let’s add the following to AbstractClass
for personal pizzas:
class func personalPizza() -> AbstractPizza!{ let pizza = AbstractPizza() pizza.size = 10.0 pizza.topping = ["Pizza Sauce","Cheese"] pizza.crust = "White" return pizza }
This allows us to make an AbstractPizza
from the class and for all other pizzas to inherit it. But there is a problem. Test the BowlPizza
like this:
let bowlPizza = BowlPizza.personalPizza() bowlPizza.volume
We get an error:
'AbstractPizza' does not have a member named 'volume'
There error says there is no method volume
to call. The volume method is in BowlPizza
. The class method only returns AbstractPizza
because we told it to, so it’s not a BowlPizza
but an AbstractPizza
. We need it to not return an AbstractPizza.
This is what the special type AnyObject
is for: it returns something, but does not tell us what type. In the class method change AbstractPizza
to AnyObject!
class func personalPizza() -> AnyObject!{
Now in BowlPizza,
override the class method
override class func personalPizza() -> AnyObject! { let pizza = super.personalPizza() as! AbstractPizza let bowlPizza = BowlPizza() bowlPizza.size = pizza.size bowlPizza.topping = pizza.topping bowlPizza.crust = pizza.crust return bowlPizza }
We did not return a BowlPizza
here, but an AnyObject!
. We define our class method like this:
override class func personalPizza() -> AnyObject!
We might subclass this class and have the same problems we had with AbstractClass.
Whenever we return our class, using AnyObject
makes it easier for the subclass to deal with the override.
This approach does add some code. Swift cannot guess what an AnyObject
is. We have to downcast the AnyObject
with the as!
operator to the class we need. For example in the code above
let pizza = super.personalPizza() as! AbstractPizza
In case you are wondering, we can’t downcast an AnyObject
to a BowlPizza
in this example. The AnyObject
was an AbstractPizza
instance, and not a BowlPizza
. AnyObject
and as!
don’t change classes. AnyObject
is a way to handle objects that might have type problems while sending them between other objects. In table views and collection views, the API returns cells by methods like cellforRowAtIndexpath
as an AnyObject
. This allows us to have custom cells and Swift does not care what we put in them until we need them, and then we downcast with as!
Test our code by changing the test code to
let bowlPizza = BowlPizza.personalPizza() as! BowlPizza bowlPizza.size bowlPizza.volume()
Once again we downcast before we use the object. We have a size of 10, and a volume of 261.66
Protocols: FoodPriceProtocol
There are times we need abstraction not between parent and child classes but all classes. That is what protocols are for. At the top of your code, just below the import UIKit
add this
protocol FoodPriceProtocol{ price(pricePerUnit:Double) -> Double }
This does nothing more than declaring there is a protocol called FoodPriceProtocol
. If any class uses this protocol, it must implement the methods listed in the protocol. However, we implement the protocol in the context of the class. This makes for a function that will work the same way for every class, but give appropriate results.
We use the protocol by adopting it. To adopt it we add the protocol after the superclass, if there is one. If not, we declare it where the superclass usually goes. Change FlatPizza
‘s declaration to this:
class FlatPizza:AbstractPizza,FoodPriceProtocol{ //flat round pizza
The protocol will demand you add price
to your class with the message.
Type 'FlatPizza' does not conform to protocol 'FoodPriceProtocol'
Add the following to the FlatPizza
class
//protocol func price(pricePerUnit: Double) -> Double { return area() * pricePerUnit }
You can now test the protocol by typing at the bottom of the playground
Adopt the protocol for the BowlPizza.
class BowlPizza:AbstractPizza,FoodPriceProtocol{
You don’t have to use the same formula for all adoptions of a protocol. For example, Bowl pizzas only come in half or pound sizes, so change price
to this.
func price(pricePerUnit: Double) -> Double { if pricePerUnit == 1.0 { return 23.50 } else if pricePerUnit == 0.5 { return 11.75 } else { return 0 } }
Now bowl pizzas will only give a price for a pound or half-pound bowl of two price per units, and otherwise return zero.
Data Sources and Delegates: ChocolateChipCookie
We don’t even need a class based on AbstractPizza
to use the protocol. Make the class ChocolateChipCookie
with a price computed by the number of of chocolate chips.
class ChocolateChipCookie:FoodPriceProtocol{ var chipCount = 0 func price(pricePerUnit: Double) -> Double { return price pricePerUnit * Double(chipCount) } }
Data Sources
Because protocols are so easy to use in any class, there are two special uses for them: Data Sources and Delegates. A data source is protocol with a series of methods describing how to get and use some data. For example add this data source protocol above ChocolateChipCookie
protocol ChocolateChipCookieDataSource{ func numberOfChipsPerCookie() -> Int func bagOfCookiesCount() -> Double }
We added two values and methods. One, numberOfChipsPerCookie
,will be the amount of chocolate chips, essentially replacing the chipCount
property. The other is how many cookies are in a bag of cookies. Change ChocolateChipCookie
code to this
class ChocolateChipCookie:FoodPriceProtocol,ChocolateChipCookieDataSource{ var chipCount = 0 func price(pricePerUnit: Double) -> Double { //return price pricePerUnit * Double(chipCount) return pricePerUnit * Double(numberOfChipsPerCookie()) } func numberOfChipsPerCookie() -> Int { return 15 } func bagOfCookiesCount() -> Double{ return 10 } }
We adopted the data source, and used the the required method from the protocol to have 15 chocolate chips per cookie and 10 cookies per bag. While we can’t force a developer to set data with a property, we can with a data source. Classes like UITableViewController
use this to the fullest to control data going in and out. Many times the data in a data source may have a lot of conditional changes in each subclass, and the protocol makes it easy to write code to control the data. For example we can change bagOfCookiesCount
to this:
func bagOfCookiesCount() -> Double { //return 10.0 //get more cookies differnt days of the week let now = NSDate.timeIntervalSinceReferenceDate() let dayInSeconds = 86400.0 let day = Int(now / dayInSeconds) % 7 return Double(day) + 5.0 //five plus however many cookies for the day }
Now the number of cookies changes depending on the day of the week. While this is a contrived example, many times our data will change due to conditions of our app. For a real example, check out the continuous picker view tutorial. In that example we mess with the indexes of an array in the pickerView:titleforRow:forComponent
method of the data source to make the picker view seemingly loop forever.
Delegates
A delegate is a series of methods used in one class but available for any class to use the original class functionality. The protocol FoodPriceProtocol
is actually a delegate. We could write another method for the cookie like this:
func bagOfCookiesPrice(cookieCount:Double) -> Double{ return price(0.05) * cookieCount }
This is where delegates find their use. They make methods when that will be common among a bunch of classes but defined differently each time. We know they will return a certain type. The function price
will return a double giving us a price. I know any class that implements price
will return a Double
. I can add this method to ChocolateChipCookie
:
func bagOfCookiesPrice() -> Double{ let pricePerUnit = 0.05 let pizza = FlatPizza() let discount = (pizza.price(pricePerUnit) * 0.10) return discount * price(pricePerUnit) * bagOfCookiesCount() }
We make the price of a bag of cookies have a discount of 10% of a price of a default flat pizza. In discount we used price
with pizza
, and returned price
of the cookie multiplied by the discount and quantity. We used price
on two different classes that really don’t have anything to do with each other.
This can get sophisticated. The most important use of a delegate is delegation between view controllers. This allows us to move values from a destination view controller back to its calling controller. I’ve written two posts on that subject. Take a look at Why Do We Need Delegates and Using Segues and Delegates in Navigation controllers for more on this.
Classes are the core of object oriented programming in languages like Swift. Knowing how to use methods and properties, how to subclass and use abstract methods make for a well structured easy to read and debug app. Steve Jobs gave the best metaphor for using classes properly: Your code should be as modular as Lego building blocks: they just fit together.
The Whole Code
Since WordPress won’t let me add code files directly, I had to make the download of the code a .docx file. Open AbstractPizzaDemo in Microsoft Word or a compatible word processor and cut and paste the source code into the playground if you wish. Though at that point, you could cut and paste the code below into a blank playground.
//The AbstractPizza example 8/4/2015 makeapppie.com import UIKit // A simple protocol, actually a delegate protocol FoodPriceProtocol{ func price(pricePerUnit:Double) -> Double } //The abstract pizza base class class AbstractPizza { //our Abstract pizza Class let pizzaPi = 3.14 var size:Double! = nil // Nil helps indicate empty var crust:AnyObject! = nil var topping:[String]! = nil init(){ //use an empty method to initialize values pizzaDidLoad() } class func personalPizza() -> AnyObject!{ //AnyObject helps subclass let pizza = AbstractPizza() pizza.size = 10.0 pizza.topping = ["Pizza Sauce","Cheese"] pizza.crust = "White" return pizza } func area() -> Double! { //Abstract functions often return nil return nil // to indicate empty } func pizzaDidLoad(){ //empty method for initialization } } // FlatPizza -- demonstrating the basic subclassing of a abstract class //Also demonstrates protocol adoption class FlatPizza:AbstractPizza,FoodPriceProtocol{ //flat round pizza //define area override func area() -> Double! { let radius = size / 2.0 return radius * radius * pizzaPi } //set default values for properties override func pizzaDidLoad() { size = 10.0 crust = "White" topping = ["Pizza Sauce","Cheese"] } //protocol func price(pricePerUnit: Double) -> Double { return area() * pricePerUnit } } //PanPizza -- demonstrate adding a property and methods with optional chaining class PanPizza:AbstractPizza{ var depth = 0.0 override func area() -> Double! { if let currentSize = size{ let radius = currentSize / 2.0 return radius * radius * pizzaPi }else{ return nil } } func volume() -> Double!{ if let area = area() { return area * depth }else{ return nil } } } // Demonstring overriding properties, or not... class RectPizza:AbstractPizza{ struct PizzaDimension{ //a struct for the height and width of a rectangle var height:Double = 0.0 var width:Double = 0.0 } //this does not work -- we cant overrride type of a property //var size:PizzaDimension = PizzaDimension() //added property sizeRect var sizeRect = PizzaDimension() override func area() -> Double! { return sizeRect.height * sizeRect.width } override func pizzaDidLoad() { sizeRect.height = 10.0 sizeRect.width = 12.0 crust = "Wheat" topping = ["Pizza Sauce","Cheese","Mushroom"] } } //Subclassing a subclass that used an abstract class class FlatBreadPizza:RectPizza{ override func area() -> Double!{ let rect = (sizeRect.height - sizeRect.width) * sizeRect.width //area with round parts removed to make a rectangle let radius = sizeRect.width / 2.0 let circle = radius * radius * pizzaPi return rect + circle } } //Demonstrating class methods and AnyObject! //Also Demonstrates protocol adoption class BowlPizza:AbstractPizza,FoodPriceProtocol{ //protocol adoption override class func personalPizza() -> AnyObject! { let pizza = super.personalPizza() as! AbstractPizza let bowlPizza = BowlPizza() bowlPizza.size = pizza.size bowlPizza.topping = pizza.topping bowlPizza.crust = pizza.crust return bowlPizza } func volume() -> Double!{ if let bowlSize = size { let radius = bowlSize / 2.0 return radius * radius * radius * pizzaPi * (2.0/3.0) //volume of half a sphere } else { return nil } } //Protocols (delegates and Data sources) func price(pricePerUnit: Double) -> Double { if pricePerUnit == 1.0 { return 23.50 } else if pricePerUnit == 0.5 { return 11.75 } else { return 0 } } } //An example of a data source protocol ChocolateChipCookieDataSource{ func numberOfChipsPerCookie() -> Int func bagOfCookiesCount() -> Double } //examples of delegates and data sources with a different class // no superclass here, so the protocols goes directly after the class declaration // If there is a superclass, like aboove, the superclass goes first in the list. class ChocolateChipCookie:FoodPriceProtocol,ChocolateChipCookieDataSource{ var chipCount = 0 func price(pricePerUnit: Double) -> Double { //return price pricePerUnit * Double(chipCount) return pricePerUnit * Double(numberOfChipsPerCookie()) } func numberOfChipsPerCookie() -> Int { //simple datasource which returns a value return 15 } func bagOfCookiesCount() -> Double { //datasource whihc calcualtes its value //return 10.0 //get more cookies differnt days of the week let now = NSDate.timeIntervalSinceReferenceDate() //get the number of seconds from refrence date to now let dayInSeconds:Double = 60 * 60 * 24 //convert to days let day = Int(now / dayInSeconds) % 7 // convert to day of week as 0 - 6 return Double(day) + 5.0 //five plus however many cookies for the day } func bagOfCookiesPrice(cookieCount:Double) -> Double{ //using the protocol method return price(0.05) * cookieCount } func bagOfCookiesPrice() -> Double{ //using the protocol method in two different classes let pricePerUnit = 0.05 let pizza = FlatPizza() let discount = (pizza.price(pricePerUnit) * 0.10) return discount * price(pricePerUnit) * bagOfCookiesCount() } } // test area let flatRoundPizza = FlatPizza() flatRoundPizza.area() flatRoundPizza.price(0.05) let panPizza = PanPizza() panPizza.size = 10.0 panPizza.depth = 2.0 panPizza.volume() let rectanglePizza = RectPizza() rectanglePizza.area() let bowlPizza = BowlPizza.personalPizza() as! BowlPizza bowlPizza.size bowlPizza.volume() bowlPizza.price(1.0) bowlPizza.price(0.5) bowlPizza.price(1.1) let cookie = ChocolateChipCookie() cookie.price(0.05) cookie.bagOfCookiesPrice(5)
Leave a Reply