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
activeTasksafter 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.
Check out other posts in the Design Patterns series:
- Visitor Design Pattern in Swift
- Template Method Design Pattern in Swift
- Strategy Design Pattern in Swift
- State Design Pattern in Swift
- Observer Design Pattern in Swift
- Memento Design Pattern in Swift
- Mediator Design Pattern in Swift
- Iterator Design Pattern in Swift
- Command Design Pattern in Swift
- Chain of Responsibility Design Pattern in Swift
- FlyWeight Design Pattern in Swift
- Facade Design Pattern in Swift
- Decorator Design Pattern in Swift
- Composite Design Pattern in Swift
- Bridge Design Pattern in Swift
- Adapter Design Pattern in Swift
- Singleton Design Pattern in Swift
- Prototype Design Pattern in Swift
- Builder Design Pattern in Swift
- Abstract Factory Design Pattern in Swift
- Factory Method Design Pattern in Swift
- Design Patterns: Basics
