Classes are one of the most fundamental building blocks of Swift code. In this tutorial, we’re going to discuss how classes work, what instances are, how inheritance works, and much more.
Here’s what we’ll get into:
override
and super
Ready? Let’s go.
Classes are the building blocks of your app. You use them to structure and organize your code. You typically use classes for complex data, and structs for simple ones.
Think about it like this:
Let’s say we’re building an app you can use to draw something. One of the classes we’re including in the app is Circle
. Like this:
class Circle {
var radius: Double = 0.0
var color: String = ""
}
This class Circle
has 2 properties:
radius
of type Double
and a default value of 0.0
color
of type String and a default value ""
A property is a variable or constant that belongs to a class. In the above code, we’ve added 2 properties: radius
and color
. You could say that every circle now has a radius and color.
You can imagine that this Circle
class can be used to draw a circle of a given radius and color on screen, in a drawing app. In fact, we can also add a function to Circle
to calculate its circumference. Like this:
class Circle {
···
func circumference() -> Double {
return 2.0 * .pi * radius
}
}
A function is a self-contained block of code that performs an operation, like calculating the circumference of a circle. Functions can also return a value to whoever called the function. You can take the value that the circumference()
function returns, and pass that value around in your code.
See how we’re organizing the code that belongs to a circle, in the Circle
class? That’s what classes are all about. You can organize components in your code, ranging from Invoice
to Tweet
to DatabaseController
, and that helps you to create code that’s easy to reuse, maintain and extend.
You typically use classes for complex data types, like Timer or DateFormatter. Classes have a counterpart, called structures, that you typically use for simpler data types, like String or View.
Working with classes in Swift has 2 important aspects: the class and the instance. So far, we’ve created a class called Circle
. Like this:
class Circle {
var radius: Double = 0.0
func circumference() -> Double {
return 2.0 * .pi * radius
}
}
Instances of a class are often simply called “object”. You got the class and the object, and the object is an instance of the class. Jargon like that often gets in the way of communication, but when learning about coding, it’s often smart to know the appropriate terminology.
If you want to use this class in your code, you’ll have to create an instance of the class. It’s like making a copy of it, but different. Check this out:
let sun = Circle()
sun.radius = 696340.0 // in km
print(sun.circumference())
// Output: 4375233.25
In the above code, we’ve created an instance of the Circle
class by using the Circle()
initializer. This instance is assigned to a constant called sun
.
On the second line of the above code, we’re assigning a number to the radius
property of the sun
instance. And on the third line, we’re printing out the output of the circumference()
function.
What’s the difference between the class and an instance? Let’s take a look at a metaphor:
As with any blueprints, you can build an unlimited number of houses based on it. This is no different with classes and instances, and that’s the power of classes: you define them once, and reuse them everywhere.
Check this out:
let radii = [3.0, 4.0, 5.0, 10.0, 99.0, 42.0]
for radius in radii {
let circle = Circle()
circle.radius = radius
print(circle.circumference())
}
In the above code, we’re reusing the Circle
class to calculate the circumference of an array with arbitrary radii. This is exactly what programming is about! How can you be as lazy as possible by creating and reusing code?
The concept of classes belongs to an overarching concept called Object-Oriented Programming (OOP). Within this category, you can learn more about structs, functions, properties, inheritance, protocols, and much more.
Classes have an essential characteristic, that structs don’t have. A class can subclass another class, and inherit the properties and functions of that class. Subclassing allows you to reuse components in your code, while customizing their functions by overriding them.
Take a look at the following class:
class Tweet {
var text: String = ""
func show() {
print(text)
}
}
In the above code, we’ve defined a class called Tweet
. It has one property text
of type String
, and a function show()
that prints out the value of text
.
We’re now going to create a subclass of Tweet
. Like this:
class MediaTweet: Tweet {
var image: String = ""
}
You can see that MediaTweet
is a subclass of Tweet
because its syntax is class subclass : superclass
. The MediaTweet
has now inherited all properties and functions of the Tweet
class, and has also added a property of its own: image
of type String
.
Here’s how you’d use the MediaTweet
class:
let tweet = MediaTweet()
tweet.text = "My hovercraft is full of eels"
tweet.image = "https://picsum.photos/300/300"
tweet.show()
// Output: My hovercraft is full of eels
What if we want to adjust the output of the show()
function with our own implementation? You can do that by overriding the show()
function like this:
class MediaTweet: Tweet {
···
override func show() {
print("\(text) -- \(image)")
}
}
When we run the tweet.show()
function again, its output is now:
My hovercraft is full of eels -- https://picsum.photos/300/300
That’s because we’ve “redefined” the show()
function. You can still access the superclass’ function within the show()
function with super.show()
. You’ll see this often in practical iOS development, where you still need to run the original of the overriden function.
Before we call it quits, let’s discuss one important principle in working with inheritance. When you subclass a class, you can use that subclass in places where the superclass is required. Here, check this out:
let tweet = MediaTweet()
tweet.text = ···
func upload(tweet: Tweet) {
print("Uploading tweet...")
tweet.show()
}
upload(tweet: tweet)
// Output: Uploading tweet...
// My hovercraft is full of eels -- https://picsum.photos/300/300
In the above code, we’ve created an instance of MediaTweet
and assigned it to the tweet
constant. Just as before, we’re changing the text
and image
properties to a meaningful value.
In the second part of the code, we’re creating a function upload(tweet:)
. This function accepts one parameter of type Tweet
. Inside the function, the data of the tweet is “uploaded” and printed out.
Finally, on the last line, we’re calling the upload(tweet:)
function with an argument tweet
, i.e. the function is called and the MediaTweet
object is provided as input.
What’s so special about this? Take a look at the signature of the upload(tweet:)
function. It accepts a parameter of type Tweet
, but we’re providing it with another type: MediaTweet
. The types don’t seem to match! Why is this valid code?
It’s because MediaTweet
is a Tweet
. The MediaTweet
class inherits everything from Tweet
, so we can be logically certain that MediaTweet
is a Tweet
– and more!
We can even test this hierarchy with the “is” keyword. Like this:
if tweet is Tweet {
print("tweet is of class Tweet")
}
if tweet is MediaTweet {
print("tweet is of class MediaTweet")
}
// Output: tweet is of class Tweet
// tweet is of class MediaTweet
Why is this an important principle to understand? In practical iOS development, you’ll come across many scenarios where you can provide a subclass of a class as a value, whose required type is the superclass you’re subclassing.
Differently said, you may end up working with a function that asks for a Tweet
. You’ve subclassed that Tweet
class to give it more functionality, like MediaTweet
. You can still provide an instance of MediaTweet
to a function that asks for type Tweet
, like upload(tweet:)
, because one class subclasses the other. MediaTweet is a Tweet!
This substitution of a subclass for a superclass is formally described as the Liskov substitution principle, which is part of the SOLID principles. It has crucial consequences in coding, for example, that you cannot change the return type of a function you’re overriding. You also cannot change properties in a subclass, but you can override their getter and setter.
Awesome! We’ve discussed classes in this tutorial. Here’s what you learned:
Want to learn more? Check out these resources: