Downloading Images in order of url's - iOS Swift - ios

I have 10 urls in an array and when 4 of them downloaded I need to display them. Im using Semaphores and groups to implement . But looks like im hitting deadlock. Not sure how to proceed. Please advice how I can
Simulating same in playground:
PlaygroundPage.current.needsIndefiniteExecution = true
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async(group: group) {
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
nums.append(i)
print("Hola image \(i)")
if nums.count == 4 {
print("4 downloaded")
semaphore.signal()
group.leave()
}
}
if nums.count == 4 {
break
}
}
group.notify(queue: DispatchQueue.main) {
print(nums)
}
I get this in o/p console
> Downloading image 1
> Downloading image 2
> Downloading image 3
> Downloading image 4
Semaphores(41269,0x70000ade5000) malloc: *** error for object 0x1077d4750: pointer being freed was not allocated
Semaphores(41269,0x70000ade5000) malloc: *** set a breakpoint in malloc_error_break to debug
I'm expecting to print [1,2,3,4] in order
I know im trying to access a shared resource in async but not sure how I can fix this. Please advice
Also How can I use this with semaphore's if I want to download 4,4,2 tasks at a time so it display [1,2,3,4,5,6,7,8,9,10] in my ouput

Your title says “Downloading Images in order of url’s”, but your code snippet is not attempting to do that. It appears to be attempting to use semaphores to constrain the download to four images at a time, but it won’t guarantee that they’ll be in order.
It is commendable that this code snippet isn’t attempting to download them in order, sequentially, one after another, because that would impose a huge performance penalty. It is also good that this code snippet is constraining this degree of concurrency to something reasonable, thereby avoiding exhausting worker threads or causing some of the latter requests to timeout. So, the idea of using semaphore to allow concurrent image download, but constrain it to four at a time, is a fine approach; we only need to sort the results at the end if you want them in order.
But before we get to that, let’s tackle a bunch of problems in the supplied code snippet:
You are calling group.enter() and semaphore.wait() for every iteration (which is correct), but group.leave() and semaphore.signal() only when i is 4 (which is not correct). You want to leave and signal for every iteration.
Obviously, that break call is not needed, either.
So, to fix this “do four at a time” process, one can simplify this code:
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async() { // NB: the `group` parameter is not needed
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
nums.append(i)
print("Hola image \(i)")
semaphore.signal()
group.leave()
}
}
group.notify(queue: .main) {
print(nums)
}
That will download four images at a time and will call your group.notify closure when they’re all done.
While the above fixes the semaphore and group logic, there is yet another problem lurking in the above code snippet. It is updating that nums array from multiple background threads, but Array is not thread-safe. So you should synchronize those updates to that array. An easy way to achieve this is to dispatch that update back to the main thread. (Any serial queue would have been fine, but the main thread works fine for this purpose.)
Also, since one should never call wait on the main queue, so I’d suggest that you explicitly dispatch this entire for loop to a background thread:
DispatchQueue.global(qos: .utility).async {
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .userInteractive)
let semaphore = DispatchSemaphore(value: 4)
var nums: [Int] = []
for i in 1...10 {
group.enter()
semaphore.wait()
queue.async() {
print("Downloading image \(i)")
// Simulate a network wait
Thread.sleep(forTimeInterval: 3)
DispatchQueue.main.async {
nums.append(i)
print("Hola image \(i)")
}
semaphore.signal()
group.leave()
}
}
group.notify(queue: .main) {
print(nums)
}
}
That is now the correct “do four at a time and let me know when it’s done.”
OK, now that we’re downloading all of the images properly, let’s figure out how to sort the results. Frankly, I think it’s easier to follow what’s going on if we imagine that we have some image download method, like so, that downloads a particular image:
func download(_ url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) { ... }
Then the routine to (a) download the images, no more than four at a time; and (b) return the results back in order, might look like:
func downloadAllImages(_ urls: [URL], completion: #escaping ([UIImage]) -> Void) {
DispatchQueue.global(qos: .utility).async {
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 4)
var imageDictionary: [URL: UIImage] = [:]
// download the images
for url in urls {
group.enter()
semaphore.wait()
self.download(url) { result in
defer {
semaphore.signal()
group.leave()
}
switch result {
case .failure(let error):
print(error)
case .success(let image):
DispatchQueue.main.async {
imageDictionary[url] = image
}
}
}
}
// now sort the results
group.notify(queue: .main) {
completion(urls.compactMap { imageDictionary[$0] })
}
}
}
And you’d call it like so:
downloadAllImages(urls) { images in
self.images = images
self.updateUI() // do whatever you want to trigger the update of the UI
}
FWIW, the “download single image” routine might look like:
enum DownloadError: Error {
case notImage
case invalidStatusCode(URLResponse)
}
func download(_ url: URL, completion: #escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse, error == nil else {
completion(.failure(error!))
return
}
guard 200..<300 ~= response.statusCode else {
completion(.failure(DownloadError.invalidStatusCode(response)))
return
}
guard let image = UIImage(data: data) else {
completion(.failure(DownloadError.notImage))
return
}
completion(.success(image))
}
}
And this is using the Swift 5 Result enumeration. If you’re using an earlier version of Swift, you can define a simple rendition of this enum yourself:
enum Result<Success, Failure> {
case success(Success)
case failure(Failure)
}
Finally, it’s worth noting a few other alternatives:
Wrap your network request in asynchronous Operation subclass and add them to an operation queue whose maxConcurrentOperationCount is set to 4. If you’re interested in this approach, I can supply some references.
Use an image downloading library like Kingfisher.
Instead of manual downloading of all the images, use the UIImageView extension (such as provided by Kingfisher) and completely abandon the “download all images” process at all, and move to a pattern where you simply instruct your image views to asynchronously retrieve the images in either a just-in-time manner (or prefetching).

Related

URLSession.shared.dataTask vs dataTaskPublisher, when to use which?

I recently encounter two data fetching (download) API that performs seemingly the same thing to me. I cannot see when should I use one over the other.
I can use URLSession.shared.dataTask
var tasks: [URLSessionDataTask] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
let task = URLSession.shared.dataTask(with: tuple.imageURL, completionHandler :
{ data, response, error in
guard let data = data, error == nil else { return }
DispatchQueue.main.async() { [weak self] in
self?.displayFlag(data: data, title: tuple.name)
}
})
tasks.append(task)
task.resume()
}
deinit {
tasks.forEach {
$0.cancel()
}
}
Or I can use URLSession.shared.dataTaskPublisher
var cancellables: [AnyCancellable] = []
func loadItems(tuple : (name : String, imageURL : URL)) {
URLSession.shared.dataTaskPublisher(for: tuple.imageURL)
.sink(
receiveCompletion: {
completion in
switch completion {
case .finished:
break
case .failure( _):
return
}},
receiveValue: { data, _ in DispatchQueue.main.async { [weak self] in self?.displayFlag(data: data, title: tuple.name) } })
.store(in: &cancellables)
}
deinit {
cancellables.forEach {
$0.cancel()
}
}
I don't see their distinct differences, as both also can fetch, and both also provide us the ability to cancel the tasks easily. Can someone shed some light on their differences in terms of when to use which?
The first one is the classic. It has been present for quite some time now and most if not all developers are familiar with it.
The second is a wrapper around the first one and allows combining it with other publishers (e.g. Perform some request only when first two requests were performed). Combination of data tasks using the first approach would be far more difficult.
So in a gist: use first one for one-shot requests. Use second one when more logic is needed to combine/pass results with/to other publishers (not only from URLSession). This is, basically, the idea behind Combine framework - you can combine different ways of async mechanisms (datatasks utilising callbacks being one of them).
More info can be found in last year's WWDC video on introducing combine.

Completion handlers and Operation queues

I am trying to do the following approach,
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 10
func registerUser(completionHandler: #escaping (Result<Data, Error>) -> Void) -> String {
self.registerClient() { (result) in
switch result {
case .success(let data):
self.downloadUserProfile(data.profiles)
case .failure(let error):
return self.handleError(error)
}
}
}
func downloadUserProfile(urls: [String]) {
for url in urls {
queue.addOperation {
self.client.downloadTask(with: url)
}
}
}
I am checking is there anyway I can get notified when all operations gets completed and then I can call the success handler there.
I tried checking the apple dev documentation which suggests to use
queue.addBarrierBlock {
<#code#>
}
but this is available only from iOS 13.0
Pre iOS 13, we’d use dependencies. Declare a completion operation, and then when you create operations for your network requests, you’d define those operations to be dependencies for your completion operation.
let completionOperation = BlockOperation { ... }
let networkOperation1 = ...
completionOperation.addDependency(networkOperation1)
queue.addOperation(networkOperation1)
let networkOperation2 = ...
completionOperation.addDependency(networkOperation2)
queue.addOperation(networkOperation2)
OperationQueue.main.addOperation(completionOperation)
That having been said, you should be very careful with your operation implementation. Do I correctly infer that downloadTask(with:) returns immediately after the download task has been initiated and doesn’t wait for the request to finish? In that case, neither dependencies nor barriers will work the way you want.
When wrapping network requests in an operation, you’d want to make sure to use an asynchronous Operation subclass (e.g. https://stackoverflow.com/a/32322851/1271826).
The pre-iOS 13 way is to observe the operationCount property of the operation queue
var observation : NSKeyValueObservation?
...
observation = operationQueue.observe(\.operationCount, options: [.new]) { observed, change in
if change.newValue == 0 {
print("operations finished")
}
}
}

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

How to call 20 webservices at same time in iOS with Swift?

In my application I have a requirement to get the response before loading the tableview. I need to call around 20 API's at same time. And each API data need to show each 1 cell in tableview.
I need to call them in Viewdidload method which calls before tableview methods.
Can anyone guide or provide some useful example to do this?
My suggestion is to use GCD's groups for that.
let backgroundQueue = DispatchQueue.global(attributes: .qosDefault)
let group = DispatchGroup()
var dataForTable:[String] = []
for number in 0..<n {
group.enter()
// Do your request with async callback, append data and leave GCD group.
backgroundQueue.async(group: group, execute: {
let newData = String()
dataForTable.append(newData)
group.leave()
})
}
group.notify(queue: DispatchQueue.main, execute: {
print("All requests data")
self.tableViewData = dataForTable
self.tableView.reloadData()
})
You should use dispatch groups like this:
let group = DispatchGroup()
group.enter()
networkCall1 {
// response received
group.leave()
}
group.enter()
networkCall2 {
// response received
group.leave()
}
group.notify(queue: DispatchQueue.main, execute: {
// this will be notified only when there is no one left in the group
})
Before a network call you enter a group. When you receive a response you leave the group and when there is no one left in the group, group.notify block will execute.
This is just a simple explanation, you should read more about it to fully understand how it works.

Swift, dispatch_group_wait not waiting

I am trying to use grand central dispatch to wait for files to finish download before continuing. This question is a spin-off from this one: Swift (iOS), waiting for all images to finish downloading before returning.
I am simply trying to find out how to get dispatch_group_wait (or similar) to actually wait and not just continue before the downloads have finished. Note that if I use NSThread.sleepForTimeInterval instead of calling downloadImage, it waits just fine.
What am I missing?
class ImageDownloader {
var updateResult = AdUpdateResult()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)
private let group = dispatch_group_create()
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func downloadImages(imageFilesOnServer: [AdFileInfo]) {
dispatch_group_async(group, downloadQueue) {
for serverFile in imageFilesOnServer {
print("Start downloading \(serverFile.fileName)")
//NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
self.downloadImage(serverFile)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish. Why?
print("All Done!") // It gets here too early!
}
private func downloadImage(serverFile: AdFileInfo) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
print("Done downloading \(serverFile.fileName)")
}
}
}
}
Note: these downloads are in response to an HTTP POST request and I am using an HTTP server (Swifter) which does not support asynchronous operations, so I do need to wait for the full downloads to complete before returning a response (see original question referenced above for more details).
When using dispatch_group_async to call methods that are, themselves, asynchronous, the group will finish as soon as all of the asynchronous tasks have started, but will not wait for them to finish. Instead, you can manually call dispatch_group_enter before you make the asynchronous call, and then call dispatch_group_leave when the asynchronous call finish. Then dispatch_group_wait will now behave as expected.
To accomplish this, though, first change downloadImage to include completion handler parameter:
private func downloadImage(serverFile: AdFileInfo, completionHandler: (NSError?)->()) {
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
print("Done downloading \(serverFile.fileName)")
}
completionHandler(error)
}
}
I've made that a completion handler that passes back the error code. Tweak that as you see fit, but hopefully it illustrates the idea.
But, having provided the completion handler, now, when you do the downloads, you can create a group, "enter" the group before you initiate each download, "leave" the group when the completion handler is called asynchronously.
But dispatch_group_wait can deadlock if you're not careful, can block the UI if done from the main thread, etc. Better, you can use dispatch_group_notify to achieve the desired behavior.
func downloadImages(_ imageFilesOnServer: [AdFileInfo], completionHandler: #escaping (Int) -> ()) {
let group = DispatchGroup()
var downloaded = 0
group.notify(queue: .main) {
completionHandler(downloaded)
}
for serverFile in imageFilesOnServer {
group.enter()
print("Start downloading \(serverFile.fileName)")
downloadImage(serverFile) { error in
defer { group.leave() }
if error == nil {
downloaded += 1
}
}
}
}
And you'd call it like so:
downloadImages(arrayOfAdFileInfo) { downloaded in
// initiate whatever you want when the downloads are done
print("All Done! \(downloaded) downloaded successfully.")
}
// but don't do anything contingent upon the downloading of the images here
For Swift 2 and Alamofire 3 answer, see previous revision of this answer.
In Swift 3...
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
// do something, including background threads
dispatchGroup.leave()
dispatchGroup.notify(queue: DispatchQueue.main) {
// completion code
}
https://developer.apple.com/reference/dispatch/dispatchgroup
The code is doing exactly what you are telling it to.
The call to dispatch_group_wait will block until the block inside the call to dispatch_group_async is finished.
The block inside the call to dispatch_group_async will be finished when the for loop completes. This will complete almost immediately since the bulk of the work being done inside the downloadImage function is being done asynchronously.
This means the for loop finishes very quickly and that block is done (and dispatch_group_wait stops waiting) long before any of the actual downloads are completed.
I would make use of dispatch_group_enter and dispatch_group_leave instead of dispatch_group_async.
I would change your code to something like the following (not tested, could be typos):
class ImageDownloader {
var updateResult = AdUpdateResult()
private let fileManager = NSFileManager.defaultManager()
private let imageDirectoryURL = NSURL(fileURLWithPath: Settings.adDirectory, isDirectory: true)
private let group = dispatch_group_create()
private let downloadQueue = dispatch_queue_create("com.acme.downloader", DISPATCH_QUEUE_SERIAL)
func downloadImages(imageFilesOnServer: [AdFileInfo]) {
dispatch_async(downloadQueue) {
for serverFile in imageFilesOnServer {
print("Start downloading \(serverFile.fileName)")
//NSThread.sleepForTimeInterval(3) // Using a sleep instead of calling downloadImage makes the dispatch_group_wait below work
self.downloadImage(serverFile)
}
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // This does not wait for downloads to finish. Why?
print("All Done!") // It gets here too early!
}
private func downloadImage(serverFile: AdFileInfo) {
dispatch_group_enter(group);
let destinationPath = imageDirectoryURL.URLByAppendingPathComponent(serverFile.fileName)
Alamofire.download(.GET, serverFile.imageUrl) { temporaryURL, response in return destinationPath }
.response { _, _, _, error in
if let error = error {
print("Error downloading \(serverFile.fileName): \(error)")
} else {
self.updateResult.filesDownloaded++
print("Done downloading \(serverFile.fileName)")
}
dispatch_group_leave(group);
}
}
}
This change should do what you need. Each call to downloadImage enters the group and it doesn't leave the group until the download completion handler is called.
Using this pattern, the final line will execute when the other tasks are finished.
let group = dispatch_group_create()
dispatch_group_enter(group)
// do something, including background threads
dispatch_group_leave(group) // can be called on a background thread
dispatch_group_enter(group)
// so something
dispatch_group_leave(group)
dispatch_group_notify(group, mainQueue) {
// completion code
}

Resources