Search code examples
swiftgenericsswiftuicombine

How to Implement API for both Success and Failure Response with Combine Swift


EDIT: I am trying my level best to make my question simpler, here what I am trying to get a solution for is, I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.

also if my data is wrong the API will fail and it will produce an error response which is a different struct.

with the use of combine, I only could decode a single struct type.

SO how do I make my decode accept any type? Generics is one way I hoped to solve but here the protocol that I need to implement is an issue I believe restricting me from using generics.

thanks for giving it a try.

// MARK: - ResponseStruct Model

struct ResponseStruct: Codable {
   
}

//MARK: -Protocol

public protocol RequestProtocol{
    associatedtype ResponseOutput 
    func fetchFunction() -> AnyPublisher<ResponseOutput, Error>
}

//Mark: - Implementation

struct RequestStruct: Codable, RequestProtocol {
    typealias ResponseOutput = ResponseStruct

    
    func fetchFunction() -> AnyPublisher<ResponseOutput, Error>  {
        let networkManager = NetworkManager()
        do {
            return try networkManager.apiCall(url: url, method: .post, body: JSONEncoder().encode(self))
                .decode(type: ResponseStruct.self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        } catch {
            
        }
    }
}



Above this is the code, and this is fine if the API call works but if the call fails I will get an error response, so how to decode that struct in a combined way? I don't want to write another call for that and I am hoping to get something to do with Failure in the combine. or CAN I MAKE THE associated type (see protocol) generic?


Solution

  • I beg for your patience. I think I understand the problem, but I'm having a hard time lining it up to the code you've given. Your fetchFunction is particularly odd and I don't understand what your protocol is trying to accomplish.

    Let me start with the problem statement and explore a solution. I'll do it step-by-step so this will be a long response. The tl;dr is a Playground at the end.

    I have an API and if my data is valid the API will give the correct response, for which I need to decode with the respective struct in swift.
    If my data is wrong the API will fail and it will produce an error response which is a different struct.
    

    So we need two structs. One for success and one for failure. I'll make some up:

    struct SuccessfulResult : Decodable {
        let interestingText : String
    }
    
    struct FailedResult : Decodable {
        let errorCode : Int
        let failureReason : String
    }
    

    Based on that, request to the network can:

    • Return success data to decode into SuccessfulResult
    • Return failure data to decode into FailedResult
    • Fail because of a low-level error (e.g. The network is unreachable).

    Let's create a type for "The network worked just fine and gave me either success data or failure data":

    enum NetworkData {
        case success(Data)
        case failure(Data)
    }
    

    I'll use Error for low-level errors.

    With those types an API request can be represented as a publisher of the type AnyPublisher<NetworkData, Error>

    But that's not what you asked for. You want to parse the data into SuccessfulResult or FailedResult. This also raises the possibility that JSON parsing fails which I will also sweep under the rug of a generic Error.

    We need a data type to represent the parsed variant of NetworkData:

    enum ParsedNetworkData {
        case success(SuccessfulResult)
        case failure(FailedResult)
    }
    

    Which means the real Network request type you've asked for is a publisher of the type AnyPublisher<ParsedNetworkData,Error>

    We can write a function to transform a Data bearing network request, AnyPublisher<NetworkData,Error>, into an AnyPublisher<ParsedNetworkData,Error>.

    One way to write that function is:

    func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
    
        let decoder = JSONDecoder()
        return networkRequest
            .tryMap { networkData -> ParsedNetworkData in
                switch(networkData) {
                    case .success(let successData):
                        return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
                    case .failure(let failureData):
                        return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
                }
            }
            .eraseToAnyPublisher()
    }
    

    To exercise the code we can write a function to create a fake network request and add some code that tries things out. Putting it all together into a playground you get:

    import Foundation
    import Combine
    
    struct SuccessfulResult : Decodable {
        let interestingText : String
    }
    
    struct FailedResult : Decodable {
        let errorCode : Int
        let failureReason : String
    }
    
    enum NetworkData {
        case success(Data)
        case failure(Data)
    }
    
    enum ParsedNetworkData {
        case success(SuccessfulResult)
        case failure(FailedResult)
    }
    
    func transformRawNetworkRequest(_ networkRequest: AnyPublisher<NetworkData,Error>) -> AnyPublisher<ParsedNetworkData, Error> {
    
        let decoder = JSONDecoder()
        return networkRequest
            .tryMap { networkData -> ParsedNetworkData in
                switch(networkData) {
                    case .success(let successData):
                        return ParsedNetworkData.success(try decoder.decode(SuccessfulResult.self, from: successData))
                    case .failure(let failureData):
                        return ParsedNetworkData.failure(try decoder.decode(FailedResult.self, from: failureData))
                }
            }
            .eraseToAnyPublisher()
    }
    
    func fakeNetworkRequest(shouldSucceed: Bool) -> AnyPublisher<NetworkData,Error> {
        let successfulBody = """
        { "interestingText" : "This is interesting!" }
        """.data(using: .utf8)!
    
        let failedBody = """
        {
          "errorCode" : -4242,
          "failureReason" : "Bogus! Stuff went wrong."
        }
        """.data(using: .utf8)!
    
        return Future<NetworkData,Error> { fulfill in
            let delay = Set(stride(from: 100, to: 600, by: 100)).randomElement()!
    
            DispatchQueue.global(qos: .background).asyncAfter(
                deadline: .now() + .milliseconds(delay)) {
                    if(shouldSucceed) {
                        fulfill(.success(NetworkData.success(successfulBody)))
                    } else {
                        fulfill(.success(NetworkData.failure(failedBody)))
                    }
            }
        }.eraseToAnyPublisher()
    }
    
    var subscriptions = Set<AnyCancellable>()
    let successfulRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: true))
    successfulRequest
        .sink(receiveCompletion:{ debugPrint($0) },
              receiveValue:{ debugPrint("Success Result \($0)") })
        .store(in: &subscriptions)
    
    let failedRequest = transformRawNetworkRequest(fakeNetworkRequest(shouldSucceed: false))
    failedRequest
        .sink(receiveCompletion:{ debugPrint($0) },
              receiveValue:{ debugPrint("Failure Result \($0)") })
        .store(in: &subscriptions)