I'd like to return some values after the long term operation is completed.
But furthermore I'd like to split the logic and the gui.
For example; I have two classes
SomeServices.swift which has a method named "getDataFromService..."
MyTableViewController.swift which will display the result from "getDataFromService"
So, previously in Objective-C I've just add a method in SomeServices like this:
(void)getDataFromService:(void (^)(NSArray *, NSError *))completionBlock{ ...... }
In this method I've just called completionBlock(myData, myError) to return my values to the tableviewcontroller.
What would be the equivalent closure which I have to define in SomeServices.swift and how will it be called in MyTableViewController?
I know how to call a simple closures like this one:
....({
responseData, error in
if(!error){
//Do something
}
})
But I don't have any ideas how to define a closure with a completionBlock equivalent.
Any help would be appreciated
The plus of closures are, that you can pass everything you want. Methods or functions - it doesn't matter.
You can pass a function within the parameters and just call it.
func someFunctionThatTakesAClosure(completionClosure: () -> ()) {
// function body goes here
if(error = false) {
completionClosure()
}
}
//Call it
someFunctionThatTakesAClosure({
//Completions Stuff
println("someFunctionThatTakesAClosure")
});
Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/ch/jEUH0.l
The answer is in the language guide:
Assume you want to return a String. This is the syntax
({(responseData: DataClass, error: ErrorClass) -> String in
//do stuff - calculations etc..
return calculatedString
})
Here is an example that takes two strings and concatenates them, and returns the result:
let sumStrings = ({(first: String, second: String) -> String in
return first + " " + second
})
then you can do the following:
sumStrings("Hello","Swift") // "Hello Swift"
Here is how I use a singleton ServiceManager to achieve this.
class ServiceManager: NSObject {
// Static Instance variable for Singleton
static var sharedSessionManager = ServiceManager()
// Function to execute GET request and pass data from escaping closure
func executeGetRequest(with urlString: String, completion: #escaping (Data?) -> ()) {
let url = URL.init(string: urlString)
let urlRequest = URLRequest(url: url!)
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
// Log errors (if any)
if error != nil {
print(error.debugDescription)
} else {
// Passing the data from closure to the calling method
completion(data)
}
}.resume() // Starting the dataTask
}
// Function to perform a task - Calls executeGetRequest(with urlString:) and receives data from the closure.
func downloadMovies(from urlString: String, completion: #escaping ([Movie]) -> ()) {
// Calling executeGetRequest(with:)
executeGetRequest(with: urlString) { (data) in // Data received from closure
do {
// JSON parsing
let responseDict = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
if let results = responseDict!["results"] as? [[String:Any]] {
var movies = [Movie]()
for obj in results {
let movie = Movie(movieDict: obj)
movies.append(movie)
}
// Passing parsed JSON data from closure to the calling method.
completion(movies)
}
} catch {
print("ERROR: could not retrieve response")
}
}
}
}
Below is the example how I use the singleton class.
ServiceManager.sharedSessionManager.downloadMovies(from: urlBase) { (movies : [Movie]) in // Object received from closure
self.movies = movies
DispatchQueue.main.async {
// Updating UI on main queue
self.movieCollectionView.reloadData()
}
}
I hope this helps anybody looking for the same solution.
Related
I have a service class that makes an api call and stores data into its property. Then my interactor class have a method where I want to make service class api call and when data will be stored - return it. I tried myself to handle this with completion handler and dispatch group, but (I suppose I just missing something) this didn't work. I would be very appreciated if you help me to deal with this problem. Thanks in advance!
Service class:
class PunkApiService{
var beers = [Beer]()
func loadList(at page: Int){
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
guard error == nil else {
print(error!)
return
}
//MARK: - Handling data came
guard let data = data else{
print("Failed to load data")
return
}
do{
let beers = try JSONDecoder().decode([Beer].self, from: data)
self.beers.append(contentsOf: beers)
}
catch{
print("Failed to decode data")
}
}
task.resume()
}
And Interactor class(without completion handler or dispatch group):
class BeersListInteractor:BeersListInteractorProtocol{
private var favoriteBeers = FavoriteBeers()
private var service = PunkApiService()
//MARK: - Load list of Beers
func loadList(at page: Int) -> [Beer]{
service.loadList(at: page)
return service.beers
}
Added: my attempt with completion handler
var beers: [Beer]
func loadList(at page: Int, completion: ()->()){
service.loadList(at: page)
completion()
}
func completion(){
beers.append(contentsOf: service.beers)
}
loadList(at: 1) {
completion()
}
This is what async/await pattern is for, described here. In your case both loadList functions are async, and the second one awaits for the first one:
class PunkApiService {
func loadList(at page: Int) async {
// change function to await for task result
let (data, error) = try await URLSession.shared.data(from: url)
let beers = try JSONDecoder().decode([Beer].self, from: data)
...
return beers
}
}
class BeersListInteractor: BeersListInteractorProtocol {
func loadList(at page: Int) async -> [Beer]{
let beers = await service.loadList(at: page)
return service.beers
}
}
See a good explanation here
I think that you were on the right path when attempting to use a completion block, just didn't do it correctly.
func loadList(at page: Int, completion: #escaping ((Error?, Bool, [Beer]?) -> Void)) {
//MARK: - Checks is URL is valid + pagination
guard let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=25") else {
print("Invalid URL")
completion(nil, false, nil)
return
}
//MARK: - Creating URLSession DataTask
let task = URLSession.shared.dataTask(with: url){ data, response, error in
//MARK: - Handling no erros came
if let error = error {
completion(error, false, nil)
print(error!)
return
}
//MARK: - Handling data came
guard let data = data, let beers = try? JSONDecoder().decode([Beer].self, from: data) else {
completion(nil, false, nil)
return
}
completion(nil, true, beers)
}
task.resume()
}
This is the loadList function, which now has a completion parameter that will have three parameters, respectively the optional Error, the Bool value representing success or failure of obtaining the data, and the actual [Beers] array, containing the data (if any was retrieved).
Here's how you would now call the function:
service.loadList(at: page) { error, success, beers in
if let error = error {
// Handle the error here
return
}
if success, let beers = beers {
// Data was correctly retrieved - and safely unwrapped for good measure, do what you need with it
// Example:
loader.stopLoading()
self.datasource = beers
self.tableView.reloadData()
}
}
Bear in mind the fact that the completion is being executed asynchronously, without stopping the execution of the rest of your app.
Also, you should decide wether you want to handle the error directly inside the loadList function or inside the closure, and possibly remove the Error parameter if you handle it inside the function.
The same goes for the other parameters: you can decide to only have a closure that only has a [Beer] parameter and only call the closure if the data is correctly retrieved and converted.
I am trying to recover a data set from a URL (after parsing a JSON through the parseJSON function which works correctly - I'm not attaching it in the snippet below).
The outcome returns nil - I believe it's because the closure in retrieveData function is processed asynchronously. I can't manage to have the outcome saved into targetData.
Thanks in advance for your help.
class MyClass {
var targetData:Download?
func triggerEvaluation() {
retrieveData(url: "myurl.com") { downloadedData in
self.targetData = downloadedData
}
print(targetData) // <---- Here is where I get "nil"!
}
func retrieveData(url: String, completion: #escaping (Download) -> ()) {
let myURL = URL(url)!
let mySession = URLSession(configuration: .default)
let task = mySession.dataTask(with: myURL) { [self] (data, response, error) in
if error == nil {
if let fetchedData = data {
let safeData = parseJSON(data: fetchedData)
completion(safeData)
}
} else {
//
}
}
task.resume()
}
}
Yes, it’s nil because retrieveData runs asynchronously, i.e. the data hasn’t been retrieved by the time you hit the print statement. Move the print statement (and, presumably, all of the updating of your UI) inside the closure, right where you set self.targetData).
E.g.
func retrieveData(from urlString: String, completion: #escaping (Result<Download, Error>) -> Void) {
let url = URL(urlString)!
let mySession = URLSession.shared
let task = mySession.dataTask(with: url) { [self] data, response, error in
guard
let responseData = data,
error == nil,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async {
completion(.failure(error ?? NetworkError.unknown(response, data))
}
return
}
let safeData = parseJSON(data: responseData)
DispatchQueue.main.async {
completion(.success(safeData))
}
}
task.resume()
}
Where
enum NetworkError: Error {
case unknown(URLResponse?, Data?)
}
Then the caller would:
func triggerEvaluation() {
retrieveData(from: "https://myurl.com") { result in
switch result {
case .failure(let error):
print(error)
// handle error here
case .success(let download):
self.targetData = download
// update the UI here
print(download)
}
}
// but not here
}
A few unrelated observations:
You don't want to create a new URLSession for every request. Create only one and use it for all requests, or just use shared like I did above.
Make sure every path of execution in retrieveData calls the closure. It might not be critical yet, but when we write asynchronous code, we always want to make sure that we call the closure.
To detect errors, I'd suggest the Result pattern, shown above, where it is .success or .failure, but either way you know the closure will be called.
Make sure that model updates and UI updates happen on the main queue. Often, we would have retrieveData dispatch the calling of the closure to the main queue, that way the caller is not encumbered with that. (E.g. this is what libraries like Alamofire do.)
I wrote a method using generics in order to make it reusable to any kind of download. It downloads json arrays and returns a generic object or an error.
This is my class to download json arrays from server:
import Foundation
enum Result<T> {
case success(T)
case failure(Error)
}
class LTLNetworkClient: NSObject {
fileprivate var session : URLSession
fileprivate var objectTask : URLSessionDataTask?
override init() {
let config = URLSessionConfiguration.ephemeral
config.timeoutIntervalForResource = 5
config.timeoutIntervalForRequest = 5
self.session = URLSession.init(configuration: config)
}
/**
Download asynchronously json object from Server and returns it into generic data models
*/
func getData<K: Codable>(request: URLRequest, completion: #escaping (Result<[K]>) -> Void) {
let sessionDataTask = URLSession.shared.dataTask(with: request) { (responseData, response, responseError) in
if let jsonData = responseData {
let decoder = JSONDecoder()
do {
let response = try decoder.decode([K].self, from: jsonData)
let result: Result<[K]> = Result.success(response)
completion(result)
} catch {
completion(.failure(error))
}
} else if let error = responseError {
completion(.failure(error))
} else {
let error = NSError(domain: "Cannot form jsonData with Response Data", code: 501, userInfo: nil)
completion(.failure(error))
}
}
sessionDataTask.resume()
}
}
And this is how I call the method and the error I get:
Does anyone knows how to fix it and why this error appears?
Many thx in advance
Here's your method declaration:
func getData<K: Codable>(request: URLRequest, completion: #escaping (Result<[K]>) -> Void)
This method has a type parameter, K, which is only used in the type of the completion argument. When the compiler sees a call to getData, it has to deduce a concrete type for K based on the argument type of the closure you pass as completion.
And here's your call:
networkClient.getData(request: urlRequest) { (result) in
// Do stuff
}
In this call, the completion closure is { (result) in }. What is the type of result? There is no information given about it, so all the compiler knows is that result must be type Result<[K]> for some type K. It has no way to deduce a concrete type for K, so it emits an error.
You can make the type explicit like this:
networkClient.getData(request: urlRequest) { (_ result: Result<[SomeConcreteCodableType]>) -> Void in
// Do stuff
}
where SomeConcreteCodableType is some concrete type that conforms to Codable. The Codable conformance is required because of the constraint in getData's declaration.
A different solution is to change getData to take another argument that lets the caller specify K directly:
func getArray<K: Codable>(of: K.Type, for request: URLRequest,
completion: #escaping (Result<[K]>) -> Void) {
...
}
Then you call it like this:
networkClient.getArray(of: SomeConcreteCodableType.self, for: request) { result in
// Do stuff
}
I have an issue with my code and I think it could be related to the order in which code is called.
import WatchKit
import Foundation
class InterfaceController: WKInterfaceController {
private var tasks = [Task]()
override func willActivate() {
let taskUrl = "http://myjsonurl.com"
downloadJsonTask(url: taskUrl)
print(tasks.count) // EMPTY
super.willActivate()
}
func downloadJsonTask(url: String) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
return
}
do
{
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("somehting went wrong after downloading")
}
}.resume()
}
}
I'm defining the private var tasks and fill it with the downloadJsonTask function but after the function ran the print(tasks.count) gives 0.
When I call print(downloadedTasks.tasks.count) it gives 4.
I think that in sequence of time the tasks variable is empty when I print it and it is filled later on.
When you are trying to print number of tasks in willActivate(), function downloadJsonTask(url: String) hasn't been completed yet, so you have empty array because tasks haven't been set yet.
You should add completion handler to downloadJsonTask just like this:
(don't forget to pass completion as parameter of function)
func downloadJsonTask(url: String, completion: #escaping () -> Void) {
var request = URLRequest(url: URL(string: url)!)
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalCacheData
URLSession.shared.dataTask(with: request) { data, urlResponse, error in
guard let data = data, error == nil, urlResponse != nil else {
print("something is wrong")
completion()
return
}
do {
let decoder = JSONDecoder()
let downloadedTasks = try decoder.decode(Tasks.self, from: data)
self.tasks = downloadedTasks.tasks
print(downloadedTasks.tasks.count) //4
} catch {
print("something went wrong after downloading")
}
completion() // This is moment when code which you write inside closure get executed
}.resume()
}
In your willActivate() use this function like this:
downloadJsonTask(url: taskUrl) {
print(tasks.count)
}
So that means when you get your data, your code inside curly braces will get executed.
You’re correct in your assumption that tasks has not yet been assigned a value when it’s first printed.
The thing is network requests are performed asynchronously. It means that iOS does not wait until downloadJsonTask(url:) is finished but continues executing the code right away (i.e. it calls print(tasks.count) immediately after the network request started, without waiting for it to produce any results).
The piece of code inside brackets after URLSession.shared.dataTask(with:) is called a completion handler. This code gets executed once the network request is competed (hence the name). The tasks variable is assigned a value only when the request is finished. You can make sure it works by adding print(self.tasks.count) after self.tasks = downloadedTasks.tasks:
self.tasks = downloadedTasks.tasks
print(self.tasks)
print(downloadedTasks.tasks.count)
This question already has answers here:
Returning data from async call in Swift function
(13 answers)
Closed 17 days ago.
Please have a look into the code below:
backgroundthread.async {
return self.mycallback() //return string, int etc
}
I want to return a value from an async block. I don't want any completion handler or any other workaround.
func getAppConfigFromDB(_ key: String) -> String
{
let value = String()
backgroundthread.async {
let inst = AppConfigDB.init(_APP_CONFIG_DB_PATH)
value = inst.getConfigurationInfo(key) // I want to return from here.
}
return value
}
getAppConfigFromDB("path")
Like #rmaddy said, you have no other way than to use completion handlers.
func getAppConfigFromDB(_ key: String, completion: #escaping ((String) -> Void)) {
let value = String()
backgroundthread.async {
let inst = AppConfigDB.init(_APP_CONFIG_DB_PATH)
value = inst.getConfigurationInfo(key) // I want to return from here.
completion(value)
}
}
You call the method like this.
getAppConfigFromDB("") { (value) in
// Use value to do something
}
Your function would need a closure like so
func getAppConfigFromDB(_ key: String, completion: #escaping (String?) -> Void) {
backgroundthread.async {
completion("string here")
}
}
When you call your function you would do
getAppConfigFromDB("key") { (returnedString) in
//returnedString is Optional("string here")
}
What you are describing with return self.mycallback() is something like in
TypeScript:
String text = await someTask();
or in C#:
String text = someTask().result;
and from an async function:
String text = await someTask();
However this concept does not exist in swift (and i think Java too).
The only other method I can think of other than using a completion handler, is passing the results to another function (NOTE: If you intend on working on the main thread/UI Thread you will get an exception - you will need to wrap your call in DispatchQueue.main.async {/*Do stuff...*/}) like so
func startAsync {
let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
guard let data = data else { return }
guard let jsonString = String(data: data, encoding: .utf8) else { return }
DispatchQueue.main.async {
self.setResults(text: jsonString);
}
}
task.resume()
}
func setResults(text: String?){
textView.text = text;
}
full project:
https://github.com/AppLogics/SwiftAsyncTaskWithoutCompletionHandler