Interior Mutability in Swift

📆 Published 2021-04-26 ⏰ 4m read

I'm working on a project at the moment where I need to start some task as an "anonymous" user (i.e. somebody without an account who's just downloaded the app) and then allow them to authenticate, and complete the task as an authenticated user, remembering all the actions they took before, but now with additional privileges.

These anonymous users are actually authenticated behind the scenes with some anonymous user ID, allowing conversions can be more accurately tracked. There is also other benefits to this approach, such as making sure the API is not publically exposed to anyone the internet. Therefore, we have the concept of a user of the app.

protocol User {
    var uid: String { get }
    var apiClient: APIClient { get }
}

For example, the implementation of an anonymous user could take a simple form which is essentially is just a wrapper to access the API.

class AnonymousUser: User {
   let uid: String
   let apiClient: APIClient
   // initialised once a uid is known from some authenticating service
   init(uid: String) {
      self.uid = uid
      self.apiClient = APIClient(uid: uid)
   }
}

Authenticated users take a very similar form, maybe with some additional state contained within it, for example.

class AuthenticatedUser: User {
   let uid: String
   let apiClient: APIClient
   // initialised once a uid is known from some authenticating service
   init(uid: String) {
      self.uid = uid
      self.apiClient = APIClient(uid: uid)
   }
   // other properties, unique to authenticated users...
   var friends = 0
   var enemies = 0
}

Now, we need to have some idea of a session that can keep track of the user and the tasks they're performing.

class Session {
    var user: User
    private let sessionHelper: SessionHelper
    init(user: User) {
        self.user = user
        self.sessionHelper = SessionHelper(user: user)
    }
    // user session....
    var likesPuppies = false
    var firstName = "John"
}

// in the startup of the app...
let uid = AuthService.getAnonymousUserId()
let anonymousUser = AnonymousUser(uid: uid)
let userInfoSession = Session(user: anonymousUser)
// use the session!

Right, so we have our users and our session. Now, when the anonymous user becomes authenticated we can just fetch their new authenticated user id, create an AuthenticatedUser object and reassign the session's user.

Not so fast. If nested class instances also refer to the User, they will have their own references to the user object, which will not be automatically updated when we reassign the user. For example, you may have noticed the private SessionHelper above. It contains logic and helpers for the session, internal to the session and unimportant for a consumer of this API (don't worry about the details). Importantly, it also refers to the same User instance as was assigned to the session. We want a way to ensure that this user is updated for anyone that refered to the original instance.

let newUid = AuthService.getAuthenticatedUserId()
let authenticatedUser = AuthenticatedUser(uid: newUid)
userInfoSession.user = authenticatedUser
// 😬 OOPS!
// the `sessionHelper` was not updated
// it still refers to the old, anonymous user!

We could work around this by adding a property observer to the Session's user property, but that requires us to keep it updated if other properties come along in the future, which harms maintainability.

// inside 'Session'
var user: User {
    didSet {
        sessionHelper.user = user
    }
}

There's other issues with this approach, for example, it won't work if the SessionHelper has an immutable reference to the user or any other dependency in the future takes an immutable reference. Additionally, if the SessionHelper itself also has internal other references to the User, there will need to be additional logic to update those as well.

A possible solution is interior mutability, which is the concept of being able to modify internal values, even when an immutable (let) reference is held to a given value. It allows us to pass a given reference around to many consumers, then update the underlying value without affecting the original reference. This means that all consumers get the newly modified value for free, no manual updating required.

We can make use of this pattern in Swift by simply using a reference type (class). Classes are passed by reference, so even if we hold an immutable reference to a class, we can still update the inner values while maintaining the original reference.

class UserHolder {
    var user: User
    init(_ user: User) {
        self.user = user
    }
}

Now instead of passing a User, we pass a UserHolder instead.

final class Session {
    var user: UserHolder
    private let sessionHelper: SessionHelper
    init(user: UserHolder) {
        self.user = user
        self.sessionHelper = SessionHelper(user: user)
    }
    // user session....
    var likesPuppies = false
    var firstName = "John"
}

// in the startup of the app...
let uid = AuthService.getAnonymousUserId()
let anonymousUser = AnonymousUser(uid: uid)
let heldUser = UserHolder(anonymousUser)
let userInfoSession = Session(user: heldUser)
// use the session!

Now, when the user is updated, we just need to update the user property of UserHolder and all places that refer to the UserHolder will see the updated user right away!

let newUid = AuthService.getAuthenticatedUserId()
let authenticatedUser = AuthenticatedUser(uid: newUid)
// update the inner value of the UserHolder
userInfoSession.user.user = authenticatedUser
// YES! we know it's updated everywhere that takes a UserHolder

This is a great win for reducing verbosity, but there's now an additional layer of indirection between the UserHolder and the User. Thankfully, Swift has a feature called dynamic member lookup, allowing a base type to look up arbitrary properties at runtime. We can make use of this feature, along with generics, to arrive at a generalised "Box" that can contain another type and provide interior mutability.

@dynamicMemberLookup
class Box<T> {
    var value: T
    init(_ value: T) {
        self.value = value
    }
    subscript<R>(dynamicMember keyPath: KeyPath<T, R>) -> R {
        value[keyPath: keyPath]
    }
}

Now using Box<User> in place of UserHolder above more clearly describes the semantics of this container type and allows it to be re-used among any type. We can also directly access the properties of the inner type, thanks to @dynamicMemberLookup. How cool is that!