The power of inout parameters in Swift

Feb 18, 2024 · Follow on Twitter and Mastodon

In Swift, inout parameters can reduce code duplication and the amount of code needed to perform certain tasks. Let’s take a look at some examples.

Background

Swift function parameters are constant, which means that you can’t modify their value from within the function. There are however other things you can do.

You can for instance modify a reference type parameter or make a local, mutable copy of a value type parameter within the function body, but you can’t change the parameter itself.

Marking a parameter as inout lets you pass in a value reference, which lets you change its value from within a function. This is not common, but can be used in certain situations.

Example

As an example, let’s take my open-source project RichTextKit, which has a RichTextEditor that wraps a UIKit/AppKit RichTextView, has a RichTextContext that provides observable state, and uses a RichTextCoordinator to sync changes between the view and the context.

The coordinator observes the context and syncs all changes to the text view, and sets itself up as a text view delegate to sync text view changes to the context.

Code without inout

The current coordinator code for syncing text view changes to the context looks like this:

func syncContextWithTextViewAfterDelay() {
    let styles = textView.richTextStyles

    let string = textView.attributedString
    if context.attributedString != string {
        context.attributedString = string
    }

    let range = textView.selectedRange
    if context.selectedRange != range {
        context.selectedRange = range
    }

    let canRedo = textView.undoManager?.canRedo ?? false
    if context.canRedoLatestChange != canRedo {
        context.canRedoLatestChange = canRedo
    }

    ... and so on
}

There are many problems with the code above. Besides being tedious to read and write, it performs identical checks for each property. It also refers to the local variable and a certain context property multiple times, which can lead to bugs.

For instance, say that we copy and paste a code block to handle another property. We then have to change both properties in two places to avoid checking or setting the wrong value.

We can improve this by introducing an inout-based function.

Code with inout

By using an inout-based function, we can move the equality check and property setter to a single place that is also able to write to the parameter we pass in.

The code for this is very basic:

func sync<T: Equatable>(_ prop: inout T, with value: T) {
    if prop == value { return }
    prop = value
}

With this function in place, we can drastically reduce the amount of code from above:

func syncContextWithTextViewAfterDelay() {
    let font = textView.richTextFont ?? .standardRichTextFont
    sync(&context.attributedString, with: textView.attributedString)
    sync(&context.selectedRange, with: textView.selectedRange)
    sync(&context.canCopy, with: textView.hasSelectedRange)
    sync(&context.canRedoLatestChange, with: textView.undoManager?.canRedo ?? false)
    sync(&context.canUndoLatestChange, with: textView.undoManager?.canUndo ?? false)
    sync(&context.fontName, with: font.fontName)
    sync(&context.fontSize, with: font.pointSize)
    ... and so on
}

Every operation is now a single line, which makes the code it cleaner and less error-prone, since we’re only using the same context property and text view state once per operation.

As you can see, we must use & when calling the inout function, since we’re are actually providing a refrence to a value, not the value itself.

Discussions & More

If you found this interesting and would like to share your thoughts, please comment in the Disqus section below or reply to this tweet or this toot.

Follow on Twitter and Mastodon to be notified when new content & articles are published.