Proxy Design Pattern in Swift


Greetings, traveler!

We continue to analyze Design Patterns, and today, we will focus on the latest Structural Pattern, a Proxy.

A Proxy is used in different situations. For instance, you might have a service that you only want to start under specific conditions. In this case, you could use a Proxy to check if the conditions are met before allowing the service to start.

But a Proxy’s versatility doesn’t end there. It can also log or cache data or check conditions for running specific functions.

What proxy actually is

A proxy is an object that stands between the client and a real service.

It implements the same interface as the real object, but adds control logic around the call. The client does not need to know whether it talks to a proxy or the real implementation.

You can use this mental model: a proxy is a gatekeeper.

When proxy makes sense

Proxy is useful when the real service is:

  • expensive
  • slow
  • remote
  • sensitive (requires permissions)
  • worth caching
  • easy to misuse from different places

You can solve some of these problems with ad-hoc checks in view models or services. But those checks tend to spread across the codebase. A proxy keeps them in one place.

Common proxy types

In real apps, proxy usually shows up in a few classic forms.

Virtual proxy (lazy initialization)

You delay creating a heavy object until you actually need it.

Protection proxy

You control access based on permissions or user state.

This is especially common with subscriptions: the same UI calls the same API, but the proxy decides whether the user is allowed to see the result.

Caching proxy

You store results so repeated calls don’t trigger the expensive operation again.

Remote proxy

You interact with a remote service through a local object, hiding the details of the remote call.

For instance, the “real object” can be a network client.

Example 1: preventing duplicate downloads

Consider a DownloadManager that downloads a file by URL.

The problem: if the user taps the same button multiple times, you don’t want multiple downloads for the same URL.

You could add this check inside the manager itself, but that forces every use case into the same behavior. Sometimes you do want parallel requests. Sometimes you want to cancel the previous request. This is not a manager’s job, but a policy decision. So we keep the manager “dumb”, and put the policy into a proxy.

protocol DownloadManagerProtocol {
    func download(url: URL) async throws -> (fileURL: URL, response: URLResponse)
}
final class DownloadManager: DownloadManagerProtocol {

    func download(url: URL) async throws -> (URL, URLResponse) {
        let (fileURL, response) = try await URLSession.shared.download(from: url)
        return (fileURL, response)
    }
}

Now the proxy.

This proxy cancels an existing task for the same URL before starting a new one:

actor DownloadManagerProxy: DownloadManagerProtocol {

    private let manager: DownloadManagerProtocol
    private var activeTasks: [URL: Task<(URL, URLResponse), Error>] = [:]

    init(manager: DownloadManagerProtocol) {
        self.manager = manager
    }

    func download(url: URL) async throws -> (URL, URLResponse) {
        if let task = activeTasks[url] {
            task.cancel()
            activeTasks[url] = nil
        }

        let task = Task {
            try await manager.download(url: url)
        }

        activeTasks[url] = task

        do {
            let result = try await task.value
            activeTasks[url] = nil
            return result
        } catch {
            activeTasks[url] = nil
            throw error
        }
    }
}
  • we clean up activeTasks after completion
  • we keep the cancellation logic outside the real downloader

This is proxy at its best: same interface, extra control, no changes to the real service.

Example 2: subscription gating with protection proxy

Another common case: premium features.

The UI wants to call something like display(text:). Sometimes it should work, sometimes it should show a paywall.

A protection proxy makes this explicit.

The protocol stays simple:

protocol DisplayProtocol {
    func display(text: String)
}

The real implementation:

final class DisplayManager: DisplayProtocol {

    func display(text: String) {
        print("Displayed: \(text)")
    }
}

A subscription service:

final class SubscriptionManager {
    var hasPremium: Bool = false
}

Now the proxy:

final class DisplayManagerProxy: DisplayProtocol {

    private let manager: DisplayProtocol
    private let subscription: SubscriptionManager

    init(manager: DisplayProtocol, subscription: SubscriptionManager) {
        self.manager = manager
        self.subscription = subscription
    }

    func display(text: String) {
        guard subscription.hasPremium else {
            print("User is not premium. Showing paywall.")
            return
        }

        manager.display(text: text)
    }
}

Notice what we got:

  • same public API
  • one clear place for the access rule
  • the client does not care what it is talking to

In a real app, instead of print, the proxy might:

  • trigger navigation to a paywall
  • send analytics
  • log the attempt
  • show a toast

Proxy vs decorator

Proxy and decorator look similar because both usually wrap another object. The difference is why they exist. Decorator exists to attach extra behavior to an object, like adding features. Proxy exists to control access.

Caching, rate limiting, permission checks, lazy initialization — these are proxy territory. When the main job is “let it pass / block it / delay it / reuse it”, you are thinking in terms of proxy.

Conclusion

Proxy solves the kind of problems that you will face in iOS apps:

  • duplicated work
  • uncontrolled side effects
  • permission logic scattered across the codebase
  • expensive operations triggered too often

If you keep the real service focused and put the policies into a proxy, your codebase stays easier to reason about.

Of course, there are many more great examples, but I think the overall idea is clear, so let’s wrap up this discussion of the Proxy design pattern and move on to the next topic, Behavioral Design Patterns. The first one we will discuss is the Chain of Responsibility pattern. See you in the following article.