LEARN SWIFT

Buy the PDF

Chapter 12 Classes

If you’ve programmed in any Object Oriented language, classes in Swift should be unsurprising.

Box 12.1. Classes and Structures

In this chapter we introduce classes. However any discussion of classes could also include a discussion of structures (and enumerations). In many programming languages, structures are just a way of grouping bits of related data together, whereas classes are ways of grouping related functionality and data together. However in Swift, structures have much of the same capabilities as classes. Enumerations also have similar capabilities. Structures in Swift can have methods that act on their properties, initializers for initializing the structure, they can conform to protocols and they can be extended past their default implementation. Thus structures have most of the same capabilities as classes. The main difference between structures and classes is that classes are a reference type whereas structures are a value type. I.e. when you pass an object around your program you are actually passing a reference to that object, so different parts of your program can share and modify your object. When you pass a structure around your program, what gets passes around is a copy of the structure. So modifications to structures don’t get shared.

Rather than get into this discussion in detail here we’ll defer it to chapter 13. Classes in Swift behave similarly to classes in many other languages and in this chapter we’ll show how classes work. We’ll get into comparing them to structures later.

12.1 Defining a class

We can define a class in Swift using the class keyword. We instantiate a new instance of a class using the class name followed by (). Classes can have properties and functions associated with them. A property is a value that is associated with an instance of a class and a method is a function that is associated with a class. A class is a way of grouping related data along with the methods that operate on them.

1 class Animal {
2     var species = ""
3 }
4 
5 let dog = Animal()
6 dog.species         // ""
7 dog.species = "dog"
8 dog.species         // "dog"

Here we create an Animal class with a property called species. We create a new Animal object at line 5 and change the value of the species property.

12.2 Methods

Methods are functions that are attached to classes and operate on the classes properties. We can define a method by defining a function within our class definition. That method is then available on objects of that class and has access to the properties of its object.

 1 class MethodCircle{
 2     var radius = 1.0
 3 
 4     func area() -> Double {
 5         return(3.14 * radius * radius)
 6     }
 7 
 8     func description() -> String{
 9         return("I am a Circle!")
10     }
11 }
12 var m = MethodCircle()
13 m.area()         // 3.14
14 m.description()  // "I am a Circle!"

Here we define a class with 2 instance methods. The syntax for defining methods is the same as for defining functions (and is described in more detail in chapter 11) - just that those functions are defined within the class definition. Thus they are attached to instances of the class and they have access to the classes properties. In this example, our MethodCircle class has a property called radius. It has an instance method called area, which returns a Double and an instance method called description which returns a String. Once we created a new instance of our class (line 12) we can call these methods using dot syntax (lines 13-14).

12.3 Properties: stored and computed

In Swift, classes can have 2 types of properties - stored properties and computed properties.

Stored properties are declared using var or let, and given an initial value. Computed properties are also declared with var, but instead of an initial value you assign a function to calculate the value. (By default, you just need a getter to calculate the value, but you can also create a setter to initialize the value). You must declare the type for computed properties.

Stored properties are stored as vars on the object, whereas computed properties are computed by defining a method to calculate the value.

 1 class Circle{
 2     var radius = 1.0
 3     var area:Double {
 4         return(3.14 * radius * radius)
 5     }
 6 }
 7 
 8 var c = Circle()
 9 c.area           // 3.14
10 c.radius = 2.5
11 c.area           // 19.625

In this example we define a class Circle that has a stored property (radius) and a computed property (area). We define the computed property by providing a closure that will calculate its value. They are basically methods that look like properties. Here we can see that when we change the value of our stored property radius, the value returned by our computed property area also changes.

Computed values and methods can look very similar. E.g. what’s the difference between a computed value and a method that does the same. In many cases it’s a matter of style or judgment. Use properties for things that are properties of the object and use methods for things do stuff. In our previous example we implemented a method to calculate the circles area, whereas in this example we implemented the area calculation as a computed property. For this example I prefer to use a property, as area seems more like a property of a circle and than related to functionality. One difference is that you can define a setter for a computed value. This is a method that sets up any properties that would affect the computed value.

Computed values can have both getters and setters. Our example above used a read-only computed value, so we didn’t need to explicitly declare the getter. Our next example explicitly declares both get and set methods for our computed variable.

 1 class Circle2{
 2     var radius = 2.5
 3     var area:Double {
 4         get {
 5             return(3.14 * radius * radius)
 6         }
 7         set(new_area) {
 8             radius = sqrt(new_area / 3.14)
 9         }
10     }
11 }
12 
13 var c2 = Circle2()
14 c2.area          // 19.625
15 c2.area = 3.14
16 c2.radius        // 1
17 c2.area = 19.625
18 c2.radius        // 2.5

In this example, the area property is calculated based on the radius property. Since we are providing both get and set in this example we define them explicitly using get (line 4) and set (line 7) in the method body. By also providing a set method (line 7) we can now set the area directly. Since our get for the area computed value depends on radius our set method calculates and sets the radius value based on the given area.

12.4 Lazy stored properties

You can declare stored properties as lazy if you want to defer their initialization. The value of an lazy property won’t be calculated until the first time it is accessed. For example, say you had a stored property performs a network call to get its initial value. Instead of having this happen when the object is initialized, you can have it happen when the property is first accessed by declaring the property as lazy. E.g.

...
lazy var shippingCosts = downloadShippingData()
...

Note that lazy properties must always be declared using var (as their value will changed when they are first accessed, so we can’t use let even if the value never changes after it’s initialized). Also, you can’t attach observers to lazy properties.

12.5 Property callbacks

We can also attach a function to a stored property and have it called whenever the value of that property is about to change. These are called property observers.

There are 2 observers that you can define: willSet gets called before the variable changes, and didSet gets called after the variable has changed.

 1 class ObservedCirlce {
 2     var radius:Double = 1.0 {
 3         willSet{
 4             print(Radius is changing from \(radius) to \(newValue).)
 5         }
 6 
 7     }
 8     var area:Double {
 9         return(3.14 * radius * radius)
10     }
11 }
12 
13 var oc = ObservedCirlce()
14 oc.radius = 2.5 // prints "Radius is changing from 1.0 to 2.5."
15 oc.area // 19.625

This example defines an ObservedCirlce class and attaches an observer to the radius property. We do this by providing a function called willSet when we define the property. When you run the above code in a playground and check the console output, you’ll see that every time we change the value of radius it prints the old value and the new value.

12.6 Subscripts

Subscripts allow you do define a way of allowing your class (or structure or enumeration) to be accessed using an index or key surrounded by square brackets. These are usually used as a shortcut for accessing members of a collection. More specifically it allows you to define a way of accessing elements of your class using square brackets after the name of the instance. For example, arrays have a subscript that lets you access elements in the array using an index that indicates the position in the array i.e. myArray[3]. Similarly dictionaries have a subscript that allows you to access elements in the dictionary using a key e.g. myDict["key"].

We can define a subscript function that takes a key and uses it to find and return the appropriate value. Similar to computed properties subscripts can be read-write, in which case we need to provide both a get and a set method, or read-only, in which case we can just provide a single method which Swift assumes is the get method.

Lets look at an example. Lets say we want to represent individual hands in a game of cards (We could better represent this using structures and enumerations but we’ll stick with the example to illustrate subscripts here).

1 class Hand{
2     var cards = ["Ah", "Ad", "Ac", "As", "7c"]
3     subscript(index: Int) -> String{
4         return cards[index]
5     }
6 }
7 var h = Hand()
8 h[3] // "As"

A hand is a collection of cards, which we’ve represented here using strings. First is a read-only version. In this example we can access the individual cards in the hand using an index. Since we’re not providing a set method, we can just provide a subscript method that takes and Integer and returns a string (line 3)

 1 class UpdatableHand{
 2     var cards = ["Ah", "Ad", "Ac", "As", "7c"]
 3     subscript(index: Int) -> String{
 4         get{
 5             return cards[index]
 6         }
 7         set(newVal){
 8             cards[index] = newVal
 9         }
10     }
11 }
12 var uh = UpdatableHand()
13 uh[4]          // "7c"
14 uh[4] = "Kc"
15 uh[4]          // "Kc"

If we want to extend this example to allow us to set values in the collection using an index, we can provide both a get and set method. The get method gets called when someone accessing a Hand instance using square bracket notation (line 13) and the set method gets called when someone assigns a value to an instance of Hand using square bracket notation (line 14). In this case the value that was assigned gets passed to the set method as a parameter newVal.

Subscripts are not limited to a single dimension. You can define multi-dimensional subscripts if you want to, i.e. myObject[0][2]["key"], by defining a subscript with the appropriate number of parameters.

12.7 Object Initialization

We can initialize properties by setting a default value when we define the class. In our Circle class above, we gave radius an initial value of 1.0. Then we changed the radius property to a different value. When we give a property an initial value in the class definition like this, all our Circle objects will have an initial radius of 1.0 when we create them and we’ll then have to change the value of that property if we want it to have a different value.

We could also initialize this value with an intializer. We create an initializer using the init keyword in the class definition. Initializers follow the same conventions as methods and functions for defining their parameters.

 1 class InitializedCircle{
 2     var radius:Float
 3 
 4     init(initialRadius:Float){
 5         radius = initialRadius
 6     }
 7     var area:Float {
 8         return(3.14 * radius * radius)
 9     }
10 }
11 var ic = InitializedCircle(initialRadius: 2.5)
12 ic.radius // 2.5
13 ic.area   // 19.625

In this example we add an initializer to our class definition. We can now use that to declare the initial value for our radius. We pass the radius value to the initializer by using the name we defined in the initializer definition as a key - i.e. in this example initialRadius: 2.5.

We can define multiple initializers for a class as long as each has a different type signature.

 1 class InitializedCircle2{
 2     var radius=1.0
 3 
 4     init(){}
 5     init(initialRadius:Double){
 6         radius = initialRadius
 7     }
 8     init(initialArea:Double){
 9         area = initialArea
10     }
11     var area:Double {
12         get {
13             return(3.14 * radius * radius)
14         }
15         set(new_area) {
16             radius = sqrt(new_area / 3.14)
17         }
18     }
19 
20 }
21 var ic2 = InitializedCircle2()
22 ic2.radius // 1.0
23 ic2.area   // 3.14
24 ic2 = InitializedCircle2(initialRadius: 2.5)
25 ic2.radius // 2.5
26 ic2.area   // 19.625
27 ic2 = InitializedCircle2(initialArea: 78.5)
28 ic2.area   // 78.5
29 ic2.radius // 5.0

In this example we have defined 3 different initializers (with 3 different type-signatures). The first takes no arguments and does nothing (line 4). When we initialize an object with no arguments, this initializer gets called and radius ends up with the default value of 1.0 (line 21). The second initializer takes an argument called initialRadius and uses it to initialize the radius to that value (line 5). This initializer can be used to create a new circle with a given radius (line 24). In this example we have defined area as a computed property with a getter and setter similar to our previous example. Since we have a setter for area we can also initialize our circle using a given area. We define this initializer at line 8. This initializer gets called when we call the initializer with the initialArea key (line 27).

12.8 Deinitialization

If you need some code to be run before your objects are destroyed, you can define a deinitializer. You do this using using the deinit and providing a block with no arguments. The deinit function will be called just before your object is destroyed. Note: deinit is only available for classes, not for structures or enumerations.

import Foundation

class MelancholicCircle{
    var radius = 1.0
    var area:Double {
    return(3.14 * radius * radius)
    }
    deinit{
        print("Goodbye Cruel World")
    }
}
var mc: MelancholicCircle?
mc = MelancholicCircle()
print("Radius is \(mc!.radius)")
mc = nil

This example shows a deinitilization method. Unlike all our other code, we won’t be able to see this working correctly just by entering it in a playground. The reason is that the playground may hold on to a reference to the object longer than it’s needed. Whenever we want to experiment with code related to memory management, we need to create a project to run the code rather than check it in a playground. If you create a command-line application, put the above code in main.swift and then run it from Xcode, you’ll see that “Goodbye Cruel World!” is printed after we assign nil to the var mc, which causes it to be deallocated. We’ll cover memory-management in more detail in chapter 17

12.9 Inheritance

Classes can inherit functionality from other classes. (Note: Only classes can inherit functionality from other classes. Inheritance doesn’t apply to structures and enumerations.) Subclasses in Swift can inherit and override properties (including adding new observers), methods and subscripts from a parent class.

 1 class Coder {
 2     var favouriteEditor = "vi"
 3     init() {}
 4     func startEditorWar() {
 5         print("Obviously \(favouriteEditor) is best")
 6     }
 7 }
 8 
 9 class Rubyist:Coder {
10     override init(){
11         super.init()
12         favouriteEditor = "TextMate"
13     }
14 }
15 
16 class Lisper:Coder {
17     override init(){
18         super.init()
19         favouriteEditor = "emacs"
20     }
21 }
22 
23 var r = Rubyist()
24 var l = Lisper()
25 r.startEditorWar() // Obviously TextMate is best
26 l.startEditorWar() // Obviously emacs is best

In this example we create a base class called Coder. It has a property with an initial value (line 2), an initializer that doesn’t do anything (line 3) and a method (line 4). We can inherit from this class when defining new classes by putting the name of the class we want to inherit from after a colon when defining new classes as shown in lines 9 and 16. We define 2 new classes than inherit from our base Coder class. Both of these override the empty initializer from the base class and use it to set a new value for the favouriteEditor variable. We do this by redefining the init method and using the override keyword (lines 10 and 17). You must use the override keyword when you want to redefine a method that you inherited. If you were to leave out the override keyword here you would get a compiler error. This prevents you from accidentally redefining a method in a subclass.

If we want to access a method from the parent class we can use the super keyword. E.g. in the initializers of our 2 subclasses we call the parent initializer before we change the value for favouriteEditor (lines 11 and 18).

We can’t override stored properties when declaring the base class. (Since they already exist in the base class and are inherited, there is no need to redeclare them. But we can change their value in an initializer as shown in the above example). However we can override their behaviour. For example, we could add observers to the property, or change the getter or setter if it’s a computed property.

 1 class PopulistCoder:Coder{
 2     override var favouriteEditor:String{
 3         didSet{
 4             print("I changed my mind, \(favouriteEditor) is the new hotness")
 5         }
 6     }
 7 }
 8 var pop = PopulistCoder()
 9 pop.startEditorWar()
10 pop.favouriteEditor = "Sublime" // prints "I changed my mind, ...
11                                 // ... Sublime is the new hotness"

This example inherits the favouriteEditor property from the Coder class, but it adds an observer to that property. We do this by overriding the property(line 2) and providing an observer function similar to how we would declare an observer on any property (line 3).

We can override inherited methods in a similar way i.e. by redefining them in the subclass and using the override keyword.

 1 class PeacefullCoder:Coder {
 2     override init() {
 3         super.init()
 4         favouriteEditor = "all of them"
 5     }
 6     override func startEditorWar() {
 7         print("They're all good")
 8     }
 9 }
10 
11 var p = PeacefullCoder()
12 p.startEditorWar() // prints "They're all good"

In this example we override the startEditorWar method in the subclass and replace it with a methods that is better suited to the subclasses behaviour.

12.9.1 Designated and convenience initializers

We’ve already looked at initializers, but once inheritence involved, initializers turn out to be a bit more complex than is obvious from the previous example. Swift has 2 kinds of initializers (for classes): designated initializers and convenience initializers. Designated initializers must initilize all properties in the class and also call an initializer from the superclass that will initialize properties inherited from superclass. Every class must have at least one designated initializer. Designated initializers are defined with init similar to how we defined our initializer previously. But when we are dealing with subclasses we have the added constraints that we must initialize all the properties defined in the class and call the superclass initializer.

Convenience initializers are defined similarly but are tagged with the convenience keyword. The main use of convenience initializers is to call a designated initializer with some default values. I.e. they are a way to declare a common initialization pattern, either for convenience or for readability.

12.9.2 Inheriting initializers

Subclasses don’t inherit initializers by default (But you can call them in your designated initializer user super). But, if your subclass doesn’t provide any designated initializers, then it will inherit all of the superclasses designated initializers. Following on from that, if your subclasses inherits all of its designated initializers from the superclass, it will also inherit all of the superclasses convenience initializers.

So to clarify, here are the rules when it comes to inheritance and initializers:

  • Designated initializers must call the designated initializer from the immediate superclass.
  • Convenience initializers must call another initializer from the same class
  • Convenience initializers must eventually call a designated initializer
  • Designated initializer must initialize all of the properties introduced by the class before then calling the superclass initializer
  • Designated initializer must call superclass initializer before it changes the value of an inherited property
  • Convenience initializer must call another initializer before it changes any property
  • An initializer can’t call any instance methods or read property values before all properties have been initialized

So in our previous example we overrode the designated initializer and changed the value of favouriteEditor. In that example, init() is the designated initializer for the class. Before we can change the value of that property, we have to call the designated initializer for its superclass (line 3).

12.9.3 Failable initializers

Sometimes you may want to have an initializer that can fail, depending on the state of other objects in the system or the values passed it. You can do this by defining an initializer with a ? at the end i.e. init?. You can then return nil within your init? method at any point where a failure occurs.

Failable initializers return an optional. Thus when your initialization succeeds you get an object of the class back and when initialization fails you get nil back. You can treat objects that are created with failable initializers the same way as you would any other optional.

12.9.4 required and final

You can use the required keyword for an initializer to indicate that all subclasses must implement an initializer with that type signature.

You can prevent a property or method from being overridden by using the final keyword.

class StubbornCoder:Coder{
    var declaration:String

    final override func startEditorWar() {
        print("\(declaration)! Emacs is obviously better")
    }

    required init(d:String){
        declaration = d
    }
}
var st = StubbornCoder(d: "By Jove")
st.startEditorWar()

In this example, we mark the method startEditorWar as final. Any subclass that inherits from this class now cannot override that method. Any attempt to override it will result in a compiler error. We also mark declare an initializer that takes a string argument. By marking this as required we ensure that any class that inherits from StubbornCoder must implement an initializer like this.

12.10 Type Methods

In other languages these are often referred to as class variables and class methods. They are properties and methods that are attached to (and operate on) the class rather than on individual objects of that class. So they are available to all instances of the class and they usually implement functionality or store data that is shared between all elements of the class.

Note: In Swift these are referred to as type properties and type methods rather than class properties and class methods because they are something that can apply to types other than classes. E.g. Similar to classes, structures and enumerations are also types. And you can define a “class method” on a structure or an enumeration the same way you can on a class. Hence the more general term “type methods” as these methods aren’t just restricted to classes.

Type methods are defined by prefixing the method declaration with class (for structs and enumerations use the static keyword instead of class to indicate type methods).

1 class ShippingCalculator{
2     class func countryRates() -> [String : Float]{
3         return ["Ireland" : 10.0, "USA" : 7.5, "Argentina": 19.0]
4     }
5 }
6 ShippingCalculator.countryRates()

Type methods are called by using the type name with a dot followed by the function name as shown above in line 6.