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:

struct Fraction {
    var numerator: Int
    var denominator: Int
}

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:

extension Fraction {
    func decimal() -> Double {
        Double(numerator) / Double(denominator)
    }
}

This totally works! Another equally valid solution is with the get variable property we can rewrite this as:

extension Fraction {
    var decimal: Double { Double(numerator) / Double(denominator) }
}

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.

extension Fraction {
    
    var decimal: Double {
        get { Double(numerator) / Double(denominator) }
        set { (numerator, denominator) = decimalToFraction(newValue) }
    }
    
}

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.

var ... = ... {
    willSet {
        <code to execute before setting the variable>
    }
}

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).

var ... = ... {
    didSet {
        <code to execute after setting the variable>
    }
}

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.

var image: Image

var imageSize: (width: Int, height: Int) {
    didSet {
        image = Image(width: imageSize.width, height: imageSize.height)
    }
}

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.

var maxRenderSize: (width: Int, height: Int)

var renderSize: (width: Int, height: Int) {
    (width: min(maxRenderSize.width, imageSize.width), 
    height: min(maxRenderSize.height, height))
}

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.

var background: Image
var image: Image { // Note that I am editing the image that I previously defined
     willSet {
          background = image.scaledTo(imageSize)
     }
}

Last updated