Properties
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! 🚀