Articles, podcasts and news about Swift development, by John Sundell.

Mutating and non-mutating Swift contexts

Published on 07 Jul 2021
Basics article available: Value and Reference Types

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

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

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.

Support Swift by Sundell by checking out this sponsor:

RevenueCat

RevenueCat: Easily build and manage iOS and Android in-app purchases. With just a few lines of code RevenueCat provides IAP infrastructure, customer analytics, data integrations, and gives you time back from dealing with edge cases and updates across all platforms.

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!