Adapter Design Pattern in Swift


Greetings, traveler!

We continue to learn about Design Patterns. Next, we will look at Structural Patterns, starting with the Adapter Pattern. What is the purpose of this pattern? It is used when two different entities need to communicate with each other, but their interfaces are not compatible. The Adapter pattern handles this by bridging the two entities, allowing them to interact seamlessly.

Example of usage

Let’s simplify this with an example. Imagine we have an application that receives posts from a server and displays their content. The issue arises when the user interface and the ViewModel expect a slightly different structure than what the service provides. 

// MARK: - Models

struct Post {
    let title: String
    let content: String
    let tags: [String]
    let categories: [String]
}

struct RefinedPost {
    let title: String
    let content: String
}

// MARK: - API Service

final class PostAPIService {
    
    func getPost(_ completion: @escaping (Result<Post, Error>) -> Void) {
        
        let post = Post(
            title: "Post Title",
            content: "Post Content",
            tags: [],
            categories: []
        )
        
        completion(.success(post))
    }
    
}


// MARK: - ViewModel

final class ViewModel {
    
    var completion: ((RefinedPost) -> Void)?
    private let apiService = PostAPIService()
    
    func update() {
        apiService.getPost { result in
            switch result {
            case .success(let post):
            let refinedPost = RefinedPost(title: post.title, content: post.content)
                completion?(refinedPost)
            case .failure(let error):
                break
            }
        }
    }
    
}

To bridge this gap, we can create an adapter. This adapter will seamlessly adapt the service’s interface to the required specifications, allowing the ViewModel to obtain the data it needs, without any additional effort or knowledge of what is happening behind the scenes.

protocol PostManager {
    func getPost(_ completion: @escaping (Result<RefinedPost, Error>) -> Void)
}

final class PostServiceAdapter: PostManager {
    
    private let apiService = PostAPIService()
    
    func getPost(_ completion: @escaping (Result<RefinedPost, Error>) -> Void) {
        
        apiService.getPost { result in
            switch result {
            case .success(let post):
                completion(.success(.init(title: post.title, content: post.content)))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }
    
}

final class ViewModel {
    
    var completion: ((RefinedPost) -> Void)?
    private let manager: PostManager
    
    init(manager: PostManager) {
        self.manager = manager
    }
    
    func update() {
        manager.getPost { [weak self] result in
            switch result {
            case .success(let post):
                self?.completion?(post)
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
    
}

Adapter and Testability

Another practical benefit of using the Adapter pattern is improved testability. By introducing an abstraction between the ViewModel and the underlying API service, we gain several advantages:

  • The service becomes replaceable. The ViewModel no longer depends on PostAPIService directly, which allows you to swap the real implementation for a mock or stub in tests.
  • The ViewModel becomes independent of external APIs. Its behavior can be verified without relying on network calls, data formatting, or any service-level logic.
  • Mocking becomes straightforward. With a simple protocol such as PostManager, you can easily create lightweight mock objects that return predefined results, making your tests more predictable and faster.

This separation of concerns not only makes the codebase cleaner but also leads to more reliable and maintainable tests.

Potential Drawbacks and When Not to Use the Adapter Pattern

Although the Adapter pattern offers clear benefits, it’s not free of trade-offs.

Adding an adapter introduces an extra layer in the call stack. In performance-critical code (e.g., tight rendering loops or high-frequency network processing), this indirection can add measurable overhead and make debugging slightly harder, as you’ll jump through one more class when stepping through the code.

Use an adapter when:

  • You’re integrating legacy or third-party code that you cannot (or should not) modify.
  • You need to support multiple incompatible implementations behind the same protocol.
  • Testability and dependency injection are primary concerns.

Avoid it when the transformation is trivial and the source type is under your control—favor extensions, computed properties, or simple mapping functions instead.

Conclusion

That’s all for now. We’ve just introduced a powerful tool in your software development arsenal — the Adapter pattern. It’s a simple yet effective way to share responsibilities between entities, helping you organize your code and make it more maintainable. In our next article, we’ll delve deeper into Structural Design Patterns and explore the Bridge Pattern.