MVC Networking Swift - 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.

Related

Value after Firestore operation 0 || Swift [duplicate]

I have created a utility class in my Swift project that handles all the REST requests and responses. I have built a simple REST API so I can test my code. I have created a class method that needs to return an NSArray but because the API call is async I need to return from the method inside the async call. The problem is the async returns void.
If I were doing this in Node I would use JS promises but I can't figure out a solution that works in Swift.
import Foundation
class Bookshop {
class func getGenres() -> NSArray {
println("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
println(urlPath)
let url: NSURL = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
var resultsArray:NSArray!
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error) {
println(error.localizedDescription)
}
var err: NSError?
var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
if(err != nil) {
println("JSON Error \(err!.localizedDescription)")
}
//NSLog("jsonResults %#", jsonResult)
let results: NSArray = jsonResult["genres"] as NSArray
NSLog("jsonResults %#", results)
resultsArray = results
return resultsArray // error [anyObject] is not a subType of 'Void'
})
task.resume()
//return "Hello World!"
// I want to return the NSArray...
}
}
You can pass callback, and call callback inside async call
something like:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
and then call this method:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Introduced in Swift 5.5 (iOS 15, macOS 12), we would now use the async-await pattern:
func fetchGenres() async throws -> [Genre] {
…
let (data, _) = try await URLSession.shared.dataTask(for: request)
return try JSONDecoder().decode([Genre].self, from: data)
}
And we would call it like:
let genres = try await fetchGenres()
The async-await syntax is far more concise and natural than the traditional completion handler pattern outlined in my original answer, below.
For more information, see Meet async/await in Swift.
The historic pattern is to use completion handlers closure.
For example, we would often use Result:
func fetchGenres(completion: #escaping (Result<[Genre], Error>) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(.success(results))
}
}.resume()
}
And you’d call it like so:
fetchGenres { results in
switch results {
case .failure(let error):
print(error.localizedDescription)
case .success(let genres):
// use `genres` here, e.g. update model and UI
}
}
// but don’t try to use `genres` here, as the above runs asynchronously
Note, above I’m dispatching the completion handler back to the main queue to simplify model and UI updates. Some developers take exception to this practice and either use whatever queue URLSession used or use their own queue (requiring the caller to manually synchronize the results themselves).
But that’s not material here. The key issue is the use of completion handler to specify the block of code to be run when the asynchronous request is done.
Note, above I retired the use of NSArray (we don’t use those bridged Objective-C types any more). I assume that we had a Genre type and we presumably used JSONDecoder, rather than JSONSerialization, to decode it. But this question didn’t have enough information about the underlying JSON to get into the details here, so I omitted that to avoid clouding the core issue, the use of closures as completion handlers.
Swiftz already offers Future, which is the basic building block of a Promise. A Future is a Promise that cannot fail (all terms here are based on the Scala interpretation, where a Promise is a Monad).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Hopefully will expand to a full Scala-style Promise eventually (I may write it myself at some point; I'm sure other PRs would be welcome; it's not that difficult with Future already in place).
In your particular case, I would probably create a Result<[Book]> (based on Alexandros Salazar's version of Result). Then your method signature would be:
class func fetchGenres() -> Future<Result<[Book]>> {
Notes
I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.
I recommend parsing all the way down to a Book object before returning your results as a Future. There are several ways this system can fail, and it's much more convenient if you check for all of those things before wrapping them up into a Future. Getting to [Book] is much better for the rest of your Swift code than handing around an NSArray.
Swift 4.0
For async Request-Response you can use completion handler. See below I have modified the solution with completion handle paradigm.
func getGenres(_ completion: #escaping (NSArray) -> ()) {
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult["genres"] as! NSArray
print(results)
completion(results)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
You can call this function as below:
getGenres { (array) in
// Do operation with array
}
Swift 3 version of #Alexey Globchastyy's answer:
class func getGenres(completionHandler: #escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
Swift 5.5, async/wait-based solution
The original test URL provided by the original poster is no longer functional, so I had to change things a bit. This solution is based on a jokes API I found. That API returns a single joke, but I return it as an array of String ([String]), to keep it as consistent as possible with the original post.
class Bookshop {
class func getGenres() async -> [String] {
print("Hello inside getGenres")
let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
typealias Continuation = CheckedContinuation<[String], Never>
let genres = await withCheckedContinuation { (continuation: Continuation) in
let task = session.dataTask(with: url) { data, response, error in
print("Task completed")
var result: [String] = []
defer {
continuation.resume(returning: result)
}
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
print("jsonResult is \(jsonResult)")
if let joke = (jsonResult as? [String: String])?["joke"] {
result = [joke]
}
} catch {
print("JSON Error \(error.localizedDescription)")
print("data was \(String(describing: String(data: data, encoding: .utf8)))")
return
}
}
task.resume()
}
return genres
}
}
async {
let final = await Bookshop.getGenres()
print("Final is \(final)")
}
The withCheckedContinuation is how you made the Swift async function actually run in a separate task/thread.
I hope you're not still stuck on this, but the short answer is that you can't do this in Swift.
An alternative approach would be to return a callback that will provide the data you need as soon as it is ready.
There are 3 ways of creating call back functions namely:
1. Completion handler
2. Notification
3. Delegates
Completion Handler
Inside set of block is executed and returned when source is available, Handler will wait until response comes so that UI can be updated after.
Notification
Bunch of information is triggered over all the app, Listner can retrieve n make use of that info. Async way of getting info through out the project.
Delegates
Set of methods will get triggered when delegate is been called, Source must be provided via methods itself
Swift 5.5:
TL;DR: Swift 5.5 is not yet released(at the time of writing). To use swift 5.5, download swift toolchain development snapshot from here and add compiler flag -Xfrontend -enable-experimental-concurrency. Read more here
This can be achieved easily with async/await feature.
To do so, you should mark your function as async then do the operation inside withUnsafeThrowingContinuation block like following.
class Bookshop {
class func getGenres() async throws -> NSArray {
print("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
return try await withUnsafeThrowingContinuation { continuation in
let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
print("Task completed")
if(error != nil) {
print(error!.localizedDescription)
continuation.resume(throwing: error!)
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
let results: NSArray = jsonResult!["genres"] as! NSArray
continuation.resume(returning: results)
} catch {
continuation.resume(throwing: error)
}
})
task.resume()
}
}
}
And you can call this function like
#asyncHandler
func check() {
do {
let genres = try await Bookshop.getGenres()
print("Result: \(genres)")
} catch {
print("Error: \(error)")
}
}
Keep in mind that, when calling Bookshop.getGenres method, the caller method should be either async or marked as #asyncHandler
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
self.endNetworkActivity()
var responseError: Error? = error
// handle http response status
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode > 299 , httpResponse.statusCode != 422 {
responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
}
}
var apiResponse: Response
if let _ = responseError {
apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
self.logError(apiResponse.error!, request: request)
// Handle if access token is invalid
if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Unautorized access
// User logout
return
}
}
else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Down time
// Server is currently down due to some maintenance
return
}
}
} else {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
self.logResponse(data!, forRequest: request)
}
self.removeRequestedURL(request.url!)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(apiResponse)
})
}).resume()
There are mainly 3 ways of achieving callback in swift
Closures/Completion handler
Delegates
Notifications
Observers can also be used to get notified once the async task has been completed.
There are some very generic requirements that would like every good API Manager to satisfy:
will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
This is a small use case that might be helpful:-
func testUrlSession(urlStr:String, completionHandler: #escaping ((String) -> Void)) {
let url = URL(string: urlStr)!
let task = URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else { return }
if let strContent = String(data: data, encoding: .utf8) {
completionHandler(strContent)
}
}
task.resume()
}
While calling the function:-
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}

Call completion Handler function using "Result"

How to use call getEmployee function while using "Result"?
struct Employee {
let name: String
let designation: String
}
func getEmployee(name: String, completion: #escaping(Result<Employee?, Error>) -> Void) {
}
First you need to make your structure conform to Codable
struct Employee: Codable {
let name, designation: String
}
Than you need to fetch your data from your server and call completion if decoding was successful with your employee .success(employee) or if it fails pass failure with your error .failure(error):
func getEmployee(name: String, completion: #escaping(Result<Employee, Error>) -> Void) {
URLSession.shared.dataTask(with: URL(string: "http://www.example.com/getEmployee.api?name=\(name)")!) { data, response, error in
guard let data = data, error == nil else {
if let error = error { completion(.failure(error)) }
return
}
do {
let employee = try JSONDecoder().decode(Employee.self, from: data)
completion(.success(employee))
} catch {
completion(.failure(error))
}
}
}
Usage:
getEmployee(name: "Ella") { result in
switch result {
case let .success(employee):
print("employee:", employee)
case let .failure(error):
print("error:", error)
default: break
}
}

Swift: How do I add JSON data to my struct? [duplicate]

I have created a utility class in my Swift project that handles all the REST requests and responses. I have built a simple REST API so I can test my code. I have created a class method that needs to return an NSArray but because the API call is async I need to return from the method inside the async call. The problem is the async returns void.
If I were doing this in Node I would use JS promises but I can't figure out a solution that works in Swift.
import Foundation
class Bookshop {
class func getGenres() -> NSArray {
println("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
println(urlPath)
let url: NSURL = NSURL(string: urlPath)
let session = NSURLSession.sharedSession()
var resultsArray:NSArray!
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if(error) {
println(error.localizedDescription)
}
var err: NSError?
var options:NSJSONReadingOptions = NSJSONReadingOptions.MutableContainers
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: options, error: &err) as NSDictionary
if(err != nil) {
println("JSON Error \(err!.localizedDescription)")
}
//NSLog("jsonResults %#", jsonResult)
let results: NSArray = jsonResult["genres"] as NSArray
NSLog("jsonResults %#", results)
resultsArray = results
return resultsArray // error [anyObject] is not a subType of 'Void'
})
task.resume()
//return "Hello World!"
// I want to return the NSArray...
}
}
You can pass callback, and call callback inside async call
something like:
class func getGenres(completionHandler: (genres: NSArray) -> ()) {
...
let task = session.dataTaskWithURL(url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
and then call this method:
override func viewDidLoad() {
Bookshop.getGenres {
genres in
println("View Controller: \(genres)")
}
}
Introduced in Swift 5.5 (iOS 15, macOS 12), we would now use the async-await pattern:
func fetchGenres() async throws -> [Genre] {
…
let (data, _) = try await URLSession.shared.dataTask(for: request)
return try JSONDecoder().decode([Genre].self, from: data)
}
And we would call it like:
let genres = try await fetchGenres()
The async-await syntax is far more concise and natural than the traditional completion handler pattern outlined in my original answer, below.
For more information, see Meet async/await in Swift.
The historic pattern is to use completion handlers closure.
For example, we would often use Result:
func fetchGenres(completion: #escaping (Result<[Genre], Error>) -> Void) {
...
URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
return
}
// parse response here
let results = ...
DispatchQueue.main.async {
completion(.success(results))
}
}.resume()
}
And you’d call it like so:
fetchGenres { results in
switch results {
case .failure(let error):
print(error.localizedDescription)
case .success(let genres):
// use `genres` here, e.g. update model and UI
}
}
// but don’t try to use `genres` here, as the above runs asynchronously
Note, above I’m dispatching the completion handler back to the main queue to simplify model and UI updates. Some developers take exception to this practice and either use whatever queue URLSession used or use their own queue (requiring the caller to manually synchronize the results themselves).
But that’s not material here. The key issue is the use of completion handler to specify the block of code to be run when the asynchronous request is done.
Note, above I retired the use of NSArray (we don’t use those bridged Objective-C types any more). I assume that we had a Genre type and we presumably used JSONDecoder, rather than JSONSerialization, to decode it. But this question didn’t have enough information about the underlying JSON to get into the details here, so I omitted that to avoid clouding the core issue, the use of closures as completion handlers.
Swiftz already offers Future, which is the basic building block of a Promise. A Future is a Promise that cannot fail (all terms here are based on the Scala interpretation, where a Promise is a Monad).
https://github.com/maxpow4h/swiftz/blob/master/swiftz/Future.swift
Hopefully will expand to a full Scala-style Promise eventually (I may write it myself at some point; I'm sure other PRs would be welcome; it's not that difficult with Future already in place).
In your particular case, I would probably create a Result<[Book]> (based on Alexandros Salazar's version of Result). Then your method signature would be:
class func fetchGenres() -> Future<Result<[Book]>> {
Notes
I do not recommend prefixing functions with get in Swift. It will break certain kinds of interoperability with ObjC.
I recommend parsing all the way down to a Book object before returning your results as a Future. There are several ways this system can fail, and it's much more convenient if you check for all of those things before wrapping them up into a Future. Getting to [Book] is much better for the rest of your Swift code than handing around an NSArray.
Swift 4.0
For async Request-Response you can use completion handler. See below I have modified the solution with completion handle paradigm.
func getGenres(_ completion: #escaping (NSArray) -> ()) {
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
guard let url = URL(string: urlPath) else { return }
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let jsonResult = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers) as? NSDictionary {
let results = jsonResult["genres"] as! NSArray
print(results)
completion(results)
}
} catch {
//Catch Error here...
}
}
task.resume()
}
You can call this function as below:
getGenres { (array) in
// Do operation with array
}
Swift 3 version of #Alexey Globchastyy's answer:
class func getGenres(completionHandler: #escaping (genres: NSArray) -> ()) {
...
let task = session.dataTask(with:url) {
data, response, error in
...
resultsArray = results
completionHandler(genres: resultsArray)
}
...
task.resume()
}
Swift 5.5, async/wait-based solution
The original test URL provided by the original poster is no longer functional, so I had to change things a bit. This solution is based on a jokes API I found. That API returns a single joke, but I return it as an array of String ([String]), to keep it as consistent as possible with the original post.
class Bookshop {
class func getGenres() async -> [String] {
print("Hello inside getGenres")
let urlPath = "https://geek-jokes.sameerkumar.website/api?format=json"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
typealias Continuation = CheckedContinuation<[String], Never>
let genres = await withCheckedContinuation { (continuation: Continuation) in
let task = session.dataTask(with: url) { data, response, error in
print("Task completed")
var result: [String] = []
defer {
continuation.resume(returning: result)
}
if let error = error {
print(error.localizedDescription)
return
}
guard let data = data else {
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data, options: [.mutableContainers])
print("jsonResult is \(jsonResult)")
if let joke = (jsonResult as? [String: String])?["joke"] {
result = [joke]
}
} catch {
print("JSON Error \(error.localizedDescription)")
print("data was \(String(describing: String(data: data, encoding: .utf8)))")
return
}
}
task.resume()
}
return genres
}
}
async {
let final = await Bookshop.getGenres()
print("Final is \(final)")
}
The withCheckedContinuation is how you made the Swift async function actually run in a separate task/thread.
I hope you're not still stuck on this, but the short answer is that you can't do this in Swift.
An alternative approach would be to return a callback that will provide the data you need as soon as it is ready.
There are 3 ways of creating call back functions namely:
1. Completion handler
2. Notification
3. Delegates
Completion Handler
Inside set of block is executed and returned when source is available, Handler will wait until response comes so that UI can be updated after.
Notification
Bunch of information is triggered over all the app, Listner can retrieve n make use of that info. Async way of getting info through out the project.
Delegates
Set of methods will get triggered when delegate is been called, Source must be provided via methods itself
Swift 5.5:
TL;DR: Swift 5.5 is not yet released(at the time of writing). To use swift 5.5, download swift toolchain development snapshot from here and add compiler flag -Xfrontend -enable-experimental-concurrency. Read more here
This can be achieved easily with async/await feature.
To do so, you should mark your function as async then do the operation inside withUnsafeThrowingContinuation block like following.
class Bookshop {
class func getGenres() async throws -> NSArray {
print("Hello inside getGenres")
let urlPath = "http://creative.coventry.ac.uk/~bookshop/v1.1/index.php/genre/list"
print(urlPath)
let url = URL(string: urlPath)!
let session = URLSession.shared
return try await withUnsafeThrowingContinuation { continuation in
let task = session.dataTask(with: url, completionHandler: {data, response, error -> Void in
print("Task completed")
if(error != nil) {
print(error!.localizedDescription)
continuation.resume(throwing: error!)
return
}
do {
let jsonResult = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? [String: Any]
let results: NSArray = jsonResult!["genres"] as! NSArray
continuation.resume(returning: results)
} catch {
continuation.resume(throwing: error)
}
})
task.resume()
}
}
}
And you can call this function like
#asyncHandler
func check() {
do {
let genres = try await Bookshop.getGenres()
print("Result: \(genres)")
} catch {
print("Error: \(error)")
}
}
Keep in mind that, when calling Bookshop.getGenres method, the caller method should be either async or marked as #asyncHandler
self.urlSession.dataTask(with: request, completionHandler: { (data, response, error) in
self.endNetworkActivity()
var responseError: Error? = error
// handle http response status
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode > 299 , httpResponse.statusCode != 422 {
responseError = NSError.errorForHTTPStatus(httpResponse.statusCode)
}
}
var apiResponse: Response
if let _ = responseError {
apiResponse = Response(request, response as? HTTPURLResponse, responseError!)
self.logError(apiResponse.error!, request: request)
// Handle if access token is invalid
if let nsError: NSError = responseError as NSError? , nsError.code == 401 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Unautorized access
// User logout
return
}
}
else if let nsError: NSError = responseError as NSError? , nsError.code == 503 {
DispatchQueue.main.async {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
let message = apiResponse.message()
// Down time
// Server is currently down due to some maintenance
return
}
}
} else {
apiResponse = Response(request, response as? HTTPURLResponse, data!)
self.logResponse(data!, forRequest: request)
}
self.removeRequestedURL(request.url!)
DispatchQueue.main.async(execute: { () -> Void in
completionHandler(apiResponse)
})
}).resume()
There are mainly 3 ways of achieving callback in swift
Closures/Completion handler
Delegates
Notifications
Observers can also be used to get notified once the async task has been completed.
There are some very generic requirements that would like every good API Manager to satisfy:
will implement a protocol-oriented API Client.
APIClient Initial Interface
protocol APIClient {
func send(_ request: APIRequest,
completion: #escaping (APIResponse?, Error?) -> Void)
}
protocol APIRequest: Encodable {
var resourceName: String { get }
}
protocol APIResponse: Decodable {
}
Now Please check complete api structure
// ******* This is API Call Class *****
public typealias ResultCallback<Value> = (Result<Value, Error>) -> Void
/// Implementation of a generic-based API client
public class APIClient {
private let baseEndpointUrl = URL(string: "irl")!
private let session = URLSession(configuration: .default)
public init() {
}
/// Sends a request to servers, calling the completion method when finished
public func send<T: APIRequest>(_ request: T, completion: #escaping ResultCallback<DataContainer<T.Response>>) {
let endpoint = self.endpoint(for: request)
let task = session.dataTask(with: URLRequest(url: endpoint)) { data, response, error in
if let data = data {
do {
// Decode the top level response, and look up the decoded response to see
// if it's a success or a failure
let apiResponse = try JSONDecoder().decode(APIResponse<T.Response>.self, from: data)
if let dataContainer = apiResponse.data {
completion(.success(dataContainer))
} else if let message = apiResponse.message {
completion(.failure(APIError.server(message: message)))
} else {
completion(.failure(APIError.decoding))
}
} catch {
completion(.failure(error))
}
} else if let error = error {
completion(.failure(error))
}
}
task.resume()
}
/// Encodes a URL based on the given request
/// Everything needed for a public request to api servers is encoded directly in this URL
private func endpoint<T: APIRequest>(for request: T) -> URL {
guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else {
fatalError("Bad resourceName: \(request.resourceName)")
}
var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)!
// Common query items needed for all api requests
let timestamp = "\(Date().timeIntervalSince1970)"
let hash = "\(timestamp)"
let commonQueryItems = [
URLQueryItem(name: "ts", value: timestamp),
URLQueryItem(name: "hash", value: hash),
URLQueryItem(name: "apikey", value: "")
]
// Custom query items needed for this specific request
let customQueryItems: [URLQueryItem]
do {
customQueryItems = try URLQueryItemEncoder.encode(request)
} catch {
fatalError("Wrong parameters: \(error)")
}
components.queryItems = commonQueryItems + customQueryItems
// Construct the final URL with all the previous data
return components.url!
}
}
// ****** API Request Encodable Protocol *****
public protocol APIRequest: Encodable {
/// Response (will be wrapped with a DataContainer)
associatedtype Response: Decodable
/// Endpoint for this request (the last part of the URL)
var resourceName: String { get }
}
// ****** This Results type Data Container Struct ******
public struct DataContainer<Results: Decodable>: Decodable {
public let offset: Int
public let limit: Int
public let total: Int
public let count: Int
public let results: Results
}
// ***** API Errro Enum ****
public enum APIError: Error {
case encoding
case decoding
case server(message: String)
}
// ****** API Response Struct ******
public struct APIResponse<Response: Decodable>: Decodable {
/// Whether it was ok or not
public let status: String?
/// Message that usually gives more information about some error
public let message: String?
/// Requested data
public let data: DataContainer<Response>?
}
// ***** URL Query Encoder OR JSON Encoder *****
enum URLQueryItemEncoder {
static func encode<T: Encodable>(_ encodable: T) throws -> [URLQueryItem] {
let parametersData = try JSONEncoder().encode(encodable)
let parameters = try JSONDecoder().decode([String: HTTPParam].self, from: parametersData)
return parameters.map { URLQueryItem(name: $0, value: $1.description) }
}
}
// ****** HTTP Pamater Conversion Enum *****
enum HTTPParam: CustomStringConvertible, Decodable {
case string(String)
case bool(Bool)
case int(Int)
case double(Double)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else {
throw APIError.decoding
}
}
var description: String {
switch self {
case .string(let string):
return string
case .bool(let bool):
return String(describing: bool)
case .int(let int):
return String(describing: int)
case .double(let double):
return String(describing: double)
}
}
}
/// **** This is your API Request Endpoint Method in Struct *****
public struct GetCharacters: APIRequest {
public typealias Response = [MyCharacter]
public var resourceName: String {
return "characters"
}
// Parameters
public let name: String?
public let nameStartsWith: String?
public let limit: Int?
public let offset: Int?
// Note that nil parameters will not be used
public init(name: String? = nil,
nameStartsWith: String? = nil,
limit: Int? = nil,
offset: Int? = nil) {
self.name = name
self.nameStartsWith = nameStartsWith
self.limit = limit
self.offset = offset
}
}
// *** This is Model for Above Api endpoint method ****
public struct MyCharacter: Decodable {
public let id: Int
public let name: String?
public let description: String?
}
// ***** These below line you used to call any api call in your controller or view model ****
func viewDidLoad() {
let apiClient = APIClient()
// A simple request with no parameters
apiClient.send(GetCharacters()) { response in
response.map { dataContainer in
print(dataContainer.results)
}
}
}
This is a small use case that might be helpful:-
func testUrlSession(urlStr:String, completionHandler: #escaping ((String) -> Void)) {
let url = URL(string: urlStr)!
let task = URLSession.shared.dataTask(with: url){(data, response, error) in
guard let data = data else { return }
if let strContent = String(data: data, encoding: .utf8) {
completionHandler(strContent)
}
}
task.resume()
}
While calling the function:-
testUrlSession(urlStr: "YOUR-URL") { (value) in
print("Your string value ::- \(value)")
}

How to mock DataTaskPublisher?

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).

Using decoded data from an API into an algorithm

I successfully fetched and decoded data from an API and now have access to all the data I need to be used in the algorithm I want to write in my App.
The issue is that I don't know how to access this data after I decoded it, I can print it immediately after it's decoded but I have no idea how to use it in another function or place in my app.
Here is my Playground:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError : Error {
case FoundNil(String)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else { return }
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
print(marketData.data[0].open)
print(marketData.data[1].open)
print("Average=", (marketData.data[0].open + marketData.data[1].open) / 2)
//completion(marketData, nil)
throw MyError.FoundNil("data")
}
} catch {
print(error)
}
}
task.resume()
}
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
How can I use .data[0], .data[1], ..., somewhere else?
You data will be available in your fecthData() call. Probably what you want is your items variable, where you're printing it. But make sure to call the completion in your fetchData implementation.
WARNING: Untested code.
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
enum MyError: Error {
case FoundNil(String)
case DecodingData(Data)
}
struct Level: Codable {
let time: Double
let close: Double
let high: Double
let low: Double
let open: Double
}
struct Response: Codable {
let data: [Level]
private enum CodingKeys : String, CodingKey {
case data = "Data"
}
}
func fetchData(completion: #escaping (Response?, Error?) -> Void) {
let url = URL(string: "https://min-api.cryptocompare.com/data/histominute?fsym=BTC&tsym=USD&limit=60&aggregate=3&e=CCCAGG")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
completion(nil, MyError.FoundNil("data"))
}
do {
if let marketData = try? JSONDecoder().decode(Response.self, from: data) {
completion(marketData, nil)
} else {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
} catch {
completion(nil, MyError.DecodingData(data)) // work on this duplicated call
}
}
task.resume()
}
fetchData() { items, error in
if let error == error {
switch(error) {
case .foundNil(let whatsNil):
print("Something is nil: \(whatsNil)")
case .decodingData(let data):
print("Error decoding: \(data)")
}
} else {
if let items = items {
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
print(items)
} else {
print("No items to show!")
}
}
}
I don't understand what is your real issue, because you have written everything you need here, but as far I understand , to pass data
just uncomment this line completion(marketData, nil)
and in
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
print(items)
}
items is an object of your struct Response. You can pass this anywhere in your other class , by just creating an another variable like:
var items : Response!
for example :
class SomeOtherClass : NSObject{
var items : Response!
func printSomeData()
{
print(items.data[0].open)
print(items.data[1].open)
print("Average=", (items.data[0].open + items.data[1].open) / 2)
}
}
and in fetchData method write this:
fetchData() { items, error in
guard let items = items,
error == nil else {
print(error ?? "Unknown error")
return
}
let otherObject = SomeOtherClass()
otherObject.items = items
otherObject.printSomeData()
}