Practical use of the inout parameter in Swift

3 min read · ios · swift · webkit

inout who?

The inout keyword enables pass-by-reference semantics for value types. Mark a parameter inout and the function receives a reference to the original, not a copy. The & at the call site makes mutation explicit:

var request = URLRequest(url: url)
modifyHTTPHeadersIfNeeded(&request)  // `&` signals that request may be modified

Custom HTTP headers in a WKWebView request

When you need to inject custom HTTP headers into WKWebView navigation requests, you run into a constraint: the WKNavigationDelegate method webView(_:decidePolicyFor:) lets you intercept navigations but not modify the underlying request directly. The inout pattern lets you build and apply those mutations before the load happens.

1. Load

public func load(_ request: URLRequest) {
    var newRequest = request
    modifyHTTPHeadersIfNeeded(&newRequest)
    webView.load(newRequest)
}

URLRequest arrives as an immutable value. Assigning it to a var creates a mutable copy; & lets modifyHTTPHeadersIfNeeded write back through it.

2. Modify

private func modifyHTTPHeadersIfNeeded(_ request: inout URLRequest) {
    if isCustomHTTPHeadersAdded(request) {
        return
    }

    var headers = request.allHTTPHeaderFields ?? [:]

    if let providedHeaders = delegate?.provideHeaders() {
        for (key, value) in providedHeaders {
            headers[key] = value
        }
    }

    addRequiredHTTPHeaders(&headers)
    request.allHTTPHeaderFields = headers
}

The early return on isCustomHTTPHeadersAdded is the guard that prevents infinite navigation loops. More on that below.

3. Intercept

public func webView(
    _ webView: WKWebView,
    decidePolicyFor navigationAction: WKNavigationAction
) async -> WKNavigationActionPolicy {
    guard
        let url = navigationAction.request.url,
        isDomainTrusted(url)
    else {
        return .cancel
    }

    var request = navigationAction.request

    if isCustomHTTPHeadersAdded(request) {
        return .allow
    } else {
        var newRequest = navigationAction.request
        modifyHTTPHeadersIfNeeded(&newRequest)
        webView.load(newRequest)
        return .cancel
    }
}

When a navigation fires, check whether custom headers have already been added. If not, cancel the navigation, inject the headers, and reload. The reloaded request passes the check, so the second pass through the delegate returns .allow.

Avoiding infinite loops

isCustomHTTPHeadersAdded is what makes the cancel-and-reload pattern safe:

private func isCustomHTTPHeadersAdded(_ request: URLRequest) -> Bool {
    guard let headers = request.allHTTPHeaderFields else {
        return false
    }

    let customHeaderKey = RequiredHeaders.customAdded
    return headers[customHeaderKey.headerName] == customHeaderKey.headerValue
}

Notice this one takes URLRequest by value, not inout. It only reads headers, never mutates. The & isn’t needed when there’s nothing to write back.

Without this guard, every navigation would be cancelled, modified, and reloaded indefinitely.

Why & matters here

The helper changes a URLRequest, and the caller needs that changed request back. inout keeps that contract visible at both ends: the function signature says it can mutate, and the call site has to pass &request.

That visibility matters in a navigation delegate because one missing guard creates an infinite reload loop. Keep the mutating function narrow, keep the read-only check as a plain value parameter, and the control flow stays obvious.

The same rule applies elsewhere in Swift: reach for inout when mutation is the point of the helper, not when a returned copy would read more clearly.