Welcome to the fourth tutorial in the series. Today we’ll cover many of Swift’s object-oriented features.
What you will learn…
- Basic object-oriented concepts with Swift
What you should know…
- A familiarity with object-oriented programming concepts
- The basics of the Swift programming language from part one, two and three
Swift doesn’t favour one programming paradigm over another. However it does provide excellent object-oriented features, which we’ll explore the basics of.
Many of the topics covered previously, including functions and structures, will be directly applicable to our coverage of Swift’s object-oriented features. Today’s tutorial will walk you through the steps to build and use a class that models a popular weapon from the Star Wars universe.
If you’re comfortable with the topics covered in part three then getting to grips with the basics of object-oriented programming in Swift shouldn’t be too much of a stretch.
Getting Started
By now you should feel comfortable with the content covered in the first three tutorials. If not then please revisit them:
- Quick Start Guild To Swift: Part 1
- Quick Start Guild To Swift: Part 2
- Quick Start Guild To Swift: Part 3
To try out this tutorial’s code examples, install Xcode and create a playground.
Detail regarding that can be found in the first tutorial.
Classes
A class is declared using the class
keyword and its definition is placed within a pair of braces. Its general form looks like this:
// Implementation goes here
}
Unlike languages such as Objective-C and C++, Swift does not require you to write a separate interface and implementation file when creating a class. Instead everything is kept within a single file.
For this reason it is encouraged that all classes and structures use UpperCamelCase names (such as
Functionality is added to a class by defining properties and methods for it. Let’s begin by defining a class that represents an ion cannon:
let capacity = 20
var ammunition = 20
}
Our class defines two stored properties:
Instantiating a Class
A class instance can be created with initializer syntax:
If you think back to the section on structures in part three, you may reasonably assume that a memberwise initializer is provided for each class you define. For example, you may try to create an instance of your ion cannon with the following:
However, unlike structures, class instances do not receive a default memberwise initializer. The line of code above would result in a compile-time error.
Classes are Reference Types
Until now, every type we’ve looked at, including structures, has been a value type. Classes however are reference types.
Remember, a value type is copied when it is assigned to a variable or constant, or when being passed to a function. With classes, a reference to the same instance is used instead.
Here’s an example:
let cannon2 = cannon1
The code above creates two constants that point to the exact same
print(“cannon1.ammunition: \(cannon1.ammunition)”)
print(“cannon2.ammunition: \(cannon2.ammunition)”)
The following will be output:
cannon2.ammunition: 10
While structures and classes are very similar, this is one major difference between the two and should be a consideration when deciding whether it’s best to define a structure or class for a particular type you are modelling.
Classes As Constants
We saw in part three that declaring a constant instance of a structure will prevent you from modifying any variable properties belonging to that instance.
The same isn’t true of class instances, which are reference types. If you assign an instance of a reference type to a constant, you can still change any variable properties that belong to it. You can’t however, assign your constant to another class instance after it has been set. Let’s look at a few quick examples:
cannon.ammunition = 10
The code snippet above is perfectly valid. Even though we use a constant to refer to our
However, we can’t set our
cannon.ammunition = 10
The line above will result in the following compile-time error: Cannot assign to value: 'cannon' is a 'let' constant
.
Identity Operators
When working with reference types such as class instances, it’s possible for multiple constants and variables to refer to the exact same instance of a class. Here’s an example:
let cannon2 = IonCannon()
In the code above, the constants
On occasions, you may want to know if two constants or variables refer to the exact same class instance. Swift provides the "identical to" operator (===
) for this purpose. Try entering the following into your playground:
let cannon2 = IonCannon()
let cannon3 = cannon1
println(“cannon1 and cannon2 refer to the same instance: \(sameInstance)”)
println(“cannon1 and cannon3 refer to the same instance: \(sameInstance)”)
println(“cannon2 and cannon3 refer to the same instance: \(sameInstance)”)
It will result in the following being output:
cannon1 and cannon3 refer to the same instance: true
cannon2 and cannon3 refer to the same instance: false
As you might expect, Swift also provides the "not identical to" operator (!==
) to check if two constants or variables are referring to different instances.
Properties
We spent considerable time discussing properties when covering structures in part three. The good news is that everything that applies to properties for structures also applies to classes.
We’ll spend just a few moments on the subject to help build out our example
Type Properties
Remember, type properties are useful for defining values that are universal to all instances of a particular type. Type properties are defined using the static
keyword.
While we’re on the subject, let’s change our class’
var ammunition = 20
}
class
keyword instead of static
. Doing so will allow a subclass to override the superclass’ implementation. We’ll cover this at a later date in part five. We can also explicitly set our variable property,
static let capacity = 20
var ammunition =
}
This will make our code more maintainable.
Property Observers
To ensure our ion cannon example is as robust as possible, it’s important that the
For completeness, add a property observer that constrains the value of the
static let capacity = 20
var ammunition = IonCannon.capacity {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
}
Verify that everything is working as expected by attempting to set invalid values for the
cannon.ammunition = -1
print(“cannon.ammunition: \(cannon.ammunition)”)
cannon.ammunition = 21
print(“cannon.ammunition: \(cannon.ammunition)”)
cannon.ammunition = 10
print(“cannon.ammunition: \(cannon.ammunition)”)
The following will be output:
cannon.ammunition: 20
cannon.ammunition: 10
As you can see from the results above, the value of the instance’s
Methods
Methods have the exact same syntax as functions, which we covered in part three. A method belongs to a particular class and provides a means to access and modify an instance of that class in some way.
Let’s add a simple method to our class that is used to reload the cannon’s ammunition:
static let capacity = 20
var ammunition = IonCannon.capacity {
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
ammunition = IonCannon.capacity
}
}
We can now easily reset our cannon to full capacity by calling the
cannon.ammunition = 10
print(“cannon.ammunition: \(cannon.ammunition)”)
cannon.reload()
print(“cannon.ammunition: \(cannon.ammunition)”)
You should see the following output in the sidebar:
cannon.ammunition: 20
In other words, calling
Parameters
As with functions, you can optionally define one or more typed parameters for class methods.
Let’s add a method to our
static let capacity = 20
var ammunition = IonCannon.capacity {
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
func reload() {
ammunition = IonCannon.capacity
}
ammunition -= amount
}
}
We’ve just added a method named
Let’s use our method to fire 4 ion bursts at the incoming Imperial fleet:
cannon.fireBurstOf(4)
We can verify that the pulses were fired by querying our instance’s
The line above will result in the following being sent to the appropriate output:
The cannon has enough charge remaining to fire 16 more ion bursts.
Call the method again to fire 2 more bursts:
Your ion cannon’s
It’s possible to provide multiple versions of a method. Here’s an alternative version of our
static let capacity = 20
var ammunition = IonCannon.capacity {
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
func reload() {
ammunition = IonCannon.capacity
}
func fireBurstOf(amount: Int) {
ammunition -= amount
}
ammunition -= (amount * numberOfTimes)
}
}
The
cannon.fireBurstOf(3, numberOfTimes: 4)
Notice that we explicitly stated the name of the method’s second parameter when making our call. This should seem odd to you as we didn’t explicitly define an external name for our second parameter. In Swift, the default behaviour of local and external parameter names in methods differs from functions. Swift gives the first parameter in a method a local parameter name by default, but gives subsequent parameter names both a local and external parameter name.
The reason for this is that Swift attempts to make method naming and calling similar to that of Objective-C. In Objective-C, the name of a method typically refers to the method’s first parameter using a preposition such as with, for, by, of etc. We can see this in our
Return Values
As you would expect, values can be returned from methods in exactly the same manner as functions.
As a quick example, let’s modify our
Change your method’s definition to this:
var actuallyFired = 0
while ammunition > 0 && remainingShots > 0 {
ammunition--
remainingShots--
actuallyFired++
}
return actuallyFired
}
Cool! We can now tell exactly how many ion pulses were fired on each attempt. Here’s a quick example of how to use our updated method:
print(“Available ion pulses: \(cannon.ammunition)”)
var pulsesFired = cannon.fireBurstOf(17, numberOfTimes: 1)
print(“Attempting to fire 17 ion pulses: \(pulsesFired) actually fired with \(cannon.ammunition) remaining.”)
pulsesFired = cannon.fireBurstOf(5, numberOfTimes: 1)
print(“Attempting to fire 5 ion pulses. \(pulsesFired) actually fired with \(cannon.ammunition) remaining.”)
The following will be output to the sidebar:
Attempting to fire 17 ion pulses: 17 actually fired with 3 remaining.
Attempting to fire 5 ion pulses: 3 actually fired with 0 remaining.
As you can see from the final line above. We attempted to fire 5 ion pulses when there was only enough ammunition for 3. Therefore our
Default Parameter Values
As with functions, a default value can be assigned to parameters of a method. We can default our
func fireBurstOf(amount: Int) {
ammunition -= amount
}
func fireBurstOf(amount: Int, numberOfTimes: Int
var remainingShots = amount * numberOfTimes
var actuallyFired = 0
while ammunition > 0 && remainingShots > 0 {
ammunition–
remainingShots–
actuallyFired++
}
return actuallyFired
}
Thanks to our default parameter, we can still make the following two types of calls even though we’ve removed one of our methods:
fireBurstOf(_:) fireBurstOf(_:numberOfTimes:)
Here’s a concrete example:
cannon.fireBurstOf(5) // Will fire 5 ion pulses
cannon.fireBurstOf(5, numberOfTimes: 1) // Will also fire 5 ion pulses
cannon.fireBurstOf(5, numberOfTimes: 2) // Will fire 10 ion pulses
Type Methods
As well as instance methods, you can also define methods that belong to the class itself rather than an instance of the class. Such methods are known as type methods and are specified by placing the static
keyword before the method’s func
keyword.
Let’s work through a simple example. We’ll write a type method within our
Start by adding a type property that keeps track of the number of Star Destroyers that have been successfully hit by our ion cannons:
static let capacity = 20
var ammunition = IonCannon.capacity {
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
:
:
}
We’ll also add a few lines to our
class IonCannon {
static let capacity = 20
var ammunition = IonCannon.capacity {
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
static var directHits = 0
:
:
func fireBurstOf(amount: Int, numberOfTimes: Int = 1) -> Int {
var remainingShots = amount * numberOfTimes
var actuallyFired = 0
while ammunition > 0 && remainingShots > 0 {
ammunition–
remainingShots–
actuallyFired++
IonCannon.directHits++
}
}
return actuallyFired
}
}
Notice on the first line of code above that we imported iOS’s Foundation library. Doing so provides support for random number generation via the
Finally, we’ll add our type method to the class. This method will examine the
switch directHits {
case 0...2:
return “The rebel fleet has little chance”
case 3...5:
return “Some rebel transports may get away from Hoth”
default:
return “The entire rebel fleet will likely escape”
}
}
And here’s how we’d use our updated class:
let cannon2 = IonCannon()
let cannon3 = IonCannon()
cannon1.fireBurstOf(5, numberOfTimes: 3)
cannon2.fireBurstOf(12)
cannon3.fireBurstOf(7, numberOfTimes: 2)
print(IonCannon.report())
The code above creates three ion cannon instances and fires off a burst of ion pulses from each. We then obtain a report to see if the cannon fire was enough to clear the Empire’s Star Destroyers from the path of the fleeing Rebel fleet.
Pay particular attention to how we call the
We can also find out the actual number of direct hits by obtaining the value of the class’
class
keyword in place of static
when defining a type method. Doing so will allow a subclass to override the superclass’ implementation of the type method. This will be covered at a later date in part five. Initializers
In part three we covered initialization of structures including how to write custom initializers. In this tutorial we have seen that class instances are created using initializer syntax similar to structure initialization:
We can also write one or more custom initializer methods for a class, which can be called to create a new instance of a particular class. Custom initializers are useful when certain stored properties of an instance need to be set or any further setup within the instance is required.
An initializer is required if a default value has not been assigned to all of a class’ stored properties. The only exception to this are optional property types, which can be assigned a value at a later point.
Here’s how our
static let capacity = 20
var ammunition
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
static var directHits = 0
ammunition = IonCannon.capacity
}
:
:
}
In the example above we set the initial value of our
A more practical use of initializers is when you wish to customise the initialization of a class instance with input parameters. Let’s improve our
static let capacity = 20
var ammunition
didSet {
if ammunition < 0 {
ammunition = 0
} else if ammunition > IonCannon.capacity {
ammunition = IonCannon.capacity
}
}
}
static var directHits = 0
self.ammunition = ammunition
if self.ammunition > IonCannon.capacity {
self.ammunition = IonCannon.capacity
}
}
func reload() {
ammunition = IonCannon.capacity
}
func fireBurstOf(amount: Int, numberOfTimes: Int = 1) -> Int {
var remainingShots = amount * numberOfTimes
var actuallyFired = 0
while ammunition > 0 && remainingShots > 0 {
ammunition–
remainingShots–
actuallyFired++
if arc4random_uniform(10) == 0 {
IonCannon.directHits++
}
}
return actuallyFired
}
static func report() -> String {
switch directHits {
case 0...2:
return “The rebel fleet has little chance”
case 3...5:
return “Some rebel transports may get away from Hoth”
default:
return “The entire rebel fleet will likely escape”
}
}
Our custom initialiser takes a single parameter that is used to set the instance’s initial amount of ammunition. The initialiser’s body also safeguards against invalid values by ensuring that the specified ammunition value isn’t a negative integer or greater than the cannon’s capacity.
Also, our initialiser has both a local (
Finally, you should note that our custom initialiser’s parameter also has a default value of
With our initialiser in place, we can now create ion cannon instances and provide each with a specified initial amount of ammunition. Here are some concrete examples:
let cannon2 = IonCannon(withAmmunition: 15)
let cannon3 = IonCannon(withAmmunition: 40)
The Self Property
We’ve mentioned the self
property before when covering structures, but for the avoidance of doubt we’ll spend a few moments with it again.
Just like structures, every class instance has an implicit property named
ammunition = IonCannon.capacity
}
We can rewrite this method to explicitly use
}
However, for most situations you won’t need to explicitly use
The
if
}
}
self
keyword. It is commonly know as this
in most other programming languages. Next Time
We’ve covered the basics of object oriented programming. While Swift uses different naming conventions the principles are more-or-less identical to those found in other object-oriented languages. If you’re coming from a language such as Java, C++, TypeScript or ActionScript 3 then writing and working with classes in Swift should be straight forward. Even if you’re coming from JavaScript, which lacks much of the syntactic sugar that other object-oriented languages provide, you shouldn’t find things too much of a stretch to grasp.
There are still a few object-oriented features still to cover. Next time we’ll spend some time looking at inheritance. You may also have noticed that we haven’t yet covered access control. Swift’s access control is different from that found in most languages – it is file-based rather than class-based – and therefore deserves more space than this particular tutorial can afford it. Finally, we’ll also spend some time looking at class deinitialization, which allows us to perform any manual clean-up when a class instance is no longer needed.
See you in part five.