DispatchQueue vs Delegates vs Closures in Swift - ios

Excuse me if this a noobish question but I don't know the difference between executing a block of code after an API request is received and parsed via GCD, delegates and closures.
As far as I know, a creating a session to download data from an API URL is done on the main thread unless I execute the code inside a a GCD block or a delegate or a closure.
Here are two examples:
Using GCD
DispatchQueue.global(qos: .utility).async {
let requestURL = URL(string: "http://echo.jsontest.com/key/value/one/two")
let session = URLSession.shared
let task = session.dataTask(with: requestURL!) {
(data, response, error) in
print(data as Any)
DispatchQueue.main.async {
print("Hello")
}
}
task.resume()
}
Using Delegate:
import Foundation
import UIKit
protocol WeatherDataDownloaderProtocol {
func setData(weatherData: WeatherData)
}
class WeatherDataDownloader {
var weatherData = WeatherData()
var delegate: WeatherDataDownloaderProtocol?
func downloadWeatherData() {
let API_URL = WEATHER_FORECAST_URL
guard let URL = URL(string: API_URL) else {
print("Error: No valid URL")
return
}
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
let task = session.dataTask(with: URL) { (data, response, error) in
guard error == nil else {
print("Error getting data")
print("\(error)")
return
}
guard let responseData = data else {
print("Error: Did not receive data")
return
}
do {
guard let JSON = try JSONSerialization.jsonObject(with: responseData, options: []) as? Dictionary<String, AnyObject> else {
print("Error: Error trying to convert data to JSON")
return
}
print(JSON)
self.sendDataBack()
} catch {
print("Error: Parsing JSON data error")
return
}
}
task.resume()
}
func sendDataBack() {
if let _delegate = delegate {
_delegate.setData(weatherData: weatherData)
}
}
}
Both, print("Hello") and print(JSON) + self.sendDataBack() will execute after the JSON is retrieved and parsed. What's the difference between both methods? Does it have anything to do with whether my app would crash if I navigate out of the viewController while waiting for the network response?
Thanks a lot

In your first approach, the .async call is not necessary. URLSession dataTask is a background task.
So the choice is not GDC vs. delegates but completion handler vs. delegate.
Opinion based:
Using a delegate is more work and harder to read because you have to check in other areas of the code if the delegate is actually set and who it is and what it actually does.
Also no code might be executed in case the delegate does not exist any more at the time your network call has finished. So for this case I plead for using a completion closure.

Both are correct. The block/closure approach is newer and considered to have better readability since you don't have to jump between functions and even between files to follow the course of your code.

DispatchQueue.global(qos: .utility).async {
let requestURL = URL(string: "http://echo.jsontest.com/key/value/one/two")
let session = URLSession.shared
let task = session.dataTask(with: requestURL!) {
(data, response, error) in
print(data as Any)
DispatchQueue.main.async {
print("Hello")
}
}
task.resume()
}
In this method your service hits in background thread and when you completed your in background thread you come back in main thread using this method
DispatchQueue.main.async {
print("Hello")
}
and then your print("Hello") will call in main thread.
While the method
downloadWeatherData
defined in appdelegate also hits the service in background thread but in the manner of closure because closure also works like a background thread. Using closure when your task completes your control automatically comes back in main thread where you call print(JSON).
Now comes to your problem, the best thing is that you should wait untill your task complete and you get the json response on your viewcontroller then move to your next controller other your app may crash in some situations.

Related

Return to the same queue as network call was performed

I perform the next code
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
DispatchQueue.main.async {
completion(.failure(error))
}
}
}
All is clear, after doing network request in background I return completion to .main . But how to handle case, if I want to call completion not in .main but in thread in which session.uploadTask was initiated, because in my application it could be not only .main .
There is not a GCD mechanism to retrieve the current dispatch queue so that you can later dispatch to it. (A long time ago, there used to be a way to fetch the current queue, but it was deprecated back in iOS 7, and even then it was “Recommended for debugging and logging purposes only.”)
If you want to call the completion handlers on specific dispatch queue, I would suggest supplying an explicit DispatchQueue parameter to the method. Below, I have it default to .main, but the caller can override that with whatever it wants:
func perform(_ request: URLRequest, with data: Data, on queue: DispatchQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.async {
completion(.failure(error))
}
}
...
}
...
}
I know that this is not precisely what you are looking for, but it is an easy way to let the caller specify on which queue your closure will be called.
If you are using operation queues, you can get the current to determine the current operation queue. And should one do this, one would use addOperation on that operation queue in order to call the completion handler.
func perform(_ request: URLRequest, with data: Data, completion: #escaping (Result<T, Error>) -> Void) {
guard let queue = OperationQueue.current else {
fatalError("Must be called from operation queue")
}
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
But this pattern only works if the caller was using operation queue, not when only using dispatch queues. For this reason, I would still be inclined to adopt the pattern of supplying the target operation queue as a parameter:
func perform(_ request: URLRequest, with data: Data, on queue: OperationQueue = .main, completion: #escaping (Result<T, Error>) -> Void) {
let task = session.uploadTask(with: request, from: data) { data, response, error in
if let error = error {
queue.addOperation {
completion(.failure(error))
}
}
...
}
...
}
If you would like to call completion on the current thread simply call completion().
current thread
completion(.failure(error))
main thread
DispatchQueue.main.async {
completion(.failure(error))
}
background thread
Dispatch.global(qos: .background) {
completion(.failure(error))
}
I used this solution:
guard let currentDispatch = OperationQueue.current?.underlyingQueue else {
return
}
let task = session.uploadTask(with: request, from: requestData.body) { data, response, error in
if let error = error {
currentDispatch.async {
completion(.failure(error))
}
}
}
Have not tested it too much, but quick debugging shows what I wanted to achieve,

Having Trouble With Completion Handlers and Closures in Swift

Background
The function below calls two functions, which both access an API, retrieve JSON data, parse through it, etc, and then take that data and populates the values of an object variable in my View Controller class.
func requestWordFromOxfordAPI(word: String, completion: (_ success: Bool) -> Void) {
oxfordAPIManager.fetchDictData(word: word)
oxfordAPIManager.fetchThesData(word: word)
completion(true)
}
Normally, if there was only one function fetching data, and I wanted to call a new function that takes in the data results and does something with them, I would use a delegate method and call it within the closure of the data fetching function.
For Example:
Here, I fetch data from my firebase database and if retrieving the data is succesful, I call self.delegate?.populateWordDataFromFB(result: combinedModel). Since closures occur on separate thread, this ensures that the populateWordDataFromFB function runs only once retrieving the data has finished. Please correct me if I am wrong. I have just recently learned this and am still trying to see the whole picture.
func readData(word: String) {
let docRef = db.collection(K.FBConstants.dictionaryCollectionName).document(word)
docRef.getDocument { (document, error) in
let result = Result {
try document.flatMap {
try $0.data(as: CombinedModel.self)
}
}
switch result {
case .success(let combinedModel):
if let combinedModel = combinedModel {
self.delegate?.populateWordDataFromFB(result: combinedModel)
} else {
self.delegate?.fbDidFailWithError(error: nil, summary: "\(word) not found, requesting from OxfordAPI")
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
}
case .failure(let error):
self.delegate?.fbDidFailWithError(error: error, summary: "Error decoding CombinedModel")
}
}
}
Also notice from the above code that if the data is not in firebase, I call the delegate method below, which is where I am running into my issue.
self.delegate?.requestWordFromOxfordAPI(word: word, completion: { (success) in
if success {
self.delegate?.populateWordDataFromOX()
} else {print("error with completion handler")}
})
My Issue
What I am struggling with is the fact that the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions both have closures.
The body of these functions look like this:
if let url = URL(string: urlString) {
var request = URLRequest(url: url)
request.addValue(K.APISettings.acceptField, forHTTPHeaderField: "Accept")
request.addValue(K.APISettings.paidAppID , forHTTPHeaderField: "app_id")
request.addValue(K.APISettings.paidAppKey, forHTTPHeaderField: "app_key")
let session = URLSession.shared
_ = session.dataTask(with:request) { (data, response, error) in
if error != nil {
self.delegate?.apiDidFailWithError(error: error, summary: "Error performing task:")
return
}
if let safeData = data {
if let thesaurusModel = self.parseThesJSON(safeData) {
self.delegate?.populateThesData(thesModel: thesaurusModel, word: word)
}
}
}
.resume()
} else {print("Error creating thesaurus request")}
I assume both of these functions are running on separate threads in the background. My goal is to call another function once both the oxfordAPIManager.fetchDictData(word: word) and oxfordAPIManager.fetchThesData(word: word) functions run. These two functions will populate the values of an object variable in my view controller which I will use in the new function. I don't want the new function to be called before the object variable in the view controller is populated with the right data so I tried to implement a completion handler. The completion handler function is being called BEFORE the two functions terminate, so when the new function tries to access the object variable in the View Controller, it's empty.
This is my first time trying to implement a completion handler and I tried to follow some other stack overflow posts but was unsuccessful. Also if this is the wrong approach let me know too, please. Sorry for the long explanation and thank you for any input.
Use DispatchGroup for this,
Example:
Create a DispatchGroup,
let group = DispatchGroup()
Modify the requestWordFromOxfordAPI(word: completion:) method to,
func requestWordFromOxfordAPI(word: String, completion: #escaping (_ success: Bool) -> Void) {
fetchDictData(word: "")
fetchThesData(word: "")
group.notify(queue: .main) {
//code after both methods are executed
print("Both methods executed")
completion(true)
}
}
Call enter() and leave() methods of DispatchGroup at the relevant places in fetchDictData(word:) and fetchThesData(word:) methods.
func fetchDictData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
func fetchThesData(word: String) {
group.enter()
URLSession.shared.dataTask(with: url) { (data, response, error) in
//your code
group.leave()
}.resume()
}
At last call requestWordFromOxfordAPI(word: completion:)
requestWordFromOxfordAPI(word: "") { (success) in
print(success)
}

EXC_BAD_ACCESS (code=2) when using JSONEncoder.encode()

I have a (custom, linked-list based) queue that I want to deserialize when the app starts and serialize when the app stops, like so (AppDelegate.swift):
func applicationWillResignActive(_ application: UIApplication) {
RequestManager.shared.serializeAndPersistQueue()
}
func applicationDidBecomeActive(_ application: UIApplication) {
RequestManager.shared.deserializeStoredQueue()
}
The issue is during serialization when I exit the app. Here's the code that's running:
public func serializeAndPersistQueue() {
do {
let encoder = JSONEncoder()
let data = try encoder.encode(queue) // Bad access here
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
}
catch {
print(error)
}
}
As you can see, fairly straightforward. It uses the JSONEncoder to convert my queue to a data object, then writes that data to the file at url.
However, during encoder.encode() I get EXC_BAD_ACCESS every time, without fail.
Additionally, I should note that peak and dequeue operations are conducted on the queue from a background thread. I'm not sure if that makes a difference due to my lack of understanding surrounding GCD. Here's what that method looks like:
private func processRequests() {
DispatchQueue.global(qos: .background).async { [unowned self] in
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 0)
while !self.queue.isEmpty {
group.enter()
let request = self.queue.peek()!
self.sendRequest(request: request, completion: { [weak self] in
_ = self?.queue.dequeue()
semaphore.signal()
group.leave()
})
semaphore.wait()
}
group.notify(queue: .global(), execute: { [weak self] in
print("Ending the group")
})
}
}
Lastly, I'll note that:
My queue conforms to the Codable protocol just fine––well, there are no compiler errors, at least. If its implementation beyond that matters, let me know and I'll show it.
The crash occurs a few seconds after I exit the app, while the execution of the processRequests function stops immediately after

Setting a timeout for a request method

I'm trying to set up a timeout for a request method that checks username availability. When the user types in a username and presses a button, the checkUsername method is called. My code is not working because the code inside Timeout(5.0){} is never executed and timeout never gets the value false. I know this is not the best way to do it but I wanted to give it a try and wonder if this can be modified in some way or do I need a different approach?
var timeout: Bool = false
func usernameAvailable(username: String) -> String{
let response: String!
response = Server.checkUsername(username!)
Timeout(5.0){
self.timeout = true
}
while(!timeout){
if(response != nil){
return response
}
}
return "Timeout"
}
The Timeout.swift class looks like this and is working
class Timeout: NSObject{
private var timer: NSTimer?
private var callback: (Void -> Void)?
init(_ delaySeconds: Double, _ callback: Void -> Void){
super.init()
self.callback = callback
self.timer = NSTimer.scheduledTimerWithTimeInterval(NSTimeInterval(delaySeconds),
target: self, selector: "invoke", userInfo: nil, repeats: false)
}
func invoke(){
self.callback?()
// Discard callback and timer.
self.callback = nil
self.timer = nil
}
func cancel(){
self.timer?.invalidate()
self.timer = nil
}
}
I see what you are trying to do and it would make more sense to use an existing framework unless you really need/want to write your own networking code.
I would suggest instead to use the timeoutInterval support in an NSURLRequest along with a completion handler on NSURLSession to achieve the solution that you are seeking.
A timeout of the server response can be handled in the completion handler of something like an NSURLSessionDataTask.
Here is a working example to help get you started that retrieves data from the iTunes Store to illustrate how your timeout could be handled:
let timeout = 5 as NSTimeInterval
let searchTerm = "philip+glass"
let url = NSURL(string: "https://itunes.apple.com/search?term=\(searchTerm)")
let request: NSURLRequest = NSURLRequest(URL: url!,
cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData,
timeoutInterval: timeout)
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config)
let task: NSURLSessionDataTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if response == nil {
print("Timeout")
} else {
print(String(data: data!, encoding: NSUTF8StringEncoding))
}
}
)
task.resume()
If you reduce the timeout interval to something short, you can force the timeout to happen.
The code in the Timeout block will never run because the timer will fire on the on the main thread, but you're blocking the main thread with your while loop.
You have another issue here, that you're calling Server.checkUsername(username!) and returning that result, which would suggest that this must be a synchronous call (which is not good). So, this is also likely blocking the main thread there. It won't even try to start the Timeout logic until checkUsername returns.
There are kludgy fixes for this, but in my opinion, this begs for a very different pattern. One should never write code that has a spinning while loop that is polling some completion status. It is much better to adopt asynchronous patterns with completionHandler closures. But without more information on what checkUsername is doing, it's hard to get more specific.
But, ideally, if your checkUsername is building a NSMutableURLRequest, just specify timeoutInterval for that and then have the NSURLSessionTask completion block check for NSError with domain of NSURLErrorDomain and a code of NSURLError.TimedOut. You also probably want to cancel the prior request if it's already running.
func startRequestForUsername(username: String, timeout: NSTimeInterval, completionHandler: (Bool?, NSError?) -> ()) -> NSURLSessionTask {
let request = NSMutableURLRequest(URL: ...) // configure your request however appropriate for your web service
request.timeoutInterval = timeout // but make sure to specify timeout
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
dispatch_async(dispatch_get_main_queue()) {
guard data != nil && error == nil else {
completionHandler(nil, error)
return
}
let usernameAvailable = ... // parse the boolean success/failure out of the `data` however appropriate
completionHandler(usernameAvailable, nil)
}
}
task.resume()
return task
}
And you can then use it like so:
private weak var previousTask: NSURLSessionTask?
func checkUsername(username: String) {
// update the UI to say that we're checking the availability of the user name here, e.g.
usernameStatus.text = "Checking username availability..."
// now, cancel prior request (if any)
previousTask?.cancel()
// start new request
let task = startRequestForUsername(username, timeout: 5) { usernameAvailable, error in
guard usernameAvailable != nil && error == nil else {
if error?.domain == NSURLErrorDomain && error?.code == NSURLError.TimedOut.rawValue {
// everything is cool, the task just timed out
} else if error?.domain == NSURLErrorDomain && error?.code != NSURLError.Cancelled.rawValue {
// again, everything is cool, the task was cancelled
} else {
// some error other happened, so handle that as you see fit
// but the key issue that if it was `.TimedOut` or `.Cancelled`, then don't do anything
}
return
}
if usernameAvailable! {
// update UI to say that the username is available
self.usernameStatus.text = "Username is available"
} else {
// update UI to say that the username is not available
self.usernameStatus.text = "Username is NOT available"
}
}
// save reference to this task
previousTask = task
}
By the way, if you do this sort of graceful, asynchronous processing of requests, you can also increase the timeout interval (e.g. maybe 10 or 15 seconds). We're not freezing the UI, so we can do whatever we want, and not artificially constrain the time allowed for the request.

UIActivityIndicatorView not showing for duration of async task

I'm working on an app in iOS wherein I need to start spinning a UIActivityIndicatorView, upload an image to a server, and when the upload is completed, stop spinning the activity indicator.
I'm currently using XCode 7 Beta and am testing the app on the iOS simulator as an iPhone 6 and iPhone 5. My issue is that the activity indicator won't end immediately after file upload, but several (~28 seconds) later. Where should I place my calls to cause it to end?
I have an #IBOutlet function attached to the button I use to start the process, which contains the startAnimating() function, and which calls a dispatch_async method that contains the call to uploadImage, which contains the signal, wait, and stopAnimating() functions.
Note that
let semaphore = dispatch_semaphore_create(0)
let priority = DISPATCH_QUEUE_PRIORITY_HIGH
are defined at the top of my class.
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
} // end dispatch_async
} // works with startAnimating() and stopAnimating() in async but not with uploadImage() in async
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
dispatch_semaphore_signal(self.semaphore)
}
}
})! // end dataTaskWithResult
task.resume()
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
} // end uploadImage
This is just one version of my code, I have moved several things around several different ways. I have tried this:
#IBAction func uploadButton(sender: AnyObject) {
self.activityIndicatorView.startAnimating()
dispatch_async(dispatch_get_global_queue(priority, 0)) {
self.uploadImage(self.myImageView.image!)
dispatch_semaphore_signal(self.semaphore)
} // end dispatch_async
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.activityIndicatorView.stopAnimating()
}
And several, several other ways of moving my code around to attempt to get the activity indicator to display for the duration of the image upload and then immediately quit. In some cases the spinner doesn't appear at all for the duration of program execution. I read this post and this question and have migrated my dispatch_semaphore_wait and stopAnimating() to the uploadImage() method to circumvent this, but can't find enough information in the UIActivityIndicatorView documentation about the UI updating to know any other way of updating it, though I believe this might be at the core of the problem.
All I need is for the spinner to start before the upload process begins (dataTaskWithRequest) and end once it has succeeded or failed. What am I doing wrong?
Instead of using semaphores, you could just dispatch directly to the main thread in your async task,
func uploadImage(image: UIImage) {
let request = self.createRequest(image)
let session : NSURLSession = NSURLSession.sharedSession()
let task : NSURLSessionTask = session.dataTaskWithRequest(request, completionHandler: {
(data, response, error) in
if error != nil {
print(error!.description)
} else {
let httpResponse: NSHTTPURLResponse = response as! NSHTTPURLResponse
if httpResponse.statusCode != 200 {
print(httpResponse.description)
} else {
print("Success! Status code == 200.")
}
}
// dispatch to main thread to stop activity indicator
dispatch_async(disptach_get_main_queue()) {
self.activityIndicatorView.stopAnimating()
}
})! // end dataTaskWithResult
task.resume()
} // end uploadImage

Resources