Up to now, we've covered the basics of the Swift programming language. If you followed along, you should now have a solid understanding of variables, constants, functions, and closures. It's now time to use what we've learned and apply that knowledge to the object-oriented structures of Swift.
To understand the concepts discussed in this tutorial, it's important that you have a basic understanding of object-oriented programming. If you're not familiar with classes, objects, and methods, then I recommend you first read up on these topics before continuing with this tutorial.
eBook: Object-Oriented Programming With Swift
1. Introduction
In this lesson, we're going to explore the fundamental building blocks of object-oriented programming in Swift, classes and structures. In Swift, classes and structures feel and behave very similarly, but there are a number of key differences that you need to understand to avoid common pitfalls.
In Objective-C, classes and structures are very different. This isn't true for Swift. In Swift, for example, both classes and structures can have properties and methods. Unlike C structures, structures in Swift can be extended, and they can also conform to protocols.
The obvious question is: "What's the difference between classes and structures?" We'll revisit this question later in this tutorial. Let's first explore what a class looks like in Swift.
2. Terminology
Before we start working with classes and structures, I'd like to clarify a few commonly used terms in object-oriented programming. The terms classes, objects, and instances often confuse people new to object-oriented programming. Therefore, it's important that you know how Swift uses these terms.
Objects and Instances
A class is a blueprint or template for an instance of that class. The term "object" is often used to refer to an instance of a class. In Swift, however, classes and structures are very similar, and therefore it's easier and less confusing to use the term "instance" for both classes and structures.
Methods and Functions
Earlier in this series, we worked with functions. In the context of classes and structures, we usually refer to functions as methods. In other words, methods are functions that belong to a particular class or structure. In the context of classes and structures, you can use both terms interchangeably since every method is a function.
3. Defining a Class
Let's get our feet wet and define a class. Fire up Xcode and create a new playground. Remove the contents of the playground and add the following class definition.
class Person { }
The class
keyword indicates that we're defining a class named Person. The implementation of the class is wrapped in a pair of curly braces. Even though the Person
class isn't very useful in its current form, it's a proper, functional Swift class.
Properties
As in most other object-oriented programming languages, a class can have properties and methods. In the updated example below, we define three properties:
-
firstName
, a variable property of typeString?
-
lastName
, a variable property of typeString?
-
birthPlace
: a constant property of typeString
class Person { var firstName: String? var lastName: String? let birthPlace = "Belgium" }
As the example illustrates, defining properties in a class definition is similar to defining regular variables and constants. We use the var
keyword to define a variable property and the let
keyword to define a constant property.
The above properties are also known as stored properties. Later in this series, we learn about computed properties. As the name implies, stored properties are properties that are stored by the class instance. They are similar to properties in Objective-C.
It's important to note that every stored property needs to have a value after initialization or be defined as an optional type. In the above example, we give the birthPlace
property an initial value of "Belgium"
. This tells Swift that the birthplace property is of type String
. Later in this article, we take a look at initialization in more detail and explore how it ties in with initializing properties.
Even though we defined the birthPlace
property as a constant, it is possible to change its value during the initialization of a Person
instance. Once the instance has been initialized, the birthPlace
property can no longer be modified since we defined the property as a constant property with the let
keyword.
Methods
We can add behavior or functionality to a class through functions or methods. In many programming languages, method is used instead of function in the context of classes and instances. Defining a method is almost identical to defining a function. In the next example, we define the fullName()
method in the Person
class.
class Person { var firstName: String? var lastName: String? let birthPlace = "Belgium" func fullName() -> String { var parts: [String] = [] if let firstName = self.firstName { parts += [firstName] } if let lastName = self.lastName { parts += [lastName] } return parts.joined(separator: " ") } }
The method fullName()
is nested in the class definition. It accepts no parameters and returns a String
. The implementation of the fullName()
method is straightforward. Through optional binding, which we discussed earlier in this series, we access the values stored in the firstName
and lastName
properties.
We store the first and last name of the Person
instance in an array and join the parts with a space. The reason for this somewhat awkward implementation should be obvious: the first and last name can be blank, which is why both properties are of type String?
.
Instantiation
We've defined a class with a few properties and a method. How do we create an instance of the Person
class? If you're familiar with Objective-C, then you're going to love the conciseness of the following snippet.
let john = Person()
Instantiating an instance of a class is very similar to invoking a function. To create an instance, the name of the class is followed by a pair of parentheses, and the return value is assigned to a constant or variable.
In our example, the constant john
now points to an instance of the Person
class. Does this mean that we can't change any of its properties? The next example answers this question.
john.firstName = "John" john.lastName = "Doe" john.birthPlace = "France"
We can access the properties of an instance using the convenience of the dot syntax. In the example, we set firstName
to "John"
, lastName
to "Doe"
, and birthPlace
to "France"
. Before we draw any conclusions based on the above example, we need to check for any errors in the playground.
Setting firstName
and lastName
doesn't seem to cause any problems. But assigning "France"
to the birthPlace
property results in an error. The explanation is simple.
Even though john
is declared as a constant, that doesn't prevent us from modifying the Person
instance. There's one caveat, though: only variable properties can be modified after initializing an instance. Properties that are defined as constant cannot be modified once a value is assigned.
A constant property can be modified during the initialization of an instance. While the birthPlace
property cannot be changed once a Person
instance is created, the class wouldn't be very useful if we could only instantiate Person
instances with a birthplace of "Belgium". Let's make the Person
class a bit more flexible.
Initialization
Initialization is a phase in the lifetime of an instance of a class or structure. During initialization, we prepare the instance for use by populating its properties with initial values. The initialization of an instance can be customized by implementing an initializer, a special type of method. Let's define an initializer for the Person
class.
class Person { var firstName: String? var lastName: String? let birthPlace = "Belgium" init() { birthPlace = "France" } ... }
We've defined an initializer, but we run into several problems. Take a look at the error the compiler throws at us.
Not only is the initializer pointless, the compiler also warns us that we cannot modify the value of the birthPlace
property since it already has an initial value. We can resolve the error by removing the initial value of the birthPlace
property.
class Person { var firstName: String? var lastName: String? let birthPlace: String init() { birthPlace = "France" } ... }
Note that the name of the initializer, init()
, isn't preceded by the func
keyword. In contrast to initializers in Objective-C, an initializer in Swift doesn't return the instance that's being initialized.
Another important detail is how we set the birthPlace
property with an initial value. We set the birthPlace
property by using the property name, but it's also fine to be more explicit like this.
init() { self.birthPlace = "France" }
The self
keyword refers to the instance that's being initialized. This means that self.birthPlace
refers to the birthPlace
property of the instance. We can omit self
, as in the first example, because there's no confusion about which property we're referring to. This isn't always the case, though. Let me explain what I mean.
Parameters
The initializer we defined isn't very useful at the moment, and it doesn't solve the problem we started with: being able to define the birthPlace of a person during initialization.
In many situations, you want to pass initial values to the initializer to customize the instance you're instantiating. This is possible by creating a custom initializer that accepts one or more arguments. In the following example, we create a custom initializer that accepts one argument, birthPlace
, of type String
.
init(birthPlace: String) { self.birthPlace = birthPlace }
Two things are worth pointing out. First, we are required to access the birthPlace
property through self.birthPlace
to avoid ambiguity since the local parameter name is equal to birthPlace
. Second, even though we haven't specified an external parameter name, Swift by default creates an external parameter name that's equal to the local parameter name.
In the following example, we instantiate another Person
instance by invoking the custom initializer we just defined.
let maxime = Person(birthPlace: "France") print(maxime.birthPlace)
By passing a value for the birthPlace
parameter to the initializer, we can assign a custom value to the constant birthPlace
property during initialization.
Multiple Initializers
As in Objective-C, a class or structure can have multiple initializers. In the following example, we create two Person
instances. In the first line, we use the default initializer. In the second line, we use the custom initializer we defined earlier.
let p1 = Person() let p2 = Person(birthPlace: "France")
4. Defining a Structure
Structures are surprisingly similar to classes, but there are a few key differences. Let's start by defining a basic structure.
struct Wallet { var dollars: Int var cents: Int }
At first glance, the only difference is the use of the struct
keyword instead of the class
keyword. The example also shows us an alternative approach to supply initial values to properties. Instead of setting an initial value for each property, we can give properties an initial value in the initializer of the structure. Swift won't throw an error, because it also inspects the initializer to determine the initial value—and type—of each property.
5. Classes and Structures
You may start to wonder what the difference is between classes and structures. At first glance, they look identical in form and function, with the exception of the class
and struct
keywords. There are a number of key differences, though.
Inheritance
Classes support inheritance, whereas structures don't. The following example illustrates this. The inheritance design pattern is indispensable in object-oriented programming and, in Swift, it's a key difference between classes and structures.
class Person { var firstName: String? var lastName: String? let birthPlace: String init(birthPlace: String) { self.birthPlace = birthPlace } } class Student: Person { var school: String? } let student = Student(birthPlace: "France")
In the above example, the Person
class is the parent or superclass of the Student
class. This means that the Student
class inherits the properties and behavior of the Person
class. The last line illustrates this. We initialize a Student
instance by invoking the custom initializer defined in the Person
class.
Copying and Referencing
The following concept is probably the most important concept in Swift you'll learn today, the difference between value types and reference types. Structures are value types, which means that they are passed by value. An example illustrates this concept best.
struct Point { var x: Int var y: Int init(x: Int, y: Int) { self.x = x self.y = y } } var point1 = Point(x: 0, y: 0) var point2 = point1 point1.x = 10 print(point1.x) // 10 print(point2.x) // 0
We define a structure, Point
, to encapsulate the data to store a coordinate in a two-dimensional space. We instantiate point1
with x
equal to 0
and y
equal to 0
. We assign point1
to point2
and set the x
coordinate of point1
to 10
. If we output the x
coordinate of both points, we discover that they are not equal.
Structures are passed by value, while classes are passed by reference. If you plan to continue working with Swift, you need to understand the previous statement. When we assigned point1
to point2
, Swift created a copy of point1
and assigned it to point2
. In other words, point1
and point2
each point to a different instance of the Point
structure.
Let's now repeat this exercise with the Person
class. In the following example, we instantiate a Person
instance, set its properties, assign person1
to person2
, and update the firstName
property of person1
. To see what passing by reference means for classes, we output the value of the firstName
property of both Person
instances.
var person1 = Person(birthPlace: "Belgium") person1.firstName = "Jane" person1.lastName = "Doe" var person2 = person1 person1.firstName = "Janine" print(person1.firstName!) // Janine print(person2.firstName!) // Janine
The example proves that classes are reference types. This means that person1
and person2
refer to or reference the same Person
instance. By assigning person1
to person2
, Swift doesn't create a copy of person1
. The person2
variable points to the same Person
instance person1
is pointing to. Changing the firstName
property of person1
also affects the firstName
property of person2
, because they are referencing the same Person
instance.
As I mentioned several times in this article, classes and structures are very similar. What separates classes and structures is very important. If the above concepts aren't clear, then I encourage you to read the article one more time to let the concepts we covered sink in.
Conclusion
In this installment of Swift From Scratch, we've started exploring the basics of object-oriented programming in Swift. Classes and structures are the fundamental building blocks of most Swift projects, and we'll learn more about them in the next few lessons of this series.
In the next lesson, we continue our exploration of classes and structures by taking a closer look at properties and inheritance.
If you want to learn how to use Swift 3 to code real-world apps, check out our course Create iOS Apps With Swift 3. Whether you're new to iOS app development or are looking to make the switch from Objective-C, this course will get you started with Swift for app development.
Comments