How to write a completion handler in a separated block of code in Swift with parameters out of scope - swift

I was trying to make my code clean and decouple the code below, I want to remove the trailing completion handler from it and write the completion handler in another blck of code.
func uploadMarcasMetodoNovo(_ id_resenha: Int) {
let resenhaDados:ResDadoModel = db.readDadosResenhaById(id_resenha)
let resenhaMarcas:[ResMarcasModel] = db.readResMarca(id_resenha)
// this for loop runs about for 7 times
for marca in resenhaMarcas {
contadorUploadMarcas = contadorUploadMarcas + 1
myUploadGroupMarcas.enter()
jsonRequestUploadImagemGrafica = ResMarcasModel.createJsonMarcaResenha(marca, resenhaDados.IdGedave )
let json: [String: Any] = jsonRequestUploadImagemGrafica
guard let jsonData = try? JSONSerialization.data(withJSONObject: json) else {
print("guard jsonData error")
return
}
let requestImagemGrafica = requestUploadFotos(jsonData)
let task = URLSession.shared.dataTask(with: requestImagemGrafica) { data, response, error in
if let error = error {
print("error: \(String(describing: error))")
return
}
print("data")
guard let returnData = String(data: data!, encoding: .utf8) else {
print("returnData guard fail")
return
}
print("returnData")
print(returnData)
self.confirmStatusEnviada(marca)
self.myUploadGroupMarcas.leave()
print("end TASK")
}
task.resume()
}
myUploadGroupMarcas.notify(queue: DispatchQueue.main) {
print("myUploadGroupMarcas notify")
// more code ...
}
}
This is the part that I write creating a separated completion handler
let myCompletionHandler: (Data?, URLResponse?, Error?) -> Void = {
(data, response, error) in
if let error = error {
print("error: \(String(describing: error))")
return
}
print("data")
guard let returnData = String(data: data!, encoding: .utf8) else {
print("returnData guard fail")
return
}
self.confirmStatusEnviada(marca)
self.myUploadGroupMarcas.leave()
}
but it won't work because in the last two lines of code are used paramters that are out of scope. The parameter "marca" and the parameter "myUploadGroupMarcas" are out of scope. Is there a way to use these parameters inside the separated completion handler function?

Ok based on our comments above, this is the route I would try: Write a short completion handler that calls your longer completion handler, passing the variables that are out of scope.
let task = URLSession.shared.dataTask(with: requestImagemGrafica) { data, response, error in
myCompletionHandler(data, response, error, marca, myUploadGroupMarcas)
}
Then you add two parameters to your completion handler in the function definition:
let myCompletionHandler: (Data?, URLResponse?, Error?, MarcaClass, myUploadGroupMarcas) -> Void
Obviously you need to replace MarcaClass with the actual class type that is marca and myUploadGroupMarcas seems to be a function so you'd need to write an appropriate parameter type for that.

Related

Making HTTP GET request with Swift 5

I am obviously missing something very fundamental/naïve/etc., but for the life of me I cannot figure out how to make simple GET requests.
I'm trying to make an HTTP GET request with Swift 5. I've looked at these posts/articles: one, two, but I can't get print() statements to show anything. When I use breakpoints to debug, the entire section within the URLSession.shared.dataTask section is skipped.
I am looking at the following code (from the first link, above):
func HTTP_Request() {
let url = URL(string: "http://www.stackoverflow.com")!
let task = URLSession.shared.dataTask(with: url) {(data: Data?, response: URLResponse?, error: Error?) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
}
task.resume()
}
HTTP_Request()
I am running this in a MacOS Command Line Project created through XCode.
I would greatly appreciate any help I can get on this, thank you.
Right now, if there is an error, you are going to silently fail. So add some error logging, e.g.,
func httpRequest() {
let url = URL(string: "https://www.stackoverflow.com")! // note, https, not http
let task = URLSession.shared.dataTask(with: url) { data, response, error in
guard
error == nil,
let data = data,
let string = String(data: data, encoding: .utf8)
else {
print(error ?? "Unknown error")
return
}
print(string)
}
task.resume()
}
That should at least give you some indication of the problem.
A few other considerations:
If command line app, you have to recognize that the app may quit before this asynchronous network request finishes. One would generally start up a RunLoop, looping with run(mode:before:) until the network request finishes, as advised in the run documentation.
For example, you might give that routine a completion handler that will be called on the main thread when it is done. Then you can use that:
func httpRequest(completion: #escaping () -> Void) {
let url = URL(string: "https://www.stackoverflow.com")! // note, https, not http
let task = URLSession.shared.dataTask(with: url) { data, response, error in
defer {
DispatchQueue.main.async {
completion()
}
}
guard
error == nil,
let data = data,
let string = String(data: data, encoding: .utf8)
else {
print(error ?? "Unknown error")
return
}
print(string)
}
task.resume()
}
var finished = false
httpRequest {
finished = true
}
while !finished {
RunLoop.current.run(mode: .default, before: .distantFuture)
}
In standard macOS apps, you have to enable outgoing (client) connections in the “App Sandbox” capabilities.
If playground, you have to set needsIndefiniteExecution.
By default, macOS and iOS apps disable http requests unless you enable "Allow Arbitrary Loads” in your Info.plist. That is not applicable to command line apps, but you should be aware of that should you try to do this in standard macOS/iOS apps.
In this case, you should just use https and avoid that consideration altogether.
Make sure the response get print before exiting the process, you could try to append
RunLoop.main.run()
or
sleep(UINT32_MAX)
in the end to make sure the main thread won't exit. If you want to print the response and exit the process immediately, suggest using DispatchSemaphore:
let semphare = DispatchSemaphore(value: 0)
func HTTP_Request() {
let url = URL(string: "http://www.stackoverflow.com")!
let task = URLSession.shared.dataTask(with: url) {(data: Data?, response: URLResponse?, error: Error?) in
guard let data = data else { return }
print(String(data: data, encoding: .utf8)!)
semphare.signal()
}
task.resume()
}
HTTP_Request()
_ = semphare.wait(timeout: .distantFuture)
This works for me many times I suggest you snippet for future uses!
let url = URL(string: "https://google.com")
let task = URLSession.shared.dataTask(with: ((url ?? URL(string: "https://google.com"))!)) { [self] (data, response, error) in
do {
let jsonResponse = try JSONSerialization.jsonObject(with: data!, options: [])
print(jsonResponse)
guard let newValue = jsonResponse as? [String:Any] else {
print("invalid format")
}
}
catch let error {
print("Error: \(error)")
}
task.resume()
}

Swift: Setting the text of a label in a URLSessionTask

So I am downloading a JSON file using a URLRequest().
I parse through it in order to get a specific string and I want to set the text of a label I have in my ViewController to that specific string.
I use a CompletionHandler in order to retrieve the function that gets the JSON file from another Swift file.
Here is the code of calling the function and setting the label:
class SecondViewController: UIViewController {
tr = TransportServices()
tr.getLyftData(origin: originstring, destination: destinationstring){ json in
//Parsing JSON in order to get specific data
self.lyftlabel.text = stringexample
}
}
and here is the code of getting the JSON
func getLyftData(origin: String, destination: String, completionHandler: #escaping ([String: Any]) -> ()){
let urlrequest = URLRequest(url: URL(string: urlstring)!)
let config = URLSessionConfiguration.default
let sessions = URLSession(configuration: config)
let task = sessions.dataTask(with: urlrequest) {(data, response, error) in
guard error == nil else {
print(error!)
return
}
guard let responseData = data else {
print("error, did not receive data")
return
}
do {
if let json = try JSONSerialization.jsonObject(with: responseData, options: []) as? [String: Any]{
completionHandler(json)
}
}
catch {
print("Error with URL Request")
}
}
task.resume()
}
This does the job, but in a very slow manner. I know that there is a runtime issue because UILabel.text must be set from main thread only, but I don't know any other way to fix it. Please help.
If you want to set label text in main thread use this:
DispatchQueue.main.async {
self.lyftlabel.text = stringexample
}

how to pass variable value to outside of URLSession async - swift 3

I have this code :
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
print(error!)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let getDetail = parseJSON["detail"] as? String
returnDetail = getDetail!.base64Decoded()
} // parse json end
} // do end
catch {
print(error)
}
} // let task end
returnDetail has been defined previously. I did anything to set returnDetail value to getDetail!.base64Decoded() but it only works inside let task = ...
How can I pass it to the outer scope?
You have several methods to tackle the issue of returning a value from inside an asynchronous function. One of them is to wrap the asynchronous network call inside a function and make it return a completionHandler.
Some general advice: don't use force unwrapping unless you are 100% sure that your optional value won't be nil. With network requests, the data can be nil even if there's no error, so never force unwrap data, use safe unwrapping with if let or guard let. Don't use .mutableContainers in Swift when parsing a JSON value, since it has no effect. The mutability of the parsed JSON object is decided by using the let or var keyword to declare the variable holding it. Also don't use NSDictionary, use its native Swift counterpart, Dictionary ([String:Any] is a shorthand for the type Dictionary<String,Any>).
func getDetail(withRequest request: URLRequest, withCompletion completion: #escaping (String?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completion(nil, error)
return
}
else if let data = data {
do {
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] else {completion(nil, nil);return}
guard let details = json["detail"] as? String else {completion(nil, nil);return}
completion(details, nil)
}
catch {
completion(nil, error)
}
}
}
task.resume()
}
Then you can call this function by
getDetail(withRequest: request, withCompletion: { detail, error in
if error != nil {
//handle error
} else if detail = detail {
//You can use detail here
}
})
I would suggest to use a completion handler.
func foo(withCompletion completion: (String?, Error?) -> Void) {
let task = URLSession.shared.dataTask(with: request) { (data: Data?, response: URLResponse?, error: Error?) in
if error != nil {
completion(nil, error)
return
}
do {
let json = try JSONSerialization.jsonObject(with: data!, options: .mutableContainers) as? NSDictionary
if let parseJSON = json {
let details = parseJSON["detail"] as? String
completion(details, nil)
} // parse json end
} // do end
catch {
completion(nil, error)
}
} // let task end
}
I think, you use CallBack(Clourse) of Swift to return data when getDetail have data.

MACOS App closure never executed

I've created a macOS console app in swift, but the code is never executed, =I have to use Semaphore but is there another way to do this ?
my purpose is to create a method returning a json file
class test{
func gizlo(){
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: "https://itunes.apple.com/fr/rss/topmovies/limit=25/json")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.localizedDescription)
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
print(json)
}
} catch {
print("error in JSONSerialization")
}
}
})
task.resume()
}
}
let tr=test()
tr.gizlo()
Thanks
To avoid Semaphores you can use simple readLine() that will wait for input from the keyboard. Yes it is not obvious but it is woking because it prevent terminal app from exit.
Just add in the and of the file:
_ = readLine()
As Oleg points out, putting readLine() at the end of the top-level code will prevent the program for exiting until you hit Enter in the terminal or wherever FileHandle.standardInput is pointing. That's probably fine for just testing the code quickly in the debugger or in a Playground. An infinite loop would also work, though you'd have to actually terminate it in the debugger or with kill from the command line.
The real issue is why you don't want to use a semaphore. Since they're not difficult to use, I'm going to hazard a guess that it's just because you don't want to pollute your asynchronous data task completion handler with a semaphore when you probably only need it to wait for the data for testing purposes.
Assuming my guess is correct, the real issue isn't actually using a semaphore, it's where you think you need to put them. As David Wheeler once said, "Any problem can be solved by adding a layer of indirection."
You don't want the semaphore explicitly in the completion handler you pass to dataTask. So one solution would be to make gizlo accept a completion handler of its own, and then create a method that calls gizlo with a closure that handles the semaphore. That way you can decouple the two and even add some flexibility for other uses. I've modified your code to do that:
import Foundation
import Dispatch // <-- Added - using DispatchSemaphore
class test{
func gizlo(_ completion: ((Result<[String: Any]?, Error>) -> Void)? = nil) { // <-- Added externally provided completion handler
let config = URLSessionConfiguration.default // Session Configuration
let session = URLSession(configuration: config) // Load configuration into Session
let url = URL(string: "https://itunes.apple.com/fr/rss/topmovies/limit=25/json")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
let result: Result<[String: Any]?, Error>
if let responseError = error { // <-- Changed to optional binding
print(responseError.localizedDescription)
result = .failure(responseError) // <-- Added this
} else {
do {
if let json = try JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any]
{
print(json)
result = .success(json) // <-- Added this
}
else { // <-- Added this else block
result = .success(nil)
}
} catch {
print("error in JSONSerialization")
result = .failure(error) // <-- Added this
}
}
completion?(result) // <-- Added this call
})
task.resume()
}
func blockingGizlo() throws -> [String: Any]? // <-- Added this method
{
let sem = DispatchSemaphore(value: 1)
sem.wait()
var result: Result<[String: Any]?, Error>? = nil
gizlo {
result = $0
sem.signal()
}
sem.wait() // This wait will block until the closure calls signal
sem.signal() // Release the second wait.
switch result
{
case .success(let json) : return json
case .failure(let error) : throw error
case .none: fatalError("Unreachable")
}
}
}
let tr=test()
do {
let json = try tr.blockingGizlo()
print("\(json?.description ?? "nil")")
}
catch { print("Error: \(error.localizedDescription)") }

Swift http request use urlSession

I want to write func for HTTP Request to my server and get some data, when i print it (print(responseString)) it looks good, but when i try to return data, its always empty
public func HTTPRequest(dir: String, param: [String:String]?) -> String{
var urlString = HOST + dir + "?"
var responseString = ""
if param != nil{
for currentParam in param!{
urlString += currentParam.key + "=" + currentParam.value + "&"
}
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print("ERROR: HTTP REQUEST ERROR!")
return
}
guard let data = data else {
print("ERROR: Empty data!")
return
}
responseString = NSString(data: data,encoding: String.Encoding.utf8.rawValue) as! String
print(responseString)
}
task.resume()
return responseString
}
As mentioned in Rob's comments, the dataTask closure is run asynchronously. Instead of returning the value immediately, you would want to provide a completion closure and then call it when dataTask completes.
Here is an example (for testing, can be pasted to Xcode Playground as-is):
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
let HOST = "http://example.org"
public func HTTPRequest(dir: String, param: [String: String]?, completion: #escaping (String) -> Void) {
var urlString = HOST + dir + "?"
if param != nil{
for currentParam in param! {
urlString += currentParam.key + "=" + currentParam.value + "&"
}
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard error == nil else {
print("ERROR: HTTP REQUEST ERROR!")
return
}
guard let data = data else {
print("ERROR: Empty data!")
return
}
let responseString = NSString(data: data,encoding: String.Encoding.utf8.rawValue) as! String
completion(responseString)
}
task.resume()
}
let completion: (String) -> Void = { responseString in
print(responseString)
}
HTTPRequest(dir: "", param: nil, completion: completion)
You need to use completion block instead of returning value because the dataTask closure is run asynchronously, i.e. later, well after you return from your method. You don't want to try to return the value immediately (because you won't have it yet). You want to (a) change this function to not return anything, but (b) supply a completion handler closure, which you will call inside the dataTask closure, where you build responseString.
For example, you might define it like so:
public func HTTPRequest(dir: String, param: [String:String]? = nil, completionHandler: #escaping (String?, Error?) -> Void) {
var urlString = HOST + dir
if let param = param {
let parameters = param.map { return $0.key.percentEscaped() + "=" + $0.value.percentEscaped() }
urlString += "?" + parameters.joined(separator: "&")
}
let url = URL(string: urlString)
let task = URLSession.shared.dataTask(with: url!) { data, response, error in
guard let data = data, error == nil else {
completionHandler(nil, error)
return
}
let responseString = String(data: data, encoding: .utf8)
completionHandler(responseString, nil)
}
task.resume()
}
Note, I'm percent escaping the values in the parameters dictionary using something like:
extension String {
/// Percent escapes values to be added to a URL query as specified in RFC 3986
///
/// This percent-escapes all characters besides the alphanumeric character set and "-", ".", "_", and "~".
///
/// http://www.ietf.org/rfc/rfc3986.txt
///
/// - Returns: Returns percent-escaped string.
func percentEscaped() -> String {
let allowedCharacters = CharacterSet(charactersIn: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~")
return self.addingPercentEncoding(withAllowedCharacters: allowedCharacters)!
}
}
And then you'd call it like so:
HTTPRequest(dir: directory, param: parameterDictionary) { responseString, error in
guard let responseString = responseString else {
// handle the error here
print("error: \(error)")
return
}
// use `responseString` here
DispatchQueue.main.async {
// because this is called on background thread, if updating
// UI, make sure to dispatch that back to the main queue.
}
}
// but don't try to use `responseString` here