How to mock DataTaskPublisher? - swift

I'm trying to write some unit tests for my API using URLSession.DataTaskPublisher. I've found an already existing question on Stackoverflow for the same but I'm struggling to implement a working class using the proposed solution.
Here's the existing question: How to mock URLSession.DataTaskPublisher
protocol APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher
}
class APISessionDataTaskPublisher: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
return session.dataTaskPublisher(for: request)
}
var session: URLSession
init(session: URLSession = URLSession.shared) {
self.session = session
}
}
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// How can I return a mocked URLSession.DataTaskPublisher here?
}
}
My API then uses the above like this:
class MyAPI {
/// Shared URL session
private let urlSession: APIDataTaskPublisher
init(urlSession: APIDataTaskPublisher = APISessionDataTaskPublisher(session: URLSession.shared)) {
self.urlSession = urlSession
}
}
What I don't know is how to implement URLSessionMock.dataTaskPublisher().

It would probably be simpler not to mock DataTaskPublisher. Do you really care if the publisher is a DataTaskPublisher? Probably not. What you probably care about is getting the same Output and Failure types as DataTaskPublisher. So change your API to only specify that:
protocol APIProvider {
typealias APIResponse = URLSession.DataTaskPublisher.Output
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError>
}
Conform URLSession to it for production use:
extension URLSession: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
return dataTaskPublisher(for: request).eraseToAnyPublisher()
}
}
And then your mock can create the publisher in any way that's convenient. For example:
struct MockAPIProvider: APIProvider {
func apiResponse(for request: URLRequest) -> AnyPublisher<APIResponse, URLError> {
let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
let data = "Hello, world!".data(using: .utf8)!
return Just((data: data, response: response))
.setFailureType(to: URLError.self)
.eraseToAnyPublisher()
}
}

If you store in UT bundle stub JSON (XML, or something) for every API call that you want to test then the simplest mocking code might look as following
class URLSessionMock: APIDataTaskPublisher {
func dataTaskPublisher(for request: URLRequest) -> URLSession.DataTaskPublisher {
// here might be created a map of API URLs to cached stub replies
let stubReply = request.url?.lastPathComponent ?? "stub_error"
return URLSession.shared.dataTaskPublisher(for: Bundle(for: type(of: self)).url(forResource: stubReply, withExtension: "json")!)
}
}
so instead call to network server your publisher is created with URL of locally stored resource with known data, so you can verify all your workflow.

I will develop the step from having a simple Get request, to mocking .dataTaskPublisher for Combine and for the last part, testing the call. It is a ready to use code, for everyone in case someone else would need it.
Follow the comment to add your model or anything that depends on your project data.
So this is the protocol that give the rules to my NetworkRequest class:
protocol NetworkRequestProtocol {
associatedtype Resource
var resourceURL: NetworkEndpoint { get set }
var resourceSession: URLSession { get set }
func download() -> AnyPublisher<Resource, NetworkError>
}
There is some custom class, NetworkEndpoint and NetworkError, you can add your own here if you want or use URL and URLError instead:
enum NetworkEndpoint {
static let baseURL = URL(string: "API_BASE_URL")! // Add your api base url here
case live
var url: URL {
switch self {
case .live:
return NetworkEndpoint.baseURL!.appendingPathComponent("END_OR_YOUR_API_URL") // Add the end of your API url here
}
}
}
enum NetworkError: LocalizedError {
case addressUnreachable(URL)
case invalidResponse
var errorDescription: String? {
switch self {
case .invalidResponse:
return "The server response is invalid."
case .addressUnreachable(let url):
return "\(url.absoluteString) is unreachable."
}
}
}
Now, I am creating the NetworkRequest class to handle the API call. RessourceSession initializer is used for the UnitTest part only:
final class NetworkRequest<Resource> where Resource: Codable {
var resourceURL: NetworkEndpoint
var resourceSession: URLSession
init(_ resourceURL: NetworkEndpoint,
resourceSession: URLSession = URLSession(configuration: .default)) {
self.resourceURL = resourceURL
self.resourceSession = resourceSession
}
// MARK: - Dispatch Queues
let downloadQueue = DispatchQueue(
label: "downloadQueue", qos: .userInitiated,
attributes: .concurrent, autoreleaseFrequency: .inherit, target: .main)
}
// MARK: - Network Requests
extension NetworkRequest: NetworkRequestProtocol {
func download() -> AnyPublisher<Resource, NetworkError> {
resourceSession
.dataTaskPublisher(for: resourceURL.url)
.receive(on: downloadQueue)
.map(\.data)
.decode(type: Resource.self, decoder: JSONDecoder())
.mapError { error -> NetworkError in
switch error {
case is URLError:
return .addressUnreachable(self.resourceURL.url)
default:
return .invalidResponse }}
.eraseToAnyPublisher()
}
}
For the production code, this is an example of use of the NetworkRequest class, and of course, your model must be Codable:
var subscriptions = Set<AnyCancellable>()
func downloadData() {
NetworkRequest<YOUR_MODEL_NAME>(.live).download() // Add your model name inside the brackets
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
print(error)
case .finished:
break }},
receiveValue: { data in
print(data) })
.store(in: &subscriptions)
}
So now that all the code is setup in the project, we can pass to the UnitTest part and start mocking URLSession:
class MockURLSession: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
init(data: Data?, response: URLResponse?, error: Error?) {
self.data = data
self.response = response
self.error = error
}
override func dataTask(with request: URLRequest,
completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return MockURLSessionDataTask {
completionHandler(data, response, error)
}
}
}
Now, we mock URLSessionDataTask that we return when overriding dataTask in MockURLSession, and it will work for .dataTaskPublisher:
class MockURLSessionDataTask: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
We create fake response data to pass into our tests, but you must create a .json file with your data in it to fetch them in the tests:
class FakeResponseData {
static let response200OK = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!
static let responseKO = HTTPURLResponse(url: URL(string: "https://test.com")!,
statusCode: 500,
httpVersion: nil,
headerFields: nil)!
class RessourceError: Error {}
static let error = RessourceError()
static var correctData: Data {
let bundle = Bundle(for: FakeResponseData.self)
let fakeJsonURL = bundle.url(forResource: "FAKE_JSON_FILE_NAME", withExtension: "json") // Add your fake json file name in here
let fakeJsonData = try! Data(contentsOf: fakeJsonURL!)
return fakeJsonData
}
static let incorrectData = "error".data(using: .utf8)!
}
And to finish, this is the part where you test your NetworkRequest, with the fake data coming from the .json file, or the error. You use resourceSession initializer to add your MockURLSession here and avoid making real network call:
class NetworkRequestTests: XCTestCase {
var expectation: XCTestExpectation!
var subscriptions: Set<AnyCancellable>!
override func setUpWithError() throws {
try super.setUpWithError()
expectation = XCTestExpectation(description: "wait for queue change")
subscriptions = Set<AnyCancellable>()
}
override func tearDownWithError() throws {
subscriptions = nil
expectation = nil
try super.tearDownWithError()
}
func testNetworkRequest_mockURLSessionAddCorrectDataResponse_returnRatesDataModelValues() throws {
let expectedTestValue = "test" // This value is set in your .json fake data for testing
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: FakeResponseData.correctData,
response: FakeResponseData.response200OK,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertEqual(expectedTimestamp, value.InFakeJson) // Compare with your fake json file
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
func testNetworkRequest_mockURLSessionAddServerErrorAsResponse_returnNetworkErrorInvalidResponse() throws {
let expectedNetworkError = NetworkError.invalidResponse.localizedDescription
// This is where you use resourceSession to pass your fake data
let networkRequest = NetworkRequest<RatesData>(.live, resourceSession:
MockURLSession(data: nil,
response: FakeResponseData.responseKO,
error: nil))
networkRequest.download()
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTAssertEqual(expectedNetworkError, error.localizedDescription)
case .finished:
break
}
self.expectation.fulfill() },
receiveValue: { value in
XCTAssertNil(value)
})
.store(in: &subscriptions)
wait(for: [expectation], timeout: 0.1)
}
}

Answered on original question, but will repost here:
Since DataTaskPublisher uses the URLSession it is created from, you can just mock that. I ended up creating a URLSession subclass, overriding dataTask(...) to return a URLSessionDataTask subclass, which I fed with the data/response/error I needed...
class URLSessionDataTaskMock: URLSessionDataTask {
private let closure: () -> Void
init(closure: #escaping () -> Void) {
self.closure = closure
}
override func resume() {
closure()
}
}
class URLSessionMock: URLSession {
var data: Data?
var response: URLResponse?
var error: Error?
override func dataTask(with request: URLRequest, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
let data = self.data
let response = self.response
let error = self.error
return URLSessionDataTaskMock {
completionHandler(data, response, error)
}
}
}
Then obviously you just want your networking layer using this URLSession, I went with a factory to do this:
protocol DataTaskPublisherFactory {
func make(for request: URLRequest) -> URLSession.DataTaskPublisher
}
Then in your network layer:
func performRequest<ResponseType>(_ request: URLRequest) -> AnyPublisher<ResponseType, APIError> where ResponseType : Decodable {
Just(request)
.flatMap {
self.dataTaskPublisherFactory.make(for: $0)
.mapError { APIError.urlError($0)} } }
.eraseToAnyPublisher()
}
Now you can just pass a mock factory in the test using the URLSession subclass (this one asserts URLErrors are mapped to a custom error, but you could also assert some other condition given data/response):
func test_performRequest_URLSessionDataTaskThrowsError_throwsAPIError() {
let session = URLSessionMock()
session.error = TestError.test
let dataTaskPublisherFactory = mock(DataTaskPublisherFactory.self)
given(dataTaskPublisherFactory.make(for: any())) ~> {
session.dataTaskPublisher(for: $0)
}
let api = API(dataTaskPublisherFactory: dataTaskPublisherFactory)
let publisher: AnyPublisher<TestCodable, APIError> =
api.performRequest(URLRequest(url: URL(string: "www.someURL.com")!))
let _ = publisher.sink(receiveCompletion: {
switch $0 {
case .failure(let error):
XCTAssertEqual(error, APIError.urlError(URLError(_nsError: NSError(domain: "NSURLErrorDomain", code: -1, userInfo: nil))))
case .finished:
XCTFail()
}
}) { _ in }
}
The one issue with this is that URLSession init() is deprecated from iOS 13, so you have to live with a warning in your test. If anyone can see a way around that I'd greatly appreciate it.
(Note: I'm using Mockingbird for mocks).

Related

MVC Networking Swift

I have this Networking class that i declared in the Model .
class Networking {
func response (url : String ) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: urlPathCompletionHandler(data:response:error:)).resume()
}
func urlPathCompletionHandler (data : Data? , response: URLResponse? , error: Error? ) {
guard let data = data else {return }
do {
let jsondecoder = JSONDecoder()
}catch {
print("Error \(error)")
}
}
}
In the controller . I have an array of users i declared and i want the controller to call from the Model Networking class instead of doing the networking inside the controller. This is part of my controller:
var users = [Users]()
var networking : Networking()
#IBOutlet weak var tableview : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableview.delegate = self
tableview.dataSource = self
}
func getFromModel() {
var vm = networking.response()
}
I want a way of calling the networking class and return an array of users that i can set to the users array above and use it to populate the table view . If i wanted to do that inside the controller it would easy but i am not sure how i can return an array of users from the Model Networking class .
You need to modify your Network class like this:
class Networking {
func response<T: Codable>(url: String, completion: ((T) -> Void)?) {
guard let url = URL(string: url) else {return}
URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
self.urlPathCompletionHandler(data: data, response: response, error: error, completion: completion)
}).resume()
}
func urlPathCompletionHandler<T: Codable>(data : Data? , response: URLResponse? , error: Error?, completion: ((T) -> Void)?) {
guard let data = data else { return }
do {
let jsondecoder = JSONDecoder()
// Pseudo Code to decode users
completion?(decodedObject)
} catch {
print("Error \(error)")
}
}
}
And call it like this:
func getFromModel() {
networking.response(url: <#T##String#>) { (users: [User]) in
self.users = users
}
}
OK, there are a few thoughts:
Your response method is performing an asynchronous network request, so you need to give it a completion handler parameter. So, I might suggest something like:
class Networking {
enum NetworkingError: Error {
case invalidURL
case failed(Data?, URLResponse?)
}
private let parsingQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".parsing")
// response method to handle network stuff
func responseData(_ string: String, completion: #escaping (Result<Data, Error>) -> Void) {
guard let url = URL(string: string) else {
completion(.failure(NetworkingError.invalidURL))
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
guard
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(NetworkingError.failed(data, response)))
return
}
completion(.success(responseData))
}
}.resume()
}
// response method to handle the JSON parsing
func response<T: Decodable>(of type: T.Type, from string: String, completion: #escaping (Result<T, Error>) -> Void) {
responseData(string) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
self.parsingQueue.async {
do {
let responseObject = try JSONDecoder().decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(responseObject))
}
} catch let parseError {
DispatchQueue.main.async {
completion(.failure(parseError))
}
}
}
}
}
}
}
This obviously assumes that you have some Codable types. For example, it’s common for an API to have some common structure in its responses:
struct ResponseObject<T: Decodable>: Decodable {
let code: Int
let message: String?
let data: T?
}
And maybe the User is like so:
struct User: Decodable {
let id: String
let name: String
}
Then getFromModel (perhaps better called getFromRepository or something like that) could parse it with:
networking.response(of: ResponseObject<[User]>.self, from: urlString) { result in
switch result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
// do something with users
}
}
For what it’s worth, if you didn’t want to write your own networking code, you could use Alamofire, and then getFromModel would do:
AF.request(urlString).responseDecodable(of: ResponseObject<[User]>.self) { response in
switch response.result {
case .failure(let error):
print(error)
case .success(let responseObject):
let users = responseObject.data
}
}
Now, clearly the model types are likely to be different in your example, but you didn’t share what your JSON looked like, so I had to guess, but hopefully the above illustrates the general idea. Make a generic-based network API and give it a completion handler for its asynchronous responses.

How can I return all the response from API to my Swift app

I'am learning swift and I see an example here https://matteomanferdini.com/network-requests-rest-apis-ios-swift/ and Im trying to change the code for something that work for me.
this is how the original code looks
struct Wrapper<T: Decodable>: Decodable {
let items: [T]?
}
protocol NetworkRequest: AnyObject {
associatedtype ModelType
func decode(_ data: Data) -> ModelType?
func load(withCompletion completion: #escaping (ModelType?) -> Void)
}
extension NetworkRequest {
fileprivate func load(_ url: URLRequest, withCompletion completion: #escaping (ModelType?) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: url, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) -> Void in
if let error = error {
print("Error: \(error)")
}
guard let data = data else {
completion(nil)
return
}
completion(self?.decode(data))
})
task.resume()
}
}
class APIRequest<Resource: APIResource> {
let resource: Resource
init(resource: Resource) {
self.resource = resource
}
}
extension APIRequest: NetworkRequest {
func decode(_ data: Data) -> [Resource.ModelType]? {
let wrapper = try? JSONDecoder().decode(Wrapper<Resource.ModelType>.self, from: data)
return wrapper?.items
}
func load(withCompletion completion: #escaping ([Resource.ModelType]?) -> Void) {
load(resource.request, withCompletion: completion)
}
}
but what I need to change the structure Wrapper to
struct Wrapper<T: Decodable>: Decodable {
let items: [T]?
let response: Bool?
let message: String?
}
and return items, response and message not only items
In this case you don't need the protocol at all because you want to get the root object.
This is sufficient
struct Wrapper<T: Decodable>: Decodable {
let items: [T]
let response: Bool
let message: String
}
class NetworkRequest {
func load<T : Decodable>(_ request: URLRequest, withCompletion completion: #escaping (Result<Wrapper<T>,Error>) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: request) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion( Result {try JSONDecoder().decode(Wrapper<T>.self, from: data!)})
}
}
task.resume()
}
}
The completion handler returns a Result object, on success the wrapper object and on failure all errors.
In the wrapper struct declare all properties non-optional to get error messages and change only those to optional which really can be nil.
I change the code like this
class NetworkRequest<Resource: APIResource> {
let resource: Resource
init(resource: Resource) {
self.resource = resource
}
func load(withCompletion completion: #escaping (Result<Wrapper<Resource.ModelType>,Error>) -> Void) {
let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
let task = session.dataTask(with: self.resource.request) { data, _, error in
if let error = error {
completion(.failure(error))
} else {
completion( Result {try JSONDecoder().decode(Wrapper<Resource.ModelType>.self, from: data!)})
}
}
task.resume()
}
}
struct LoginResource: APIResource {
typealias ModelType = Token
let methodPath = "/users/login/"
let method = "post"
var params: [String: Any]?
init(username: String, password: String) {
self.params = ["username":username, "password": password]
}
}
In my view:
func login() {
if user == "" || password == "" {
self.title_alert = "Info"
message_alert = "Test Alert"
show_alert = true
return
}
let loginRequest = NetworkRequest(resource: LoginResource(username:user,password:password))
loginRequest.load { result in
switch result {
case .failure(let error):
print(error)
case .success(let data):
print(data)
}
}
}
I don't know if this is the best way but works Thank you #vadian

Combine SwiftUI Remote Fetch Data - ObjectBinding doesn't update view

I am trying to learn Combine and it is a PITA for me. I never learned RX Swift, so this is all new to me. I am sure I am missing something simple with this one, but hoping for some help.
I am trying to fetch some JSON from an API and load it in a List view. I have a view model that conforms to ObservableObject and updates a #Published property which is an array. I use that VM to load my list, but it looks like the view loads way before this API returns (List showing up blank). I was hoping these property wrappers would do what I thought they were supposed to and re-render the view whenever the object changed.
Like I said, I am sure I am missing something simple. If you can spot it, I would love the help. Thanks!
class PhotosViewModel: ObservableObject {
var cancellable: AnyCancellable?
#Published var photos = Photos()
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: defaultPhotosObject)
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}
}
struct PhotoListView: View {
#EnvironmentObject var photosViewModel: PhotosViewModel
var body: some View {
NavigationView {
List(photosViewModel.photos) { photo in
NavigationLink(destination: PhotoDetailView(photo)) {
PhotoRow(photo)
}
}.navigationBarTitle("Photos")
}
}
}
struct PhotoRow: View {
var photo: Photo
init(_ photo: Photo) {
self.photo = photo
}
var body: some View {
HStack {
ThumbnailImageLoadingView(photo.coverPhoto.urls.thumb)
VStack(alignment: .leading) {
Text(photo.title)
.font(.headline)
Text(photo.user.firstName)
.font(.body)
}
.padding(.leading, 5)
}
.padding(5)
}
}
Based on your updated solution, here are some improvement suggestions (that wont fit in a comment).
PhotosViewModel improvement suggestions
Might I just make the recommendation of changing your load function from returning Void (i.e. returning nothing), to be returning AnyPublisher<Photos, Never> and skipping the last step .assign(to:on:).
One advantage of this is that your code takes one step toward being testable.
Instead of replaceError with some default value you can use catch together with Empty(completeImmediately: <TRUE/FALSE>). Because is it always possible to come up with any relevant default value? Maybe in this case? Maybe "empty photos"? If so then you can either make Photos conform to ExpressibleByArrayLiteral and use replaceError(with: []) or you can create a static var named empty, allowing for replaceError(with: .empty).
To sum up my suggestions in a code block:
public class PhotosViewModel: ObservableObject {
#Published var photos = Photos()
// var cancellable: AnyCancellable? -> change to Set<AnyCancellable>
private var cancellables = Set<AnyCancellable>()
private let urlSession: URLSession
public init(urlSession: URLSession = .init()) {
self.urlSession = urlSession
}
}
private extension PhotosViewModel {}
func populatePhotoCollection(named nameOfPhotoCollection: String) {
fetchPhotoCollection(named: nameOfPhotoCollection)
.assign(to: \.photos, on: self)
.store(in: &cancellables)
}
func fetchPhotoCollection(named nameOfPhotoCollection: String) -> AnyPublisher<Photos, Never> {
func emptyPublisher(completeImmediately: Bool = true) -> AnyPublisher<Photos, Never> {
Empty<Photos, Never>(completeImmediately: completeImmediately).eraseToAnyPublisher()
}
// This really ought to be moved to some APIClient
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return emptyPublisher()
}
return urlSession.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.catch { error -> AnyPublisher<Photos, Never> in
print("☣️ error decoding: \(error)")
return emptyPublisher()
}
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
}
}
*Client suggestion
You might want to write some kind of HTTPClient/APIClient/RESTClient and take a look at HTTP Status codes.
This is a highly modular (and one might argue - overly engineered) solution using a DataFetcher and a DefaultHTTPClient conforming to a HTTPClient protocol:
DataFetcher
public final class DataFetcher {
private let dataFromRequest: (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>
public init(dataFromRequest: #escaping (URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError>) {
self.dataFromRequest = dataFromRequest
}
}
public extension DataFetcher {
func fetchData(request: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
dataFromRequest(request)
}
}
// MARK: Convenience init
public extension DataFetcher {
static func urlResponse(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
_ dataAndUrlResponsePublisher: #escaping (URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError>
) -> DataFetcher {
DataFetcher { request in
dataAndUrlResponsePublisher(request)
.mapError { HTTPError.NetworkingError.urlError($0) }
.tryMap { data, response -> Data in
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPError.NetworkingError.invalidServerResponse(response)
}
headerInterceptor?(httpResponse.allHeaderFields)
guard case 200...299 = httpResponse.statusCode else {
badStatusCodeInterceptor?(UInt(httpResponse.statusCode))
let dataAsErrorMessage = errorMessageFromDataMapper.errorMessage(from: data) ?? "Failed to decode error from data"
print("⚠️ bad status code, error message: <\(dataAsErrorMessage)>, httpResponse: `\(httpResponse.debugDescription)`")
throw HTTPError.NetworkingError.invalidServerStatusCode(httpResponse.statusCode)
}
return data
}
.mapError { castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.eraseToAnyPublisher()
}
}
// MARK: From URLSession
static func usingURLSession(
errorMessageFromDataMapper: ErrorMessageFromDataMapper,
headerInterceptor: (([AnyHashable: Any]) -> Void)?,
badStatusCodeInterceptor: ((UInt) -> Void)?,
urlSession: URLSession = .shared
) -> DataFetcher {
.urlResponse(
errorMessageFromDataMapper: errorMessageFromDataMapper,
headerInterceptor: headerInterceptor,
badStatusCodeInterceptor: badStatusCodeInterceptor
) { urlSession.dataTaskPublisher(for: $0).eraseToAnyPublisher() }
}
}
HTTPClient
public final class DefaultHTTPClient {
public typealias Error = HTTPError
public let baseUrl: URL
private let jsonDecoder: JSONDecoder
private let dataFetcher: DataFetcher
private var cancellables = Set<AnyCancellable>()
public init(
baseURL: URL,
dataFetcher: DataFetcher,
jsonDecoder: JSONDecoder = .init()
) {
self.baseUrl = baseURL
self.dataFetcher = dataFetcher
self.jsonDecoder = jsonDecoder
}
}
// MARK: HTTPClient
public extension DefaultHTTPClient {
func perform(absoluteUrlRequest urlRequest: URLRequest) -> AnyPublisher<Data, HTTPError.NetworkingError> {
return Combine.Deferred {
return Future<Data, HTTPError.NetworkingError> { [weak self] promise in
guard let self = self else {
promise(.failure(.clientWasDeinitialized))
return
}
self.dataFetcher.fetchData(request: urlRequest)
.sink(
receiveCompletion: { completion in
guard case .failure(let error) = completion else { return }
promise(.failure(error))
},
receiveValue: { data in
promise(.success(data))
}
).store(in: &self.cancellables)
}
}.eraseToAnyPublisher()
}
func performRequest(pathRelativeToBase path: String) -> AnyPublisher<Data, HTTPError.NetworkingError> {
let url = URL(string: path, relativeTo: baseUrl)!
let urlRequest = URLRequest(url: url)
return perform(absoluteUrlRequest: urlRequest)
}
func fetch<D>(urlRequest: URLRequest, decodeAs: D.Type) -> AnyPublisher<D, HTTPError> where D: Decodable {
return perform(absoluteUrlRequest: urlRequest)
.mapError { print("☢️ got networking error: \($0)"); return castOrKill(instance: $0, toType: HTTPError.NetworkingError.self) }
.mapError { HTTPError.networkingError($0) }
.decode(type: D.self, decoder: self.jsonDecoder)
.mapError { print("☢️ 🚨 got decoding error: \($0)"); return castOrKill(instance: $0, toType: DecodingError.self) }
.mapError { Error.serializationError(.decodingError($0)) }
.eraseToAnyPublisher()
}
}
Helpers
public protocol ErrorMessageFromDataMapper {
func errorMessage(from data: Data) -> String?
}
public enum HTTPError: Swift.Error {
case failedToCreateRequest(String)
case networkingError(NetworkingError)
case serializationError(SerializationError)
}
public extension HTTPError {
enum NetworkingError: Swift.Error {
case urlError(URLError)
case invalidServerResponse(URLResponse)
case invalidServerStatusCode(Int)
case clientWasDeinitialized
}
enum SerializationError: Swift.Error {
case decodingError(DecodingError)
case inputDataNilOrZeroLength
case stringSerializationFailed(encoding: String.Encoding)
}
}
internal func castOrKill<T>(
instance anyInstance: Any,
toType expectedType: T.Type,
_ file: String = #file,
_ line: Int = #line
) -> T {
guard let instance = anyInstance as? T else {
let incorrectTypeString = String(describing: Mirror(reflecting: anyInstance).subjectType)
fatalError("Expected variable '\(anyInstance)' (type: '\(incorrectTypeString)') to be of type `\(expectedType)`, file: \(file), line:\(line)")
}
return instance
}
This ended up being an issue with my Codable struct not being set up properly. Once I added a default object in the .replaceError method instead of a blank array (Thanks #Asperi), I was able to see the decoding error and fix it. Works like a charm now!
Original:
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: [])
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}
Updated:
func load(user collection: String) {
guard let url = URL(string: "https://api.unsplash.com/users/\(collection)/collections?client_id=\(Keys.unsplashAPIKey)") else {
return
}
cancellable = URLSession.shared.dataTaskPublisher(for: url)
.map { $0.data }
.decode(type: Photos.self, decoder: JSONDecoder())
.replaceError(with: defaultPhotosObject)
.receive(on: RunLoop.main)
.assign(to: \.photos, on: self)
}

Parallel URLSession requests w/ DispatchGroup call completion handler twice on 1 request

Using DispatchGroup I am trying to run 2 network requests against my client, returning the results when both have completed.
I am having an issue in that sometimes the completion handler for one of DispatchGroup requests is called twice and the other is not called at all.
An example would be -
func fetchProfileWithRelatedArticle(onSuccess: #escaping (User, [RelatedArticle]) -> Void, onError: #escaping (Error) -> Void) {
let dispatchGroup = DispatchGroup()
var user: User?
var articles: [RelatedArticle] = []
var errors: [Error] = []
dispatchGroup.enter()
fetchProfileForUser(onSuccess: {
user = $0
print("fetchProfile:",$0)
print("123")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.enter()
getArticlesForUser(onSuccess: {
articles = $0
print("getArticlesForUser:",$0)
print("456")
dispatchGroup.leave()
}, onError: { error in
errors.append(error)
dispatchGroup.leave()
})
dispatchGroup.notify(queue: .main) {
guard let user = user, errors.isEmpty else { return }
onSuccess(user, articles)
}
}
Here I fetch a user profile and also fetch a list of articles they have written. These are returned via a completion handler and presented elsewhere.
Most of the time this works, however it appears on occasion either one of those requests will call its own completion handler twice and the other request wont.
I suspect this may be down to when my access token expires as it occurs if I leave the app for a short time. My access token has a life of 2 minutes.
Should a request receive a 401 response, I have the following method in my network client that requests a new token, then invokes the call again. I believe this may not be working as I'd like.
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
I suspect calling the method again after the update is doing something to the requests my dispatch group is returning.
Is it possible to chain requests in this fashion?
struct NoContent: Codable { }
typealias RefreshHandler = (#escaping (Bool) -> Void) -> ()
typealias TokenGetter = () -> [String: String]
protocol ClientType: class {
associatedtype Route: RouterType
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void)
}
class Client<Route: RouterType>: ClientType {
enum APIError: Error {
case unknown, badResponse, jsonDecoder, other
}
enum TokenError: String, Error {
case expired = "Access Token Expired"
case refused = "Refresh Token Failed"
}
private(set) var session: SessionType
private(set) var tokenGetter: TokenGetter
private(set) var refreshHandler: RefreshHandler
private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601withFractionalSeconds
return decoder
}()
init(session: SessionType, tokenGetter: #escaping TokenGetter, refreshHandler: #escaping RefreshHandler) {
self.session = session
self.tokenGetter = tokenGetter
self.refreshHandler = refreshHandler
}
func request<T: Codable>(_ resource: Route, _ completion: #escaping (Result<T>)-> Void) {
let request = URLRequest(
resource: resource,
headers: tokenGetter()
)
URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard error == nil else { completion(.failure(APIError.unknown)); return }
guard let response = response as? HTTPURLResponse else { completion(.failure(APIError.badResponse)); return }
if response.statusIs401() {
self?.refreshHandler { success in
guard success else { completion(.failure(TokenError.refused)); return }
self?.request(resource, completion)
}
return
}
if response.statusIsSuccess() {
guard let self = self, let data = self.deserializeNoContentResponse(data: data) else { completion(.failure(APIError.badResponse)); return }
do {
let value = try self.decoder.decode(T.self, from: data)
DispatchQueue.main.async {
completion(.success(value))
}
} catch let error {
print(error)
}
return
}
completion(.failure(APIError.other))
}.resume()
}
// some calls return a 200/201 with no data
private func deserializeNoContentResponse(data: Data?) -> Data? {
if data?.count == 0 {
return "{ }".data(using: .utf8)
}
return data
}
}
Sounds like you would need to something like the following in your networking client:
func makeTheRequest(_ completion: CompletionHandler) {
URLSession.shared.dataTask(with: someURL) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
return
}
if httpResponse.statusCode == 401 {
self.refreshToken { success in
if success { self.makeTheRequest(completion) }
}
}
// handle response
completion(whateverDataYouNeedToPass)
}
}
That would make the call, check the response code, refresh the token is needed and if that succeeds it calls the method that should make the response again with the completion handler that was passed to in the first place without calling it first. So the completion handler wouldn't be called until after the API call is made the second time.
Of course, adopt this for your own code, shouldn't be too hard to do

Generic Decoder for Swift using a protocol

I tried to use a generic Json Decoder for all of my models using a protrocol.
//Here the definition of the protocol:
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Decodable) -> T?, completion: #escaping (Result<T, APIError>) -> Void) {.. other Code}
//Here the implementation:
func getData(from endPoint: Endpoint, completion: #escaping (Result<ApiResponseArray<Codable>, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { json -> Decodable in
guard let dataResult = json as? modelData else { return nil }
return dataResult
}, completion: completion)
}
ApiResponseArray gives me the error: Protocol type 'Codable' (aka 'Decodable & Encodable') cannot conform to 'Decodable' because only concrete types can conform to protocols. But how can I implement a generic decoder and passing them different models. I think I have to modify my protocol definition but how? I would like to pass the model and then receive the decoded data for the model (in my example modelData). Its obvious that the program runs when I write:
func getData(from endPoint: Endpoint, completion: #escaping (Result, APIError>) I mean when I use the concrete Model, but I want to pass the model, so that I can use the class for different models.
Thanks,
Arnold
A protocol cannot conform to itself, Codable must be a concrete type or can only be used as a generic constraint.
In your context you have to do the latter, something like this
func fetch<T: Decodable>(with request: URLRequest, decode: #escaping (Data) throws -> T, completion: #escaping (Result<T, APIError>) -> Void) { }
func getData<T: Decodable>(_ : T.Type = T.self, from endPoint: Endpoint, completion: #escaping (Result<T, APIError>) -> Void) {
let request = endPoint.request
fetch(with: request, decode: { data -> T in
return try JSONDecoder().decode(T.self, from: data)
}, completion: completion)
}
A network request usually returns Data which is more reasonable as parameter type of the decode closure
I can suggest to you how to use Decodable with your API call structure by using Alamofire.
I have created RequestManager class which inherits from SessionManager and added request call inside which common to all.
class RequestManager: SessionManager {
// Create shared instance
static let shared = RequestManager()
// Create http headers
lazy var httpHeaders : HTTPHeaders = {
var httpHeader = HTTPHeaders()
httpHeader["Content-Type"] = "application/json"
httpHeader["Accept"] = "application/json"
return httpHeader
}()
//------------------------------------------------------------------------------
// MARK:-
// MARK:- Request Methods
//------------------------------------------------------------------------------
func responseRequest(_ url: String, method: Alamofire.HTTPMethod, parameter: Parameters? = nil, encoding: ParameterEncoding, header: HTTPHeaders? = nil, completionHandler: #escaping (DefaultDataResponse) -> Void) -> Void {
self.request(url, method: method, parameters: parameter, encoding: encoding, headers: header).response { response in
completionHandler(response)
}
}
}
Then after one more class created NetworkManager class which hold required get/post method call and decode json by JSONDecoder as follow:
class NetworkManager {
static let shared = NetworkManager()
var progressVC : ProgressVC?
//----------------------------------------------------------------
// MARK:-
// MARK:- Get Request Method
//----------------------------------------------------------------
func getResponse<T: Decodable>(_ url: String, parameter: Parameters? = nil, encoding: ParameterEncoding = URLEncoding.default, header: HTTPHeaders? = nil, showHUD: HUDFlag = .show, message: String? = "Please wait...", decodingType: T.Type, completion: #escaping (Decodable?, APIError?) -> Void) {
DispatchQueue.main.async {
self.showHideHud(showHUD: showHUD, message: "")
}
RequestManager.shared.responseRequest(url, method: .get, parameter: parameter, encoding: encoding, header: header) { response in
DispatchQueue.main.async {
self.showHideHud(showHUD: .hide, message: "")
}
guard let httpResponse = response.response else {
completion(nil, .requestFailed("Request Failed"))
return
}
if httpResponse.statusCode == 200 {
if let data = response.data {
do {
let genericModel = try JSONDecoder().decode(decodingType, from: data)
completion(genericModel, nil)
} catch {
do {
let error = try JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any]
if let message = error!["message"] as? String {
completion(nil, .errorMessage(message)!)
} else if let message = error!["message"] as? Int {
completion(nil, .errorMessage(String(describing: "Bad Request = \(message)")))
}
} catch {
completion(nil, .jsonConversionFailure("JSON Conversion Failure"))
}
}
} else {
completion(nil, .invalidData("Invalid Data"))
}
} else {
completion(nil, .responseUnsuccessful("Response Unsuccessful"))
}
}
}
}
ProgressVC is my custom class to show progress view when api call.
After that, I have created DataManager class which will help me to create request url.
class DataManager: NSObject {
//------------------------------------------------------------------------------
// MARK:- Variables
//------------------------------------------------------------------------------
static let shared = DataManager()
let baseUrl = WebServiceURL.local
//------------------------------------------------------------------------------
// MARK:- Custom Methods
//------------------------------------------------------------------------------
// Get API url with endpoints
func getURL(_ endpoint: WSEndPoints) -> String {
return baseUrl + endpoint.rawValue
}
}
I have created following enum to send data or error in my completion block.
enum Result<T, U> where U: Error {
case success(T)
case failure(U)
}
Here is list of error which stored custom message related to status fired during api call.
enum APIError: Error {
case errorMessage(String)
case requestFailed(String)
case jsonConversionFailure(String)
case invalidData(String)
case responseUnsuccessful(String)
case jsonParsingFailure(String)
var localizedDescription: String {
switch self {
case.errorMessage(let msg):
return msg
case .requestFailed(let msg):
return msg
case .jsonConversionFailure(let msg):
return msg
case .invalidData(let msg):
return msg
case .responseUnsuccessful(let msg):
return msg
case .jsonParsingFailure(let msg):
return msg
}
}
}
Then after, I will extend this DataManager class to call web service based on module. So I will create Swift file and extend DataManager class and call relative API.
See following, In API call I will return relative model into Result like Result<StoreListModel, APIError>
extension DataManager {
// MARK:- Store List
func getStoreList(completion: #escaping (Result<StoreListModel, APIError>) -> Void) {
NetworkManager.shared.getResponse(getURL(.storeList), parameter: nil, encoding: JSONEncoding.default, header: getHeaders("bd_suvlascentralpos"), showHUD: .show, message: "Please wait...", decodingType: StoreListModel.self) { (decodableData, apiError) in
if apiError != nil {
completion(.failure(apiError!))
} else {
guard let userData = decodableData as? StoreListModel else {
completion(.failure(apiError!))
return
}
completion(.success(userData))
}
}
}
}
From completion block of request I will get decodable data which here safely type cast.
Use:
DataManager.shared.getStoreList { (result) in
switch result {
case .success(let storeListModel):
if let storeList = storeListModel, storeList.count > 0 {
self.arrStoreList = storeList
self.tblStoreList.isHidden = false
self.labelEmptyData.isHidden = true
self.tblStoreList.reloadData()
} else {
self.tblStoreList.isHidden = true
self.labelEmptyData.isHidden = false
}
break
case .failure(let error):
print(error.localizedDescription)
break
}
}
Note:- Some variables, models classes are my custom. You can replace it with your.