In our first part we looked at some of the basics of classes in Swift. In this part, we look at some of the more advanced features of classes which developers use every day — often without knowing it. We’ll explore first a bit more on class methods, scope, keeping others from messing with your properties and methods, property observers and computed properties
Set up the Playground
We’ll use the same playground we used in part one. I’ve created a zip file with three versions of the playground. One is the completed part one, the second is a cleaned up part one to use with this lesson, and the third is the completed part two. If you are in the mood to type or don’t want to download anything , you can create a new playground and add the following code to get started:
//: Playground - noun: a place where people can play // Part two of the classes lesson // This file is a cleaned up version of part one. // You may use this or part one for your lesson. import UIKit //************************** // a basic class for a pizza //*************************** class Pizza{ //MARK: Properties var diameter:Double = 0.0 var crust:String = "" var toppings:[String] = [] //MARK: Class Methods -- Constructors init(){} init(diameter:Double, crust:String, toppings:[String]){ self.diameter = diameter self.crust = crust self.toppings = toppings } //MARK: Methods func toppingsString()->String{ var myString = "" for topping in toppings{ myString = myString + topping + " " } return myString } func area() -> Double{ return diameter * diameter * M_PI } func price(costPerSquareUnit:Double)->Double{ return self.area() * costPerSquareUnit } } //***************************** // The subclass DeepDishPizza //***************************** class DeepDishPizza:Pizza{ // A subclass of Pizza with a pan depth. //price is computed by volume //MARK: Properties var panDepth:Double = 4.0 //MARK: Class Methods -- Constructors override init(){ super.init() } init(panDepth:Double){ super.init() self.panDepth = panDepth } init(diameter: Double, crust: String, toppings: [String], panDepth:Double) { super.init(diameter: diameter, crust: crust, toppings: toppings) self.panDepth = panDepth } //MARK: Instance Methods func volume() -> Double{ return area() * panDepth } override func price(costPerSquareUnit: Double) -> Double { return volume() * costPerSquareUnit } func price(costPerSquareUnit:Double, panDepth:Double) -> Double{ self.panDepth = panDepth return volume() * costPerSquareUnit } }
More on Class Methods: Type Methods
Lat time we worked with Swift’s constructors to initialize an object. Some languages use the same syntax for constructors and other class methods. Swift has separate syntax for constructors and class methods, which Apple refers to as Type Methods. These methods, like constructors, do not need an instance to run. They have two big uses: return something related to the method and preset initialized instances of the class. One place you see this used often is the UIKit
class UIColor
. While UIColor
has several constructors for making colors, often we just want black, orange, yellow or blue. UIColor
comes with several type methods to do this:
//UIColor UIColor.blackColor() UIColor.orangeColor() UIColor.yellowColor() UIColor.blueColor()
To make a class method is not much different from an instance method. For example, to the Pizza
method add this code:
//MARK: Class Methods -- Type Methods class func pizzaIcon() -> String{ //return something related to the class return "🍕" }
We might need the pizza emoji at times, which is not easy to get to. If you do not know how to get it in Xcode, press Control-Command-Spacebar to get the symbols menu. Navigate to the Emoji>Food section, and double-click a pizza. Just in making this class method you see how handy such a method can be. Note the only difference in this method from an instance method. We use the keyword class
in front of the method declaration func
to declare it a class method. Try to keep type methods together in code, so set up a //MARK:
to document in XCode.
At the bottom of the playground, try out the method:
Pizza.pizzaIcon()
And a pizza emoji shows in response. Type methods use the class name instead of the instance name.
Suppose we make a lot of personal cheese pizzas, or we start many personal pizzas with a cheese pizza. We can write a class method which creates one those pizzas, saving us a lot of work, just like UIColor.blackColor()
. Add this under the pizzaIcon
method :
class func personalCheese() -> Pizza{ //use as a frequently used pizza return Pizza( diameter: 10.0, crust: "White", toppings: ["Cheese","Marinara"]) }
At the bottom of the playground, try out the method:
let cheesePizza = Pizza.personalCheese()
Our basic pizza shows up as an object. However, this has a big problem: it inherits, but the subclasses get confused. Try this as your last line of code in the playground:
let deepDishPizza = DeepDishPizza.personalCheese() deepDishPizza.panDepth
You get an error:
'Pizza' does not have a member named 'panDepth'
While Xcode allows you to use the type method personalCheese
here, it makes it type Pizza
, not type DeepDishPizza
The type method returns a type Pizza
object. One way of dealing with this is override the class. Add this to the DeepDishPizza
class:
//MARK: Type Methods override class func personalCheese() -> DeepDishPizza{ let deepDish = DeepDishPizza(panDepth: 2.0) //Type DeepDishPizza let flat = Pizza.personalCheese() //Type Pizza //transfer from flat to deep dish properties we need deepDish.diameter = flat.diameter deepDish.crust = flat.crust deepDish.toppings = flat.toppings return deepDish }
The override will return a DeepDishPizza
. It first makes a deep dish and a personal cheese pizza. Then we transfer the properties of a flat pizza to the deep dish. This keeps the personal pizzas consistent. Any time we change Pizza
, DeepDishPizza
will change with it. Then we return the deep dish pizza. Now delete the last two test lines and type them in again to reset the playground and run the new code.
let deepDishPizza = DeepDishPizza.personalCheese() deepDishPizza.panDepth
The error disappears, and we get our panDepth
for the deepDishPizza
.
Scope for Classes: self and super
In the last post I mentioned self
and super.
Let’s look at that code a little more. These two keywords are ways of explicitly referencing a class within a class. For example we have this function in DeepDishPizza
from last time
func price(costPerSquareUnit:Double, panDepth:Double) -> Double{ self.panDepth = panDepth return volume() * costPerSquareUnit }
The function has a parameter named panDepth
. We also have a property panDepth
. A
function or code block indicated by braces {}
always thinks as local as possible. The identifier panDepth
is the parameter panDepth
as far as the price
method is concerned, not the property. We want to assign the parameter to the property. We reference the property by the keyword self
, which means “this class instance” The code self.panDepth = panDepth
means assign to the property value the value of the parameter.
In the old days of Objective-C, every method and property in the class required a reference of self
when used in the class. Swift assumes a lot more about your code. If you leave off self
it assumes the local scope, which is usually the class. You don’t normally need to use self
. In a few cases like conflicting identifiers and closures, it cannot figure that out, and you need to specify self
.
We also had a constructor in DeepDishPizza
that looked like this:
init(panDepth:Double){ super.init() self.panDepth = panDepth }
The second line is super.init().
This initializes everything that was in the parent class before we initialize the properties of the child class. We have two constructors named init()
, one in DeepDishPizza
and one in Pizza
. DeepDishPizza
inherits the init
method from the parent class Pizza
but also assumes scope is local, so init()
refers the init()
in DeepDishPizza
. The keyword super
tells the compiler that we want the inherited init()
from the superclass Pizza
.
You rarely use super
. Most cases are when a class and the subclass use the same identifier. Most common is in init()
or other constructors. Another place it can show up is initializing abstract functions like viewDidLoad
. We’ll discuss those in our next lesson.
Public and Private
We know how to access properties and methods of our parent. We know how to access properties and methods in instances of a class and in class methods. Up to this point in our lesson, we’ve assumed every property and method is public and every property and methods are accessible. In Swift the properties and methods are by default public to the target it is in. If you have two targets, say a WatchOS app and an iOS app, they cannot see each other. Having stuff public is not always a good idea. If we do some calculations, we do not want the middle steps of the calculation to be accessible by other classes or other developers. We want them invisible and inaccessible to anything outside the class or subclass. It keeps from having to search for bugs due to messing with an intermediate calculation.
Instead of using a highly accurate value for pi, let’s use the less accurate 3.14. Add the following to your code for the Pizza
class:
private let pi = 3.14
The keyword private
keeps pi hidden from the outside. What we mean for “outside” is different in Swift than most languages. In most computer languages private
or its equivalent makes the property private to the class. In Swift it makes it private the source code file. Unfortunately, using a Apple Playground means everything is in the same source file so we can’t test this. If we could, we’d find that when we made an instance of Pizza
, we could not do the following without an error:
cheesePizza.pi
It’s a good idea to keep calculations private. Add another private property to Pizza
:
private var pizzaArea = 0.0
Change the method area
to this:
private func area(){ pizzaArea = diameter * diameter * pi }
What did this do? First we made a private property pizzaArea
. We then changed the area
method to be private, using our custom value of pi
. Instead of returning a value it assigns it value to the property pizzaArea
. By making the property and method private
, we hide it from anything but the class and subclasses.
There were functions which used our area method which we’ll have to modify for this change. In Pizza
change this:
func price(costPerSquareUnit:Double)->Double{ area() return pizzaArea * costPerSquareUnit }
Do the same for DeepDishPizza
‘s volume method:
func volume() -> Double{ area() return pizzaArea * panDepth }
Inheritance means pizzaArea
does exist in the subclass DeepDishPizza
. So does the changes in the definition of area
, so we made change in both places. Test out the code by using this:
cheesePizza.price(0.015) cheesePizza.pizzaArea deepDishPizza.price(0.015) deepDishPizza.volume()
and we get results we like:
Setters and Getters
Swift does many things automatically some other computer languages need a lot of work to do. One of the most important is setters and getters. Setters are methods where we assign a value to a property. Getters are methods where we get the value from the property. In Swift, most times we use the assignment operator =
to do this.
We can use custom setters and getters in Swift, often for computing values or performing logic when a property changes. Note how clunky having to add the area()
was for updating the property pizzaArea
. It would be handy to make the getter for the pizzaArea
do the work for us.
Let’s change area
again to return a value:
private func area() -> Double { return diameter * diameter * pi }
Now change the property pizzaArea
from this
private var pizzaArea = 0.0
to this
/* version 2 of pizzaArea -- Computed property */ private var pizzaArea:Double { get{ return area() } }
We remove the assignment operator and default value. Since we cannot infer the type, we add a type declaration. Then we have a block of code. In that block, we have a small function-like block of code starting with the keyword get
. For a getter, it must include a return of a value.
Since we are computing here, we have two redundant calls to area
. They don’t do anything now. You can comment them out, delete them or just ignore them. In real code if we used this getter, often referred to as a Computed Property, we would not code them at all.
If you look down at our test statements nothing has changed, which is what we want.
Usually we don’t use setters as much as Property Observers. Property Observers are a special type of setter that changes some other value or does some logic when a given property changes.
You might have noticed we made a math mistake. Area is not computed by the square of the diameter, but the radius. Let’s add a new private property radius
. When we set the diameter, we’ll divide the diameter by two to get the radius.
Change the line
var diameter = 0.0
to this
private var radius = 0.0 var diameter = 0.0{ //property observer didSet{ radius = diameter / 2.0 } }
When diameter
changes, radius
changes. The above code is the way to change a value after a property changes using didSet
without a parameter. It will use the property name as the set value. We can add a parameter as well. The same code can be written:
/* didSet with a parameter */ var diameter = 0.0{ //property observer after setting didSet(newDiameter){ //old value will be stored in oldValue radius = newDiameter / 2.0 } }
If you want to do something before the property is set, you can use willSet
. We can write the same code to compute the radius with willSet
this way:
var diameter = 0.0{ //property observer before setting willSet{ //newValue is value it will be set to //diameter is value before setting radius = newValue / 2.0 } }
In any of these cases, it’s important to remember the observer does nothing until the value changes. That means in initialization or assigning default values, these do nothing. Test the method with this:
let newPizza = Pizza(diameter: 10, crust: "White", toppings: ["Cheese"]) newPizza.diameter = 12.0
Since we used an initializer, you see in the results pane this:
At first, the diameter is 10 but the radius is 0. We initialized diameter
, so the property observer didn’t fire. When we change the diameter to 12, then the radius becomes 6. We don’t have that problem in the class method since it assigns the value 10 to the object:
let newerPizza = Pizza.personalCheese()
This code returns a radius of 5. We’ve got one more thing to do to make our prices accurate: use the radius. In Pizza
, change the area
method to
private func area() -> Double{ return radius * radius * pi }
Our areas, volumes and prices change:
I didn’t plan to make this more than one lesson, but apparently there is enough for three lessons. In our last lesson (I promise!!!) , we’ll explore the ways we can make an abstract class, one that is a mere skeleton, but can be flexible enough to make a lot of classes. We’ll discuss empty methods, protocols, delegates and data sources.
The Whole Code
You can download the zip file here:ClassesPlayground
You can also refer to this full version of the playground used. I commented out versions of methods as I changed them in the text above.
//: Playground - noun: a place where people can play // Part two of the classes lesson // This file is a cleaned up version of part one. // You may use this or part one for your lesson. print("Hello Pizza!") import UIKit //************************** // a basic class for a pizza //*************************** class Pizza{ //MARK: Properties private var radius = 0.0 /* didSet with a parameter var diameter = 0.0{ //property observer after setting didSet(newDiameter){ //old value will be stored in oldValue radius = newDiameter / 2.0 } } */ /* didSet using a parameter for new value var diameter = 0.0{ //property observer after setting didSet(newDiameter){ radius = newDiameter / 2.0 } } */ /* will set without parameter */ var diameter = 0.0{ //property observer before setting willSet{ //newValue is value it will be set to //diameter is value before setting radius = newValue / 2.0 } } var crust:String = "" var toppings:[String] = [] /* version 1 of pizzaArea private var pizzaArea = 0.0 */ /* version 2 of pizzaArea -- Computed property */ private var pizzaArea:Double { get{ return area() } } private let pi = 3.14 //MARK: Class Methods -- Constructors init(){ } init(diameter:Double, crust:String, toppings:[String]){ self.diameter = diameter self.crust = crust self.toppings = toppings } //MARK: Class Methods -- Type Methods class func pizzaIcon() -> String{ //return something related to the class return "🍕" } class func personalCheese() -> Pizza { //use as a special constructor // This works in all cases, including property observers. let aPizza = Pizza() aPizza.toppings = ["Cheese","Marinara"] aPizza.crust = "White" aPizza.diameter = 10.0 return aPizza } //MARK: Methods func toppingsString()->String{ var myString = "" for topping in toppings{ myString = myString + topping + " " } return myString } /* Version one of area() -- Starting private func area() -> Double { return diameter * diameter * M_PI } */ /* Version two of area() -- Adding private property private func area() { pizzaArea = diameter * diameter * pi }*/ /* Version three of area() -- Private methods private func area() -> Double { return diameter * diameter * pi }*/ /*version four of Diameter -- Property observers*/ private func area() -> Double{ return radius * radius * pi } func price(costPerSquareUnit:Double) -> Double{ area() //does nothing without assignment return pizzaArea * costPerSquareUnit } } //***************************** // The subclass DeepDishPizza //***************************** class DeepDishPizza:Pizza{ // A subclass of Pizza with a pan depth. //price is computed by volume //MARK: Properties var panDepth:Double = 4.0 //MARK: Class Methods -- Constructors override init(){ super.init() } init(panDepth:Double){ super.init() self.panDepth = panDepth } init(diameter: Double, crust: String, toppings: [String], panDepth:Double) { super.init(diameter: diameter, crust: crust, toppings: toppings) self.panDepth = panDepth self.diameter = diameter } //MARK: Type Methods override class func personalCheese() -> DeepDishPizza{ let deepDish = DeepDishPizza(panDepth: 2.0) //Type DeepDishPizza let flat = Pizza.personalCheese() //Type Pizza //transfer from flat to deep dish properties we need deepDish.diameter = flat.diameter deepDish.crust = flat.crust deepDish.toppings = flat.toppings return deepDish } //MARK: Instance Methods func volume() -> Double{ area() //does nothing without assignment return pizzaArea * panDepth } override func price(costPerSquareUnit: Double) -> Double { return volume() * costPerSquareUnit } func price(costPerSquareUnit:Double, panDepth:Double) -> Double{ self.panDepth = panDepth return volume() * costPerSquareUnit } } //Test statements // UIColor Type (class) methods UIColor.blackColor() UIColor.orangeColor() UIColor.yellowColor() UIColor.blueColor() //Test class (type) methods Pizza.pizzaIcon() //Test class methods to make a personal cheese let cheesePizza = Pizza.personalCheese() let deepDishPizza = DeepDishPizza.personalCheese() deepDishPizza.panDepth = 3.0 //Test for self -- no test //Test private methods -- but they are public in the same file. cheesePizza.pi cheesePizza.price(0.015) cheesePizza.pizzaArea deepDishPizza.price(0.015) deepDishPizza.volume() //Test methods for getter(Computed property) should not change the code above //Test methods for setters(Property observers) //initialization and assignment of property observers let newPizza = Pizza(diameter: 10, crust: "White", toppings: ["Cheese"]) newPizza.diameter = 12.0 let newerPizza = Pizza.personalCheese() //using the property observer newPizza.price(0.015)
Leave a Reply