Mutating and non-mutating Swift contexts
Basics article available: Value and Reference TypesOne of the ways in which Swift helps us write more robust code is through its concept of value types, which limit the way that state can be shared across API boundaries. That’s because, when using value types, all mutations are (by default) only performed to local copies of the values that we’re working with, and APIs that actually perform mutations have to be clearly marked as mutating
.
In this article, let’s explore that keyword, as well as its nonmutating
counterpart, and the sort of capabilities that those language features provide.
What can a mutating function do?
Essentially, a function that’s been marked as mutating
can change any property within its enclosing value. The word “value” is really key here, since Swift’s concept of structured mutations only applies to value types, not to reference types like classes and actors.
For example, the following Meeting
type’s cancel
method is mutating, since it modifies its enclosing type’s state
and reminderDate
properties:
struct Meeting {
var name: String
var state: MeetingState
var reminderDate: Date?
...
mutating func cancel(withMessage message: String) {
state = .cancelled(message: message)
reminderDate = nil
}
}
Besides modifying properties, mutating contexts can also assign a brand new value to self
, which can be really useful when adding a mutating method to an enum (which can’t contain any stored instance properties). For example, here we’re creating an API for making it easy to appending one Operation
to another:
enum Operation {
case add(Item)
case remove(Item)
case update(Item)
case group([Operation])
}
extension Operation {
mutating func append(_ operation: Operation) {
self = .group([self, operation])
}
}
The above technique also works for other value types, such as structs, which can be really useful if we ever want to reset a value back to its default set of properties, or if we want to mutate a more complex value as a whole — for example like this:
struct Canvas {
var backgroundColor: Color?
var foregroundColor: Color?
var shapes = [Shape]()
var images = [Image]()
mutating func reset() {
self = Canvas()
}
}
The fact that we can assign a brand new value to self
within a mutating function might initially seem a bit strange, but we have to remember that Swift structs are really just values — so just like how we can replace an Int
value by assigning a new number to it, we can do the same thing with any other struct (or enum) as well.
Mutating protocol requirements
Although the concept of separating mutating and non-mutating APIs is something that’s unique to value types, we can still make a mutating
function a part of a protocol as well — even if that protocol might end up being adopted by a reference type, such as a class. Classes can simply omit the mutating
keyword when conforming to such a protocol, since they are inherently mutable.
What’s very interesting, though, is that if we extend a protocol with a default implementation of a mutating function, then we could implement things like the above reset
API without actually knowing what type of value that we’re resetting — like this:
protocol Resettable {
init()
mutating func reset()
}
extension Resettable {
mutating func reset() {
self = Self()
}
}
struct Canvas: Resettable {
var backgroundColor: Color?
var foregroundColor: Color?
var shapes = [Shape]()
var images = [Image]()
}
Performing mutations within initializers
While functions always need to be explicitly marked as mutating whenever we want to modify a value type’s internal state (whether that’s a property, or the entire value itself), initializers are always mutating by default. That means that, besides assigning initial values to a type’s properties, an initializer can also call mutating methods to perform its work (as long as self
has been fully initialized beforehand).
For example, the following ProductGroup
calls its own add
method in order to add all of the products that were passed into its initializer — which makes it possible for us to use a single code path for that logic, regardless of whether it’s being run as part of the initialization process or not:
struct ProductGroup {
var name: String
private(set) var products = [Product]()
private(set) var totalPrice = 0
init(name: String, products: [Product]) {
self.name = name
products.forEach { add($0) }
}
mutating func add(_ product: Product) {
products.append(product)
totalPrice += product.price
}
}
Just like mutating functions, initializers can also assign a value directly to self
. Check out this quick tip for an example of that.
Non-mutating properties
So far, all of the examples that we’ve been taking a look at have been about mutable contexts, but Swift also offers a way to mark certain contexts as explicitly non-mutating as well. While the use cases for doing so are certainly more limited compared to opting into mutations, it can still be a useful tool in certain kinds of situations.
As an example, let’s take a look at this simple SwiftUI view, which increments an @State
-marked value
property every time that a button was tapped:
struct Counter: View {
@State private var value = 0
var body: some View {
VStack {
Text(String(value)).font(.largeTitle)
Button("Increment") {
value += 1
}
}
}
}
Now, if we look at the above not just as a SwiftUI view, but rather as a standard Swift struct (which it is), it’s actually quite strange that our code compiles. How come we can mutate our value
property like that, within a closure, that’s not being called within a synchronous, mutable context?
The mystery continues to thicken if we then take a look at the declaration of the State
property wrapper, which is also a struct, just like our view:
@frozen @propertyWrapper public struct State<Value>: DynamicProperty {
...
}
So how come a struct-based property wrapper, that’s used within a struct-based view, can actually be mutated within a non-mutating context? The answer lies within the declaration of the State
wrapper’s wrappedValue
, which has been marked with the nonmutating
keyword:
public var wrappedValue: Value { get nonmutating set }
Although this is as far as we’re able to investigate without access to SwiftUI’s source code, State
very likely uses some form of reference-based storage under the hood, which in turn makes it possible for it to opt out of Swift’s standard value mutation semantics (using the nonmutating
keyword) — since the State
wrapper itself is not actually being mutated when we assign a new property value.
If we wanted to, this is a capability that we could add to some of our own types as well. To demonstrate, the following PersistedFlag
wrapper stores its underlying Bool
value using UserDefaults
, meaning that when we assign a new value to it (through its wrappedValue
property), we’re not actually performing any value-based mutations here either. So that property can be marked as nonmutating
, which gives PersistedFlag
the same mutation capabilities as State
:
@propertyWrapper struct PersistedFlag {
var wrappedValue: Bool {
get {
defaults.bool(forKey: key)
}
nonmutating set {
defaults.setValue(newValue, forKey: key)
}
}
var key: String
private let defaults = UserDefaults.standard
}
So just like @State
-marked properties, any property that we mark with @PersistedFlag
can now be written to even within non-mutating contexts, such as within escaping closures. What’s very important to note, though, is that the nonmutating
keyword sort of lets us circumvent key aspects of Swift’s value semantics, so it’s definitely something that should only be used within very specific situations.
Conclusion
I hope that this article has given you a few insights into what separates a mutating context from a non-mutating one, what sort of capabilities that a mutating context actually has, and what Swift’s relatively new nonmutating
keyword does.
If you have any questions, comments, or feedback, then feel free to reach out via either Twitter or email.
Thanks for reading!