Variable Properties
Variable Properties
This often-overlooked tool allows you to execute code when interacting with (getting / setting) variables. For this section, we will be using a hypothetical data type Fraction
:
Get
Sometimes it may not make sense to store some piece of data. For example, it may make sense to offer an alternate form of representation for some data. In our case, we may want to convert our fraction to a decimal. Typically you would write a function for this:
This totally works! Another equally valid solution is with the get
variable property we can rewrite this as:
We write a variable with a type and instead of an assignment, we write a closure. This is now called a computed property (which makes sense because you need to do some computation every time you get it) and it must be a variable not a constant. Get is unique within the variable properties because if it is standalone, we can write it without explicitly stating that it is a get property–it's just assumed. In this standalone case, we just write our getter within one set of curly brackets.
Set
Continuing along with the idea of an analogous representation, it also makes sense that we create a setter that goes through this representation. For properties that are not get
, we must explicitly state what property we want. Also, if we are stacking properties, and one is get
, we must explicitly state the get
property.
To write a collection of properties, we enclose the set in curly brackets and for every property, we write its name followed by another set of curly brackets that enclose the intended behavior.
You'll notice two things: newValue
and decimalToFraction
. newValue
is a variable that is inherent to the set
variable property–it is the value to be set. decimalToFraction
is a function of type (Double) -> (Int, Int)
that we abstracted for sake of brevity (aka I was lazy). If the assignment in the setter is new, all it is doing is mapping the nth component of the tuple to the nth variable. Aka, the first integer becomes the numerator and the second integer becomes the denominator.
Property Observers
There are times when you do not want to change information, you just want to execute some code when a value changes. In other languages, you may have used a setter to do this, but we have safer ways of doing this in Swift.
We have two options:
willSet
willSet
The idea of this property is that we are executing code before a new value value has been assigned to the variable. Thus, you have access to the previous value, as well as the current (or soon-to-be) value. Similar to set, we access the previous value through the newValue
identifier.
didSet
didSet
Is less powerful, but more applicable because we don't usually need both values. We can think of didSet
that is executed every time a value changes (after it has been set).
Note that you cannot have both a didSet
and a willSet
property on a variable. But to be fair, willSet
can do most things a didSet
can. This is a niche case, so it is okay if you don't understand, but the only time where you would need to put something in a didSet
instead is if the setter has side effects that will need to be processed before. For example, if you are using computed properties that rely on this specific variable, there will be different behavior before and after the value has been set.
Example
To be honest, I can't think of a good, simple example, so I am going to do my best, and talk about a topic that is near and dear to my heart–graphics!
You may or may not be familiar with ray tracing, and that's totally fine! Basically, we are making a photo-realistic image through software! It's wicked cool. Anyways, what you need to know, is that ray tracing is wicked slow. Especially, an unoptimized project by a wee-little high school student with no concept of linear algebra.
The image size was the size of the window that my app had. Hence, the image size changed whenever I changed the size of the window. Without going too far into the process, if I were to make too big of an image, it would actually stall my computer–as in my computer would be frozen. So, the solution I took was to build my image in small chunks.
Depending on how much I want to use my computer, I may be okay with letting the program slow down my computer's overall performance. This means I want to make the size that I render to be flexible.
Okay, let's get into some implementation. We are going to store the size of our image as a tuple of integers. The UI will handle updating it. But, what we need to handle is every time the window size changes. To do this, we need to create a new, empty image! I'll be abstracting a decent amount from this example as graphics (the Metal API) is very very complicated, so some of the names of types will be wrong.
We are also going to store a tuple of our maximum render size. The program may choose something below this size if the image is smaller. Using maxRenderSize
and imageSize
we will compute a new variable renderSize
, which will be the minimum of the two.
You know what we forgot...? Edge cases :/
Imagine we have a fully rendered, beautiful image. Then, the user decides they want a bigger image. We have to start from scratch–we cannot copy the previously rendered image into the new one because of aspect ratios and stuff like that. Ew, physics. Anyways, we may want to show a preview of the new image using the old one. I.e. it would just pain me to go from such a beautiful image to a completely blank one.
So, let's set a background image to be the previously rendered image. An unrendered section of the image is clear, so as the image is rendered, the old one will be covered! To do this, I will use a willSet
on image because I need the previous value! We will need to scale the image to the correct size though.
Last updated