Skip to content

Adapter ​

Adapter translates an existing interface into the interface your feature wants. It is useful at boundaries with SDKs, old app modules, or platform APIs that are awkward for new code.

When it helps in iOS ​

Suppose a legacy image loader uses a completion handler, while a new SwiftUI feature uses async/await. An adapter lets the feature depend on an app-owned protocol without changing the legacy loader.

Example: bridging a callback image service ​

swift
import Foundation

// Existing service owned by an older module.
protocol LegacyImageLoader {
    func loadImageData(
        from url: URL,
        completion: @escaping (Result<Data, Error>) -> Void
    )
}

// Interface used by new features.
protocol ImageDataFetching {
    func data(for url: URL) async throws -> Data
}

final class LegacyImageAdapter: ImageDataFetching {
    private let loader: any LegacyImageLoader

    init(loader: any LegacyImageLoader) {
        self.loader = loader
    }

    func data(for url: URL) async throws -> Data {
        try await withCheckedThrowingContinuation { continuation in
            loader.loadImageData(from: url) { result in
                continuation.resume(with: result)
            }
        }
    }
}

// A SwiftUI view model can now depend on ImageDataFetching.

The adapter owns the translation. Feature code only sees ImageDataFetching. The legacy callback must complete exactly once; otherwise a checked continuation can trap or remain suspended.

How to use it in a real app ​

Construct the adapter in your app's dependency container and inject ImageDataFetching into the view model. If the legacy service supports cancellation, connect task cancellation to its request token as well. The short bridge above does not cancel underlying work automatically.

Watch for: An adapter should translate an interface, not hide unrelated policy. Image caching, retries, and authentication deserve their own explicit layers.

Trace the call ​

  1. The view model calls data(for:) through the app-owned protocol.
  2. The adapter invokes the legacy callback API.
  3. The callback resumes the suspended task with either bytes or an error.

The continuation must be resumed exactly once on every path. Check the old SDK's behavior for cancellation, duplicate callbacks, and callbacks that never fire. A unit test can supply a fake LegacyImageLoader and verify both success and failure translation. When replacing the SDK later, only the adapter or app composition root needs to change.

Why the bridge is safe only under a contract ​

ImageDataFetching belongs to the new feature, while LegacyImageLoader belongs to old code. LegacyImageAdapter keeps the old name, callback, and error shape on one side and offers async throws on the other. withCheckedThrowingContinuation suspends the calling task; the callback resumes it with the same Result. The adapter does not make the old loader magically cancellable or thread-safe.

From example to an iOS screen ​

The app creates the legacy loader once and injects LegacyImageAdapter into the image view model. A SwiftUI .task can then try await fetcher.data(for:) and publish a loading, success, or failure state. When the view disappears, its task may be cancelled, but the underlying callback request may continue unless the legacy API offers a cancellation token. If it does, add withTaskCancellationHandler and cancel that token. Also ensure the old API calls completion exactly once, including on network failure.

Failure modes and verification ​

Test success bytes, an error, and cancellation behavior using a fake legacy loader that stores the callback. A double callback is a programming error for checked continuations; a missing callback leaves the task suspended. Keep retries and cache policy outside this adapter so its responsibility remains interface translation. A facade would simplify several operations; this adapter translates one incompatible operation.

Next: Decorator →

Released under the MIT License.