Handling API Dependencies with Chain of Responsibility and Modern Concurrency in Swift
Introduction
When building iOS applications, we often encounter scenarios requiring multiple sequential API calls, where each subsequent call depends on the previous one’s result. A common example might look like this:
Without proper organization, dependencies can quickly become unmanageable. In this context, we’ll explore how this happens and then discuss how to break down complex operations into reusable steps. This brings us to today’s topic: the Chain of Responsibility pattern, combined with Swift’s modern concurrency features, to create a clean and maintainable solution.
Problems with the Traditional Approach
Consider this typical scenario in a banking app:
// Problematic approach
func fetchUserData() async {
do {
let userDetails = try await fetchUserDetails()
let accountInfo = try await fetchAccountInfo(userId: userDetails.userId)
let transactions = try await fetchTransactions(accountId: accountInfo.accountId)
// Handle the results
} catch {
// Handle errors
}
}
Tight Coupling
Hard to Modify
Complex Error Handling
Testing Challenges
The Solution: Chain of Responsibility
The Chain of Responsibility pattern allows us to break this complex operation into discrete, reusable steps. Combined with Swift’s async/await, it creates a clean and maintainable solution.
Step 1: Define the Protocol
First, we create a protocol that each handler will implement:
protocol AsyncAPIHandler {
func handle(input: Any?) async throws -> Any
}
Step 2: Create Model Types
We’ll use Codable for type-safe data handling:
struct UserDetails: Codable {
let userId: String
let username: String
}
struct AccountInfo: Codable {
let accountId: String
let balance: Double
}
struct Transaction: Codable {
let id: String
let amount: Double
}
Step 3: Implement the Handlers
Each handler focuses on one specific API call:
class UserDetailsHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
let url = URL(string: "https://api.example.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(UserDetails.self, from: data)
}
}
class AccountInfoHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
guard let userDetails = input as? UserDetails else {
throw HandlerError.invalidInput
}
let url = URL(string: "https://api.example.com/account/\(userDetails.userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(AccountInfo.self, from: data)
}
}
class TransactionHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
guard let accountInfo = input as? AccountInfo else {
throw HandlerError.invalidInput
}
let url = URL(string: "https://api.example.com/transactions/\(accountInfo.accountId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Transaction].self, from: data)
}
}
Step 4: Create the Chain
The chain class orchestrates the execution:
class AsyncAPIChain {
private var handlers: [AsyncAPIHandler] = []
func addHandler(_ handler: AsyncAPIHandler) {
handlers.append(handler)
}
func execute() async throws -> Any {
var result: Any? = nil
for handler in handlers {
result = try await handler.handle(input: result)
}
return result ?? "No Result"
}
}
Step 5: Integration with SwiftUI
In the first part, we’ll focus on how to set up the AsyncAPIChain with the required handlers and inject it into the ViewModel in the App structure.
领英推荐
import SwiftUI
@main
struct BankingApp: App {
var body: some Scene {
WindowGroup {
// Create the handlers for the chain
let userDetailsHandler = UserDetailsHandler()
let accountInfoHandler = AccountInfoHandler()
let transactionHandler = TransactionHandler()
// Create the AsyncAPIChain with the handlers
let apiChain = AsyncAPIChain()
apiChain.addHandler(userDetailsHandler)
apiChain.addHandler(accountInfoHandler)
apiChain.addHandler(transactionHandler)
// Inject the apiChain into the ViewModel
ContentView(viewModel: ViewModel(apiChain: apiChain))
}
}
}
struct ContentView: View {
@StateObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
_viewModel = StateObject(wrappedValue: viewModel)
}
var body: some View {
VStack {
if let latestTransaction = viewModel.latestTransaction {
Text("Latest Transaction: \(latestTransaction.transactionId), Amount: \(latestTransaction.amount))")
}
Text("Total Transactions: \(viewModel.transactionCount)")
}
.padding()
.task {
await viewModel.fetchData()
}
}
}
Key Highlights:
Now, let’s look at how the ViewModel interacts with the AsyncAPIChain to fetch data and update the view with the results.
import SwiftUI
@MainActor
class ViewModel: ObservableObject {
@Published var transactionCount: Int = 0
@Published var latestTransaction: Transaction? = nil
private let apiChain: AsyncAPIChain
init(apiChain: AsyncAPIChain) {
self.apiChain = apiChain
}
func fetchData() async {
do {
let finalResult = try await apiChain.execute()
if let transactions = finalResult as? [Transaction] {
transactionCount = transactions.count
latestTransaction = transactions.first // Assuming the first is the latest
}
} catch {
result = "Error: \(error.localizedDescription)"
}
}
}
The @MainActor attribute is used to ensure that the ViewModel's properties and methods are accessed and updated on the main thread.
Key Highlights:
Mocking Handlers for Unit Testing
As we showed above, how we design and build the AsyncAPIChain to manage the API dependencies and provide the final result for the ViewModel easily as it is injected from outside, let's now explore how we can mock the handlers and test our ViewModel.
By injecting dependencies such as the AsyncAPIChain into our ViewModel, we decouple the business logic from external systems, allowing us to write clean and maintainable unit tests. This approach helps us simulate different API responses and ensures we can test the ViewModel in isolation, without making real network requests.
Create Mock Handlers
Mock handlers simulate the behavior of the actual handlers used in the AsyncAPIChain. These handlers provide predefined outputs, enabling us to simulate various test scenarios without the need for real network calls.
Example Mock Handler for UserDetailsHandler:
class MockUserDetailsHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
return UserDetails(userId: "mockUserId", username: "mockUser")
}
}
Example Mock Handler for AccountInfoHandler:
class MockAccountInfoHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
guard let userDetails = input as? UserDetails else {
throw HandlerError.invalidInput
}
return AccountInfo(accountId: "mockAccountId", balance: 100)
}
}
Example Mock Handler for TransactionHandler:
class MockTransactionHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
return [
Transaction(id: "mockTx1", amount: 100),
Transaction(id: "mockTx2", amount: 200)
]
}
}
Testable ViewModel
Now that we have mock handlers in place, we can inject them into the ViewModel to simulate API calls during testing. This allows us to test the business logic in isolation without relying on real network data.
Example of a ViewModel Test:
import XCTest
import SwiftUI
class ViewModelTests: XCTestCase {
func testFetchData_Success() async {
// Arrange: Create mock handlers
let mockUserDetailsHandler = MockUserDetailsHandler()
let mockAccountInfoHandler = MockAccountInfoHandler()
let mockTransactionHandler = MockTransactionHandler()
// Create the AsyncAPIChain with the mock handlers
let apiChain = AsyncAPIChain()
apiChain.addHandler(mockUserDetailsHandler)
apiChain.addHandler(mockAccountInfoHandler)
apiChain.addHandler(mockTransactionHandler)
// Inject the chain into the ViewModel
let viewModel = ViewModel(apiChain: apiChain)
// Act: Fetch data
await viewModel.fetchData()
// Assert: Verify the results
XCTAssertEqual(viewModel.transactionCount, 2, "Transaction count should be 2")
XCTAssertEqual(viewModel.latestTransaction?.id, "mockTx1", "The latest transaction ID should be mockTx1")
XCTAssertEqual(viewModel.latestTransaction?.amount, 100, "The latest transaction amount should be 100")
}
func testFetchData_Error() async {
// Arrange: Create a mock handler that throws an error
class MockErrorHandler: AsyncAPIHandler {
func handle(input: Any?) async throws -> Any {
throw NSError(domain: "MockError", code: 123, userInfo: nil)
}
}
// Create the AsyncAPIChain with the mock handlers
let apiChain = AsyncAPIChain()
let mockErrorHandler = MockErrorHandler()
apiChain.addHandler(mockErrorHandler)
// Inject the chain into the ViewModel
let viewModel = ViewModel(apiChain: apiChain)
// Act: Fetch data
await viewModel.fetchData()
// Assert: Verify the error result
XCTAssertEqual(viewModel.result, "Error: The operation couldn’t be completed. (MockError error 123.)", "Error message should be properly set")
}
}
This section shows how we can mock handlers to create testable, isolated code. By doing so, we can efficiently test our ViewModel without relying on external systems.
Benefits
This approach offers several advantages:
Conclusion
The Chain of Responsibility pattern, combined with Swift’s modern concurrency features, provides a clean solution for handling complex API dependencies. This approach scales well as your application grows and makes future modifications easier.
Remember that while this pattern adds some complexity, the benefits of maintainability and flexibility often outweigh the initial setup cost for complex API workflows.
Feel free to adapt this pattern to your specific needs and simplify it for simpler use cases.
Acknowledgments
I want to extend my thanks to the reviewers for their valuable feedback and insights, which helped shape this guide. Special thanks to Kareem Abd Elsattar and Mohamed Goda for their time and expertise in reviewing the content.
Happy coding!?
Senior Software Engineer (iOS) @ Warba Bank
2 个月???? ???? ???? ???? ?? Essam ????
Software Engineer
2 个月?????? ?? ??? fresh ??? ?????? ?
Backend PHP Developer
2 个月?? ???? ?? ???????? ???? .. ??? ???? ?? ???? ????? ?? ?? logs? ?? ?? ????? ??? ???? ???? ??? ???? ????? ?? ??????? ??? ?? ???? ?????? ???? ???? ???? ???? ???? ???? ?? ?????? ?? ???? ?? ??? .. ???? ???? ????? ????? ?????? ?????? ?? ???????
Senior Mobile Developer || Flutter || IOS
2 个月approach ???? ?????? ???? ??? ?? ???????? ??