Dependency management in The Composable Architecture using ReducerProtocol

Dependency management in The Composable Architecture using ReducerProtocol

By Atikur Rahman , Lead iOS Developer at Evangelist Apps

Earlier we discussed about the ReducerProtocol , which is a new way to create reducers in The Composable Architecture (TCA). With the release of the ReducerProtocol, the library provides a new dependency management system. Prior to ReducerProtocol, we had to define Environment type to hold all the dependencies for our feature. But, that’s no longer needed. Now reducers are types, so they are more natural place to hold onto dependencies.?

Let’s go through an example to see how to use this new dependency management system. In our example, let’s consider that we need to show a random quote from a web service.

First step is to model interface for our dependency. Let’s declare a struct called ApiClient with a mutable property called getRandomQuote.?

struct ApiClient {
    var getRandomQuote: () async throws -> String
}        

This can be considered as interface for an endpoint that returns a String value.

Next, we register our dependency with the library. For that, we need to conform the ApiClient to the DependencyKey protocol. By conforming to the DependencyKey protocol, we need to provide implementation for the liveValue property.

extension ApiClient: DependencyKey {
    static var liveValue = ApiClient(
        getRandomQuote: {
            // code to get data from web service
            // this code is just for demonstration purpose, ignoring any error handling or best practices.
            let url = URL(string: "https://examplewebservice.com/endpoint")
            let (data, _) = try await URLSession.shared.data(from: url!)
            return String(decoding: data, as: UTF8.self)
        }
    )
}        

Here we make live network requests to get data from the web service.?

Next, we need to add a computed property to DependencyValue with getter and setter implementation.

extension DependencyValues {
    var apiClient: ApiClient {
        get { self[ApiClient.self] }
        set { self[ApiClient.self] = newValue }
    }
}        

Now that we have registered the ApiClient dependency with the library, we can add the dependency using @Dependency property wrapper and use the dependency from the reducer -

struct MyFeature: ReducerProtocol {
    struct State: Equatable {
        var quote = ""
        var isRequestInFlight = false
    }
    
    enum Action: Equatable {
        case getQuoteButtonTapped
        case quoteResponse(String)
    }
    
    @Dependency(\.apiClient) var apiClient

    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        switch action {
        case .getQuoteButtonTapped:
            state.isRequestInFlight = true
            return .run { send in
                try await send(.quoteResponse(apiClient.getRandomQuote()))
            }
            
        case let .quoteResponse(quote):
            state.quote = quote
            state.isRequestInFlight = false
            return .none
        }
    }
}        

The getQuoteButtonTapped simulates tapping of a button on the user interface. When that happens, we send an EffectTask from the getQuoteButtonTapped action case. We use the run static method to construct EffectTask. Within the trailing closure of the?.run method, we perform the network request by using the ApiClient. This happens asynchronously. When the api response is received, the quoteResponse action case is called.

It’s also really easy to write unit tests for the reducers that depends on the external clients. For live application, we would want to make the network request and get actual data. But for writing unit tests, we need to control certain aspects and we would want to use mock clients instead of making live network requests.

?So when we will be writing unit tests for getQuoteButtonTapped, we will want to avoid network calls. It’s really easy to provide a new implementation of the dependency while writing unit tests. We need to provide a trailing closure to TestStore called withDependencies, which allows us to override any dependency we want -

func testRandomQuote() async {
        let dummyQuote = "This is a random quote"
        let store = TestStore(initialState: MyFeature.State()) {
            MyFeature()
        } withDependencies: {
            $0.apiClient.getRandomQuote = { dummyQuote }
        }
        
        await store.send(.getQuoteButtonTapped) {
            $0.isRequestInFlight = true
        }
        await store.receive(.quoteResponse(dummyQuote)) {
            $0.quote = dummyQuote
            $0.isRequestInFlight = false
        }
    }        

As you can see, here we override getRandomQuote implementation and return a static string instead of making network request. This makes it really easy to write unit tests for actions involving side effects.

Thanks for reading!

Please follow us on Twitter and Medium for more updates.

#SwiftUI #S wift.org #evangelistapps #TheComposableArchitecture #swift #iOS #coding #iOS16

要查看或添加评论,请登录

社区洞察

其他会员也浏览了