Lesson 10 - Properties in Swift
In the previous lesson, Static class members in Swift, we learned about static members.
In today's Swift tutorial, we're going to look at the promised advanced property declaration.
Getters and setters
We often want to have control over how an object property changes from
outside of the class. We could set the property as read-only or react to its
changes somehow. Let's create a new project (name it Properties
)
and add the following Student
class which will represent a student
in a database.
class Student: CustomStringConvertible { var name: String var male: Boolean var age: Int var fullAged: Bool init(name: String, male: Bool, age: Int) { self.name = name self.male = male self.age = age fullAged = true if age < 18 { fullAged = false } } var description: String { var iAmFullAged = "I'm" if (!fullAged) { iAmFullAged = "I'm not" } var gender = "male" if (!male) { gender = "woman" } return "I'm \(name), \(gender). I'm \(age) years old and \(iAmFullAged) of age." } }
The class is very simple, the student has a name, gender, and age. The
fullAged
property is set according to the age and provides a more
comfortable determination whether the student is of age from different places in
the system. We'll use a Bool
value to store gender,
true
indicates that he's male. The constructor will determine
whether the student is of age or not. The description
property has
been altered to suit our needs. In a real world situation, it would probably
just return the student's name. Let's create a student using the
constructor:
let s = Student(name: "Peter Brown", male: true, age: 20) print(s)
The output:
I'm Peter Brown, male. I'm 20 years old and I'm of age.
Everything looks nice, but the properties are able to be re-written. We can break the object like this, so it'll no longer function properly (i.e. would have an inconsistent internal state):
let s = Student(name: "Peter Brown", male: true, age: 20) s.age = 15 s.male = false print(s)
The output:
I'm Peter Brown, female. I'm 15 years old and I'm of age.
Certainly, we would want fullAged
to be updated when the age is
changed. Aside from that, there is no need to allow e.g. a gender to be changed.
However, we want to keep the properties accessible for reading, so we can't make
them private
. In earlier lessons of our course, we've used get
methods to read private properties. We would name them something like
getAge()
and so on. To read particular properties, we'll need such
methods as soon as we mark them private
, so they can't be modified
from outside. The class would now look something like this (I omitted the
constructor and description
):
class Student { var name: String var male: Bool var age: Int var fullAged: Bool // ... func getName() -> String { return name } func getFullAged() -> Bool { return fullAged } func getAge() -> Int { return age } func isMale() -> Bool { return male } func setAge(value: Int) { age = value // updating whether the student is of age fullAged = true if age < 18 { fullAged = false } } }
The methods just returning a value are very simple. In the method setting the
age is some more logic, since we have to reconsider the fullAged
property. We made sure we can't set variables in any other way than what we
want. We now control all the property changes and can work with them as needed.
Our design must prevent all unwanted changes of the internal state that would
cause an object to malfunction.
Methods for returning values are called getters and methods
for setting values are called setters. We could potentially add
an editStudent()
method to edit other properties sort of like the
constructor. Essentially, a student's name, age, and other properties would be
updated using this method. We could also validate values being set there since
we would be able to handle all attempts to change certain values in one
place.
However, asking for properties using methods takes time and could be confusing. That's why Swift implemented other syntax as well.
private(set)
If we only want to prevent setting the properties from outside, we can use
the private(set)
access modifier. We just place it before the
properties of the class, keeping complete encapsulation. Since we won't need the
getters (methods) for these properties anymore, we can delete them now.
private(set) var name: String private(set) var male: Bool private(set) Var age: Int
The properties act normally inside the class, however, we can't change them from outside now.
Computed properties
We elegantly prevented unwanted changes in properties of our class from
outside. Using a different technique, we can make a property to behave as a
method, but when working with it, we won't need to write the parentheses
()
after its name. We can put this to use when we have properties
that return a value depending on other properties. In Swift, we call such
properties Computed properties. In our class, we'll use it with
fullAged
where, instead of the stored Bool
value,
we'll return directly the age < 18
expression. This way the
property value will always be updated. The description
property
which we've already used without fully understanding it works in a similar
manner. Let's modify the fullAged
property:
var fullAged: Boolean { return age >= 18 }
After all modifications, the class will look like this:
class Student: CustomStringConvertible { private(set) var name: String private(set) var male: Bool private(set) var age: Int var fullAged: Bool { return age >= 18 } init(name: String, male: Bool, age: Int) { self.name = name self.male = male self.age = age } var description: String { var iAmFullAged = "I'm" if (!fullAged) { iAmFullAged = "I'm not" } var gender = "male" if (!male) { gender = "female" } return "I'm \(name), \(gender). I'm \(age) and \(iAmFullAged) of age." } }
We've finished a simple Swift getter that looks as a normal property, but
it's real-only. Better variable name would be isOfAge
corresponding
to the conventions for Bool
values, but we're talking about
properties in general here
Of course, if you had a more complex calculation in your getter and access it
frequently, it'd be better to think about optimization and re-calculate the
value only when the property it depends on changes. In our case, it's the
age
property.
It would look like this:
private var _age: Int private(set) var fullAged: Bool var age: Int { get { return _age } set { fullAged = newValue >= 18 _age = newValue } }
In this case we have to create a _age
variable which stores the
age itself. When setting the value, we also calculate whether the student is of
age. This approach is rarely used and exists exclusively for computed
properties. newValue
represents the new value being set, as you
probably guessed.
Observing property changes
It's often useful to be able to react to changes in properties. We could, for example, want to recalculate other properties, call a method if some limit has been exceeded, or just update the user interface.
Swift allows us to react to assigning of new values to a property easily.
It's called property observers. We are provided with the
willSet
and didSet
keywords. We write them in
curly-bracket blocks as getters and setters. Their names basically tell us what
they do. The willSet
block is called just before setting a new
value to a property. didSet
is called after the assignment. You
probably won't use both at the same time ever, didSet
is used a
little more often.
Let's have the same example with age:
private(set) var fullAged: Bool private(set) var age : Int { didSet { fullAged = age >= 18 } }
Once the age is set, the fullAged
property is also recalculated.
In this case, traditional setters and getters really weren't the best choice.
It's up to you whether you like the didSet
solution or the
fullAged
property as just a simple read-only getter. When dealing
with didSet
and` willSet`, just keep in mind that they are
unfortunately not called if the value is set from the constructor, so in our
case we have to set the age of the student for the first time by ourselves in
the constructor.
We can test setting the age
and fullyAged
properties. Let's add a simple method:
func setAge(_ age: Int) { self.age = age }
And edit main.swift
:
let s = Student(name: "Peter Brown", male: true, age: 20) print(s) s.setAge(17) print(s)
The output:
I'm Peter Brown, male. I'm 20 years old and I'm of age. I'm Peter Brown, male. I'm 17 years old and I'm not of age.
We'll use such properties from now on, because they provide us perfect
encapsulation. All class variables are basically properties for Swift, it just
depends on how they are implemented. Don't forget the very useful and simple
private(set)
modifier.
In the next lesson, Protocols (interfaces) in Swift, we'll look at how to work with protocols in Swift.