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?
CodePudding user response:
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)