Weekly Swift articles, podcasts and tips by John Sundell.

Properties

Published on 22 May 2020

Just like many other programming languages, Swift enables us to organize our in-memory data by storing it in properties — constants and variables that are attached to a given type, value, or object. In this Basics article, let’s take a look at a few examples of various kinds of properties and what their characteristics are.

Mutable properties are declared using the var keyword, along with the type of value that the property is going to store — unless the latter can be inferred by the compiler. For example, here we’re defining three properties within a Book type — two of which are manually typed as String, while the third will have its type (Int) inferred from its default value:

struct Book {
    var name: String
    var author: String
    var numberOfStars = 0
}

On the other hand, if we want to prevent one of our properties from being mutated after being initialized, then we can use the let keyword to make it a constant property instead — like this:

struct Book {
    let id: UUID
    var name: String
    var author: String
    var numberOfStars = 0
}

The above setup lets us create a Book instance using any value for the above four properties (although we can also omit numberOfStars if we’d like, since it has a default value), but only our mutable properties may be modified afterwards:

var book = Book(
    id: UUID(),
    name: "The Swift Programming Language",
    author: "Apple"
)

book.id = UUID() // Compiler error
book.name = "New name" // Allowed
book.numberOfStars += 1 // Also allowed

All of the above are examples of stored properties — properties which values are stored in memory once assigned. Computed properties, on the other hand, enable us to define convenience APIs in the shape of properties that are recomputed each time that they’re accessed.

For example, if different parts of our logic requires us to check if a given book’s name is longer than 30 characters — we might want to encapsulate that calculation within a computed hasLongName property, like this:

extension Book {
    var hasLongName: Bool {
        name.count > 30
    }
}

The benefit of computed properties is that we don’t need to manually sync them with the underlying state that they’re derived from, since they’re recomputed each time. However, that also means that we have to be careful not to perform any heavy computation within them. For more on that topic, check out “Computed properties in Swift”.

While the above computed property is read-only, we can also define ones that have both a getter and a setter (making them act identical to a var, apart from the fact that their values aren’t stored). As an example, let’s say that we wanted to modify our Book type’s author property to contain an Author value, rather than a String:

struct Author {
    var name: String
    var country: String
}

struct Book {
    let id: UUID
    var name: String
    var author: Author
    var numberOfStars = 0
}

To still be able to easily access and modify a given book’s author name, we could then add a computed property with both a getter and setter for doing just that — like this:

extension Book {
    var authorName: String {
        get { author.name }
        set { author.name = newValue }
    }
}

Next, let’s take a look at lazy properties. A property marked with the lazy keyword must be mutable, and have a default value assigned to it — however, that default value won’t be computed until the property is accessed for the first time. That characteristic sort of makes lazy properties act as a “middle ground” between computed and stored ones, which comes very much in handy when building view controller-based UIs.

Since view controllers shouldn’t start creating their subviews until the viewDidLoad method has been called by the system, one really neat way to avoid having to store such subviews as optionals is to make them lazy instead — which will defer their creation until they’re accessed (for example in viewDidLoad):

class BookViewController: UIViewController {
    private lazy var nameLabel = UILabel()
    private lazy var authorLabel = UILabel()
    
    ...

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(nameLabel)
        view.addSubview(authorLabel)
    }
}

The above two properties are marked as private to only make them visible within our view controller itself. To learn more about that, check out the Basics article about access control.

Lazy properties can also have their value computed by a closure or a function. For example, here we’re now using a private makeNameLabel method to create and setup the UILabel for our nameLabel property:

class BookViewController: UIViewController {
    private lazy var nameLabel = makeNameLabel()
    private lazy var authorLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.addSubview(nameLabel)
        view.addSubview(authorLabel)
    }
    
    private func makeNameLabel() -> UILabel {
        let label = UILabel()
        label.font = .preferredFont(forTextStyle: .largeTitle)
        label.textColor = .orange
        return label
    }
}

So far, we’ve been focusing on instance properties, which are all associated with a single instance of a type. But we can also define static properties — ones that are attached to a type itself, rather than to instances of it. Such properties can be really useful when we want to share a given object across all instances of a type, in order to avoid having to recreate it multiple times.

For example, here we’ve defined a static dateFormatter property, which is computed through a self-executing closure:

extension Book {
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()
}

Static properties are also implicitly lazy, in that they’re only computed once they’re accessed for the first time.

We can now use the above date formatter whenever we want to convert a Book-related date into a string, or vice versa:

let string = Book.dateFormatter.string(from: date)

We can also attach observers to any stored property, which enables us to run code each time that a value was (or will be) assigned to that property. For example, here we’re using the didSet property observer to automatically update a label every time that a numberOfStars property was changed:

class RatingViewController: UIViewController {
    var numberOfStars = 0 {
        didSet { starCountLabel.text = String(numberOfStars) }
    }

    private lazy var starCountLabel = UILabel()
    
    ...
}

There’s also a willSet variant that gets triggered before the property’s value was assigned, rather than afterwards. To learn more, check out “Property observers in Swift”.

Property observers can also be used to validate or modify each new value — for example to make sure that a numeric value stays within a certain range, like this:

struct Book {
    let id: UUID
    var name: String
    var author: Author
    var numberOfStars = 0 {
        didSet {
            // If the new value was higher than 9999, we reduce
            // it down to that value, which is our maximum:
            numberOfStars = min(9999, numberOfStars)
        }
    }
}

Finally, properties can also be referred to in more dynamic ways using key paths — which let us pass a reference to a property itself, rather than to its value.

Key paths can also automatically be converted into functions (since Swift 5.2), which means that they’re really useful in situations when we want to extract values for a given property from a collection of instances — for example by using map on an array, like this:

// Converting an array of books into an array of strings, by
// extracting each book's name:
let books = loadBooks() // [Book]
let bookNames = books.map(\.name) // [String]

To learn more about key paths and how they relate to functions, check out this episode of Swift Clips.

Those are just a few examples of the many ways to use properties in Swift, but hopefully this article has either given you a useful recap of some of their basic set of capabilities, or have inspired you to explore some of these topics further — for example using these articles, or by downloading this article’s playground using the link below.

Thanks for reading! 🚀

Support Swift by Sundell by checking out this sponsor:

Paw
Paw

Paw: A GraphQL and REST API client that lets you test and describe the APIs that you call from your app. Just enter the URL of the API endpoint that you’re looking to call, add any headers, parameters, authentication, or body data. Hit return — and everything is automatically checked for you, from the standard OAuth 2 login to very custom API flows.