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)?
    
    func update() {
        let post: RefinedPost = .init(title: "", content: "")
        
        completion?(post)
    }
    
}

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.

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)
            }
        }
    }
    
}

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.