The code in my viewcontroller-class is executed before the JSON-download-process is ready even though there is a completion handler in the func for downloading JSON an a DispatchGroup(). I store the JSON-data in an array called "fetchedModules" and this is filled with 11 items in this case. Why does this happen?
result in console:
---> in Class PostCell - func numberOfSections: 0
JSON call finished
// ViewController
override func viewDidLoad() {
super.viewDidLoad()
let group = DispatchGroup()
group.enter()
self.fetchJSON()
// here calling downloadJSONasync
group.leave()
group.notify(queue: .main) {
print("JSON call finished")
}
...
// networkService with completion
func downloadJSONasync(searchItem: String, completion: #escaping ([NSDictionary]) -> Void) {
//request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
request.httpMethod = "GET"
let configuration = URLSessionConfiguration.default
//let session = URLSession(configuration: configuration, delegate: nil)
let session = URLSession(configuration: configuration)
let task = session.dataTask(with: request, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
if (error != nil) {
print("error!")
}
else{
do {
let fetchedData = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as! [NSDictionary]
completion(fetchedData)
}
catch {
print("error")
}
}
})
task.resume()
}
// call in viewController
override func numberOfSections(in tableView: UITableView) -> Int {
print("---> in Class PostCell - func numberOfSections: \(String(describing: fetchedModules.count))")
return fetchedModules.count
// code of fetchJSON
func fetchJSON()
{
let baseurl = AppConstants.Domains.baseurl // https://m.myapp2go.de
let compositURL = baseurl + "getmodules_noItems.php?id=\(AppConstants.appString.startString)"
let encodedUrl : String! = compositURL.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) // remove the spaces in the url string for safty reason
let JSONurl = URL(string: encodedUrl)! // convert the string into url
var JSONrequest = URLRequest(url: JSONurl) // make request
JSONrequest.httpMethod = "GET"
//JSONrequest.cachePolicy = .reloadIgnoringCacheData
let networkService = NetworkService(request: JSONrequest)
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString, completion: { (fetchedData) in
fetchedModules.removeAll()
DispatchQueue.main.async {
for eachFetchedModul in fetchedData {
let eachModul = eachFetchedModul
if
let custid = eachModul["custid"] as? String,
let modulcat = eachModul["modulcat"] as? String,
let modulname = eachModul["modulname"] as? String,
let comment = eachModul["comment"] as? String
{
fetchedModules.append(CModules(custid: custid, modulcat: modulcat, modulname: modulname, comment: comment))
print(custid)
print(modulcat)
print(modulname)
print(comment)
print("---------------------")
}
}// for end
// ... somehow set data source array for your table view
self.tableView.reloadData()
}// dispatch
}
)} // func end
Because fetchJSON returns immediately, before the JSON is downloaded. The effect is that the DispatchGroup is entereed and left right away, without waiting for the JSON:
group.enter()
self.fetchJSON() // returns immediately
group.leave() // the JSON has yet to be downloaded
To wait until the JSON has arrived, add a completion handler to fetchJSON:
override func viewDidLoad() {
group.enter()
self.fetchJSON {
group.notify(queue: .main) {
print("JSON call finished")
}
group.leave()
}
}
// Change the data type of the completion handler accordingly
func fetchJSON(completionHandler: #escaping (Data?) -> Void) {
// ...
networkService.downloadJSONasync(searchItem: AppConstants.appString.startString) { fetchedData in
defer { completionHandler(fetchedData) }
// ...
}
)
Using defer ensures that the completion handler will always be called, no matter how the outer closure returns. I'm not clear why you use a DispatchGroup here as there is no waiting, but I kept it in place to answer your question.
Your table view doesn't have any data from the beginning because any data hasn't been fetched yet. So it's ok, that table view has no cells. You just need to reloadData of your table view since now you appended elements to table view's data source array and now you should show this data.
Please, don't use DispatchGroup for this, just use your completion parameter of your method and inside of the completion closure after data are received set data source array for table view and then ... reload data of table view
downloadJSONasync(searchItem: "someString") { dictionary in
DispatchQueue.main.async { // don't forget that your code doesn't run on the main thread
// ... somehow set data source array for your table view
self.tableView.reloadData()
}
}
Note that you should avoid using NSDictonary and you should rather use Dictionary. Also from Swift 4+ you can use Codable instead of JSONSerialization.
Related
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)")
}
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)")
}
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 1 year ago.
I'm trying to return json data from an api call. I'm able to access the json data successfully but am struggling to find a way / the best way to return it for access in my app. Thanks for any ideas!
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// make the api call and obtain data
let data = self.loadData()
print("inside viewDidLoad", data) // prints 'inside viewDidLoad emptyString'
}
func loadData() -> String {
var circData = "emptyString"
let session = URLSession.shared
let url = URL(string: "https://us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/cirdata-khyvx/service/cirData/incoming_webhook/cirlAPI")!
let task = session.dataTask(with: url, completionHandler: { data, response, error in
if let json = try? JSONSerialization.jsonObject(with: data!, options: []) {
// print("json: ", json) // prints the whole json file, verifying the connection works. Some 300kb of data.
// print("json file type: ", type(of: json)) // prints '__NSArrayI'
let jsonString = "\(json)"
circData = jsonString
// print("circData", circData) // prints the whole json file, verifying that the json string has been assigned to 'circData'
}
})
task.resume()
// print("after: ", circData) // prints 'after: emptyString'. It's as if the reassignment didn't take place.
return circData
}
}
You can't return a value synchronously becuase the api call that is fetching json data is asynchronous. You need to use a completion handler instead.
You can put breakpoints in different places inside the code to understand how the flow executes.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.loadData(completion: { [weak self] (result, error) in
if let error = error {
print(error.localizedDescription)
}
if let result = result {
print(result)
}
})
}
func loadData(completion: #escaping (_ data: Any?, _ error: Error?) -> Void) {
let url = URL(string: "https://us-east-1.aws.webhooks.mongodb-realm.com/api/client/v2.0/app/cirdata-khyvx/service/cirData/incoming_webhook/cirlAPI")!
let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let error = error {
completion(nil, error)
return
}
do {
if let data = data {
let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments])
completion(json, nil)
} else {
completion(nil, nil)
}
} catch {
completion(nil, error)
}
})
task.resume()
}
}
so i wanna parse json api, but i the way i get that param to parse i need to fetch another json (which is working), and since i cant put that data param for my 2nd json api into global var so i can just put it into another func, i have this idea that i parse my 2nd json api inside the 1st urlSession, but i always get a nil callback,
override func viewDidLoad() {
super.viewDidLoad()
getRoom()
}
func getRoom() {
guard let url = URL(Some url) else {return}
print(url)
URLSession.shared.dataTask(with: url) { data, resp, err in
guard let data = data else {return}
do{
let decoder = JSONDecoder()
let room = try decoder.decode(User.self, from: data)
self.dataClient = [room].compactMap{$0!.data}
self.DATA = [room]
print("ini dataClient 🪕\(self.dataClient)")
let roomid = self.dataClient[0].RoomID
self.roomId = roomid
print(self.roomId)
DispatchQueue.main.async {
checkRoom()
}
}catch{
print(err!)
}
}.resume()
}
func checkRoom() {
if self.roomId == 0 {
print("roomId nil")
}else if self.roomId != 0{
print("ini room id \(self.roomId)")
guard let urlRoom = URL(some url) else {return
URLSession.shared.dataTask(with: urlRoom) { (data, resp, err) in
guard let data = data else {return}
do{
let decoder = JSONDecoder()
let roomAv = try decoder.decode(User.self, from: data)
self.DATA = [roomAv]
print("ini boolnya 🎸 \(self.DATA[0].success)")
print("Success")
}catch{
print("damn😭") // this line always get called
}
}.resume()
}
}
can anyone tell me any ideas? the reason i put the 2nd urlsession inside 1st urlsession because i need that (self.roomId) for my param in my 2nd Json api.
and when i try to separate both urlsession func in my checkRoom() alwasy called "roomId Nil"
I wouldn't make a call within a call personally. That's asking for trouble. Just call the first endpoint, get the data from it and pass in whatever you needed from that into the second call in your logic controller.
Quasi code:
import Foundation
class Test {
func getRoom() {
getFirstCall { [weak self] (foo) in
self?.getSecondCall(someArg: foo) {
// Handle data here.
}
}
}
func getFirstCall(completion: #escaping (_ somethingToReturn: String) -> ()) {
guard let url = URL(string: "Some URL") else { return }
URLSession.shared.dataTask(with: url) { data, response, error in
// Logic to ingest data.
completion("foo")
}.resume()
}
func getSecondCall(someArg: String, completion: #escaping () -> ()) {
guard let url = URL(string: "Some URL 2") else { return }
// Use "someArg" however you need in this call. queryParam, body, etc.
URLSession.shared.dataTask(with: url) { data, response, error in
// Logic to ingest data.
completion()
}.resume()
}
}
I am looking to determine when a data download in another class has finished, and then return the downloaded data to the calling class. I have tried with completion handlers but this only tells me the data is downloaded and they reside in the DataManager class, how do I return to the calling class?
In FirstViewController.swift:
func download(button: UIButton) {
DataManager.getJSONData(url, completionHandler:DataManager.resultHandler)
}
In DataManager.swift:
class func getJSONData(urlInput:String, completionHandler: ((NSDictionary!) -> Void)?)
{
var url : NSURL! = NSURL(string:urlInput)
var request: NSURLRequest = NSURLRequest(URL:url)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task : NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {(data, response, error) in
var error: AutoreleasingUnsafeMutablePointer<NSError?> = nil
let jsonResult: NSDictionary! = NSJSONSerialization.JSONObjectWithData(data, options: nil, error: error) as? NSDictionary!
// then on complete I call the completionHandler...
completionHandler?(jsonResult);
});
task.resume()
}
class func resultHandler(jsonResult:NSDictionary!)
{
NSLog("Data: %# \n", jsonResult)
}
How do I return jsonResult to the FirstViewController.swift, or alternatively, inform FirstViewController.swift that the download is complete?
It doesn't look like the resultHandler is doing much, so rather than:
func download(button: UIButton) {
DataManager.getJSONData(url, completionHandler:DataManager.resultHandler)
}
You could do:
func download(button: UIButton) {
DataManager.getJSONData(url) { jsonResult in
// use jsonResult here
}
}
That way the view controller will be notified when the retrieval is done.