How to do progress reporting using Async/Await swift? - swift

I have a function that takes 2 callbacks. I want to convert this into async/await. But how can I await while continuously returning the progress also?
I am using https://github.com/yannickl/AwaitKit to get rid of callbacks.
typealias GetResultCallBack = (String) -> Void
typealias ProgressCallBack = (Double) -> Void
func getFileFromS3(onComplete callBack: #escaping GetResultCallBack,
progress progressCallback: #escaping ProgressCallBack) {
}
I am using it like this:
getFileFromS3() { [weak self] (result) in
guard let self = self else { return }
// Do something with result
} progress: { [weak self] (progress) in
guard let self = self else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else {return}
// Update progress in UI
}
}
Here is what converted code looks without progress reporting:
func getFileFromS3() -> Promise<String> {
return async {
// return here
}
}

You could use a technique similar to this:
https://developer.apple.com/documentation/foundation/urlsession/3767352-data
As you can see from the signature...
func data(for request: URLRequest,
delegate: URLSessionTaskDelegate? = nil) async throws
-> (Data, URLResponse)
...it is async, but it also takes a delegate object:
https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate
As you can see, that delegate receives callbacks for task progress. You can declare something similar, and thus feed that info from the delegate over to the main actor and the interface.

I took a while but I finally achieved this result, first you have to start the task setting a delegate:
let (data, _) = try await URLSession.shared.data(for: .init(url: url), delegate: self)
In your delegate you subscribe to the progress update:
public func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
progressObservation = task.progress.observe(\.fractionCompleted) { progress, value in
print("progress: ", progress.fractionCompleted)
}
}
Don't forget that "progressObservation" needs to be a strong reference

Related

Using ProgressView with async/await functions [duplicate]

I have a function that takes 2 callbacks. I want to convert this into async/await. But how can I await while continuously returning the progress also?
I am using https://github.com/yannickl/AwaitKit to get rid of callbacks.
typealias GetResultCallBack = (String) -> Void
typealias ProgressCallBack = (Double) -> Void
func getFileFromS3(onComplete callBack: #escaping GetResultCallBack,
progress progressCallback: #escaping ProgressCallBack) {
}
I am using it like this:
getFileFromS3() { [weak self] (result) in
guard let self = self else { return }
// Do something with result
} progress: { [weak self] (progress) in
guard let self = self else { return }
DispatchQueue.main.async { [weak self] in
guard let self = self else {return}
// Update progress in UI
}
}
Here is what converted code looks without progress reporting:
func getFileFromS3() -> Promise<String> {
return async {
// return here
}
}
You could use a technique similar to this:
https://developer.apple.com/documentation/foundation/urlsession/3767352-data
As you can see from the signature...
func data(for request: URLRequest,
delegate: URLSessionTaskDelegate? = nil) async throws
-> (Data, URLResponse)
...it is async, but it also takes a delegate object:
https://developer.apple.com/documentation/foundation/urlsessiontaskdelegate
As you can see, that delegate receives callbacks for task progress. You can declare something similar, and thus feed that info from the delegate over to the main actor and the interface.
I took a while but I finally achieved this result, first you have to start the task setting a delegate:
let (data, _) = try await URLSession.shared.data(for: .init(url: url), delegate: self)
In your delegate you subscribe to the progress update:
public func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
progressObservation = task.progress.observe(\.fractionCompleted) { progress, value in
print("progress: ", progress.fractionCompleted)
}
}
Don't forget that "progressObservation" needs to be a strong reference

Execute func after first func

self.werteEintragen() should start after weatherManager.linkZusammenfuegen() is done. Right now I use DispatchQueue and let it wait two seconds. I cannot get it done with completion func because I dont know where to put the completion function.
This is my first Swift file:
struct DatenHolen {
let fussballUrl = "deleted="
func linkZusammenfuegen () {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString)
}
func perfromRequest(urlString: String)
{
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
if error != nil{
print(error!)
return
}
if let safeFile = gettingInfo {
self.parseJSON(datenEintragen: safeFile)
}
}
task.resume()
}
}
func parseJSON(datenEintragen: Data) {
let decoder = JSONDecoder()
do {
let decodedFile = try decoder.decode(JsonDaten.self, from: datenEintragen)
TeamOne = decodedFile.data[0].home_name
} catch {
print(error)
}
}
}
And this is my second Swift File as Viewcontroller.
class HauptBildschirm: UIViewController {
func werteEintragen() {
Tone.text = TeamOne
}
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [unowned self] in
self.werteEintragen()
}
}
}
How can I implement this and where?
func firstTask(completion: (_ success: Bool) -> Void) {
// Do something
// Call completion, when finished, success or faliure
completion(true)
}
firstTask { (success) in
if success {
// do second task if success
secondTask()
}
}
You can have a completion handler which will notify when a function finishes, also you could pass any value through it. In your case, you need to know when a function finishes successfully.
Here is how you can do it:
func linkZusammenfuegen (completion: #escaping (_ successful: Bool) -> ()) {
let urlString = fussballUrl + String(Bundesliga1.number)
perfromRequest(urlString: urlString, completion: completion)
}
func perfromRequest(urlString: String, completion: #escaping (_ successful: Bool) -> ()) {
if let url = URL(string: urlString) {
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (gettingInfo, response, error) in
guard error == nil else {
print("Error: ", error!)
completion(false)
return
}
guard let safeFile = gettingInfo else {
print("Error: Getting Info is nil")
completion(false)
return
}
self.parseJSON(datenEintragen: safeFile)
completion(true)
}
task.resume()
} else {
//can't create URL
completion(false)
}
}
Now, in your second view controller, call this func like this:
override func viewDidLoad() {
super.viewDidLoad()
weatherManager.linkZusammenfuegen { [weak self] successful in
guard let self = self else { return }
DispatchQueue.main.async {
if successful {
self.werteEintragen()
} else {
//do something else
}
}
}
}
I highly recommend Google's Promises Framework:
https://github.com/google/promises/blob/master/g3doc/index.md
It is well explained and documented. The basic concept works like this:
import Foundation
import Promises
struct DataFromServer {
var name: String
//.. and more data fields
}
func fetchDataFromServer() -> Promise <DataFromServer> {
return Promise { fulfill, reject in
//Perform work
//This block will be executed asynchronously
//call fulfill() if your value is ready
//call reject() if an error occurred
fulfill(data)
}
}
func visualizeData(data: DataFromServer) {
// do something with data
}
func start() {
fetchDataFromServer
.then { dataFromServer in
visualizeData(data: dataFromServer)
}
}
The closure after "then" will always be executed after the previous Promise has been resolved, making it easy to fulfill asynchronous tasks in order.
This is especially helpful to avoid nested closures (pyramid of death), as you can chain promises instead.

Write unit test for function that uses URLSession and RxSwift

I have a function that creates and returns Observable that downloads and decodes data using URLSession. I wanted to write unit test for this function but have no idea how to tackle it.
function:
func getRecipes(query: String, _ needsMoreData: Bool) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard let data = data else {
return
}
do {
if self.recipes == nil {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList = self.recipes.results
self.baseUrl = self.recipes.baseUrl
} else {
if needsMoreData {
self.recipes = try self.decoder.decode(Recipes.self, from: data)
self.dataList.append(contentsOf: self.recipes.results.suffix(50))
} else {
self.dataList = try self.decoder.decode(Recipes.self, from: data).results
}
}
observer.onCompleted()
} catch let error {
observer.onError(error)
}
}
task.resume()
return Disposables.create {
task.cancel()
}
}
.trackActivity(activityIndicator)
}
The obvious answer is to inject the dataTask instead of using the singleton inside your function. Something like this:
func getRecipes(query: String, _ needsMoreData: Bool, dataTask: #escaping (URL, #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask) -> Observable<[Recipes]> {
guard let url = URL(string: "https://api.spoonacular.com/recipes/search?\(query)&apiKey=myApiKey") else {
return Observable.just([])
}
return Observable.create { observer in
let task = dataTask(url) { (data, response, error) in
// and so on...
You would call it in the main code like this:
getRecipes(query: "", false, dataTask: URLSession.shared.dataTask(with:completionHandler:))
In your test, you would need something like this:
func fakeDataTask(_ url: URL, _ completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
XCTAssertEqual(url, expectedURL)
completionHandler(testData, nil, nil)
return URLSessionDataTask()
}
let result = getRecipes(query: "", false, dataTask: fakeDataTask)
Did you know that URLSession has Reactive extensions already created for it? The one I like best is: URLSession.shared.rx.data(request:) which returns an Observable which will emit an error if there are any problems getting the data. I suggest you use it.

Getting data out of a completion handler and into a tableView

I've been trying to understand this process, I've done a lot of reading and it's just not clicking so I would be grateful if anyone can break this down for me.
I have a method to retrieve JSON from a URL, parse it, and return the data via a completion handler. I could post code but it's all working and I (mostly) understand it.
In my completion handler I can print the data in the console so I know it's there and everything good so far.
The next bit is what's tripping me up. While I can use the data in the completion handler I can't access it from the view controller that contains the handler.
I want to be able to pass tableData.count to numberOfRows and get "Use of unresolved identifier 'tableData'"
I'd really appreciate it if anyone can lay out what I need to do next. Thanks!
Edit: adding code as requested
Here is my completion handler, defined in the ViewController class:
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
print("Here are the \(patients)")
}
in viewDidLoad:
let url = URL(string: "http://***.***.***.***/backend/returnA")
let returnA = URLRequest(url: url!)
retrieveJSON(with: returnA, completionHandler: completionHandler)
Defined in Networking.swift file:
func retrieveJSON(with request: URLRequest, completionHandler: #escaping ([Patient]) -> Void) {
// set up the session
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
// make the request
let task = session.dataTask(with: request as URLRequest) {
// completion handler argument
(data, response, error) in
// completion handler
guard let data = data else {
print("Did not recieve data")
completionHandler([])
return
}
do {
let decoder = JSONDecoder()
let Patient = try decoder.decode(Array<Patient>.self, from: data)
// print(Patient)
completionHandler(Patient)
}
catch let err {
print("Err", err)
completionHandler([])
}
}
task.resume()
}
I also have a struct defined called Patient but I won't post that as it's very long and just a simple struct matching the JSON received.
First of all, when you use closure, you should consider strong reference cycle.
let completionHandler: ([Patient]) -> Void = { [weak self] patients in
guard let strongSelf = self else { return }
strongSelf.tableData = patients // update tableData that must be used with UITableViewDataSource functions.
strongSelf.tableView.reloadData() // notify tableView for updated data.
}
You are not populating the array(tableData) in the closure:
var completionHandler: ([Patient]) -> Void = {[weak self] (patients) in
print("Here are the \(patients)")
self?.tableData = patients
}
var tableData: [Patient] = []
var completionHandler: ([Patient]) -> Void = { (patients) in
self.tableData = patients
self.tableView.reloadData()
//make sure your tableview datasource has tableData property used
}

how to get return value in dispatch block?

How can I get the return value from an asynchronous dispatch block?
I came up with this example code:
if let url = URL(string: "https://google.com/") {
let data: Data? = ***SOME_ASYNC_AWAIT_DISPATCH_GROUP<T>*** { return try? Data(contentsOf: url) }
print("Downloaded Data: \(data)")
}
Goal: Here, I want the async call to produce a result and store it to the data constant so that I can use it.
I am simple using the completion method to do this. Test function download the data from url in background thread and after downloading the completion block run, it returns downloaded data, it may in any formate convert it in your formate.
func UpdateUI(){
test { (data) in
//data is value return by test function
DispatchQueue.main.async {
// Update UI
//do task what you want.
// run on the main queue, after the previous code in outer block
}
}
}
func test (returnCompletion: #escaping (AnyObject) -> () ){
let url = URL(string: "https://google.com/")
DispatchQueue.global(qos: .background).async {
// Background work
let data = try? Data(contentsOf: url!)
// convert the data in you formate. here i am using anyobject.
returnCompletion(data as AnyObject)
}
}
Hope it will help you.
I found a solution.
// REFERENCED TO: https://gist.github.com/kylesluder/478bf8fd8232bc90eabd
struct Await<T> {
fileprivate let group: DispatchGroup
fileprivate let getResult: () -> T
#discardableResult func await() -> T { return getResult() }
}
func async<T>(_ queue: DispatchQueue = DispatchQueue.global() , _ block: #escaping () -> T) -> Await<T> {
let group = DispatchGroup()
var result: T?
group.enter()
queue.async(group: group) { result = block(); group.leave() }
group.wait()
return Await(group: group, getResult: { return result! })
}
Call to like this.
let data = async{ return try? Data(contentsOf: someUrl) }.await()
OR
More simple:
#discardableResult func async<T>(_ block: #escaping () -> T) -> T {
let queue = DispatchQueue.global()
let group = DispatchGroup()
var result: T?
group.enter()
queue.async(group: group) { result = block(); group.leave(); }
group.wait()
return result!
}
Call to like this.
let data = async{ return try? Data(contentsOf: someUrl) }
(And thanks for edit my question, Seaman.)
This should work with a sync block
var result:T? = nil
DispatchQueue.global(qos: .background).sync {
//Do something then assigne to result
}
return result