iOS best way to download bulk images - ios

In my app, I have to download nearly 500 images as I open a ViewController. Downloading all 500 images at a time is not a right idea. I'd like to keep 5 active asynchronous downloads at a time. When any one in five is completed, it should start the next.
I also have a refresh control which will restart downloading all the images from first.
Which technique I could go for to implement this modal?
Here is what I tried so far,
Semaphore is created in property declaration
private var semaphore = dispatch_semaphore_create(5)
After getting web service response,
private func startDownloadingImages() {
for place in places {
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER)
self.downloadImageForPlace(place)
}
}
private func downloadImageForPlace(place: Place) {
ApplicationControls.getImageForPlace(place, withCompletion: { (image, error) -> () in
// error checks
dispatch_async(dispatch_get_main_queue(), {
// UI update
dispatch_semaphore_signal(self.semaphore)
})
})
}
But when I tap refresh control, app locks at dispatch_semaphore_wait and I could able to find a way to reset semaphore.

I would use an OperationQueue like this
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 5;
for url in imageUrls {
queue.addOperationWithBlock { () -> Void in
let img1 = Downloader.downloadImageWithURL(url)
NSOperationQueue.mainQueue().addOperationWithBlock({
//display the image or whatever
})
}
}
you can stop your operations with this
queue.cancelAllOperations();
and then just restart the whole thing.
The only thing that you have to change is that your requests have to be synchronous then. Because this approach wont work with callbacks.

Related

swift ios 10 execute code asynchronously or in the background

When user launch the app or finish editing the data I need to update local notifications, basically it takes around 2-3 seconds in async way. I need to make sure that this code executes even if app leave foreground. What I have now:
func buildLocalNotifications()
let dq = DispatchQueue.global(qos: .userInteractive)
dq.async {
//recreate the notifications
}
}
And I can call this method from didFinishLaunchingWithOptions or when user save the form and everything works like a charm while app stays active for more then 3-4 seconds and its not blocking UI of course.. but if user lock the screen or terminate the app - this code won;t finished and notifications won't be created. How to safely execute sensitive code?
What is coming on my mind - show up a loader while performing this action - but it will block the user interaction
Ok I found the solution for the task which requires some time and should not be interrupted when app leaves foreground.
So we need beginBackgroundTask and endBackgroundTask
Small manager which you can use to execute code even when app is not in foreground
class BackgroundTaskManager {
let backgroundDQ = DispatchQueue.global(qos: .background)
var backgroundUpdateTask: UIBackgroundTaskIdentifier!
init(withName: String) {
self.backgroundUpdateTask = UIApplication.shared.beginBackgroundTask(withName: withName) {}
}
/* Using completion handler to know when code is done*/
func runBackgroundTask(withCode: #escaping (_ cH: #escaping () -> Void) -> Void)
{
backgroundDQ.async {
withCode() {
self.endBackgroungTask()
}
}
}
func endBackgroungTask() {
if backgroundUpdateTask != nil && backgroundUpdateTask != UIBackgroundTaskInvalid {
UIApplication.shared.endBackgroundTask(backgroundUpdateTask)
backgroundUpdateTask = UIBackgroundTaskInvalid
}
}
}
And you can use it like
let taskManager = BackgroundTaskManager(withName: "LocalNotifications")
taskManager.doBackgroundTask() { (cH) in
//Your code goes here
//Send back completion handler so system knows when to finish background task
cH()
}
More information you can find on the Medium
If you want to make sure your code gets executed even if the user closes your app, you need to call your function in applicationWillTerminate. However, you only have ~5 seconds to execute code, before the system closes your app, so asynchronous execution is not encouraged here. It also doesn't matter if you execute code synchronously, since the user already quit your app, so you won't be blocking any UI updates.
Try to excute your code in background
DispatchQueue.global(qos: .background).async {
// your code here
}

API calls blocks UI thread Swift

I need to sync web database in my coredata, for which I perform service api calls. I am using Alamofire with Swift 3. There are 23 api calls, giving nearly 24k rows in different coredata entities.
My problem: These api calls blocks UI for a minute, which is a long time for a user to wait.
I tried using DispatchQueue and performing the task in background thread, though nothing worked. This is how I tried :
let dataQueue = DispatchQueue.init(label: "com.app.dataSyncQueue")
dataQueue.async {
DataSyncController().performStateSyncAPICall()
DataSyncController().performRegionSyncAPICall()
DataSyncController().performStateRegionSyncAPICall()
DataSyncController().performBuildingRegionSyncAPICall()
PriceSyncController().performBasicPriceSyncAPICall()
PriceSyncController().performHeightCostSyncAPICall()
// Apis which will be used in later screens are called in background
self.performSelector(inBackground: #selector(self.performBackgroundTask), with: nil)
}
An API call from DataSyncController:
func performStateSyncAPICall() -> Void {
DataSyncRequestManager.fetchStatesDataWithCompletionBlock {
success, response, error in
self.apiManager.didStatesApiComplete = true
}
}
DataSyncRequestManager Code:
static func fetchStatesDataWithCompletionBlock(block:#escaping requestCompletionBlock) {
if appDelegate.isNetworkAvailable {
Util.setAPIStatus(key: kStateApiStatus, with: kInProgress)
DataSyncingInterface().performStateSyncingWith(request:DataSyncRequest().createStateSyncingRequest() , withCompletionBlock: block)
} else {
//TODO: show network failure error
}
}
DataSyncingInterface Code:
func performStateSyncingWith(request:Request, withCompletionBlock block:#escaping requestCompletionBlock)
{
self.interfaceBlock = block
let apiurl = NetworkHttpClient.getBaseUrl() + request.urlPath!
Alamofire.request(apiurl, parameters: request.getParams(), encoding: URLEncoding.default).responseJSON { response in
guard response.result.isSuccess else {
block(false, "error", nil )
return
}
guard let responseValue = response.result.value else {
block (false, "error", nil)
return
}
block(true, responseValue, nil)
}
}
I know many similar questions have been already posted on Stackoverflow and mostly it is suggested to use GCD or Operation Queue, though trying DispatchQueues didn't work for me.
Am I doing something wrong?
How can I not block UI and perform the api calls simultaneously?
You can do this to run on a background thread:
DispatchQueue.global(qos: .background).async {
// Do any processing you want.
DispatchQueue.main.async {
// Go back to the main thread to update the UI.
}
}
DispatchQueue manages the execution of work items. Each work item submitted to a queue is processed on a pool of threads managed by the system.
I usually use NSOperationQueue with Alamofire, but the concepts are similar. When you set up an async queue, you allow work to be performed independently of the main (UI) thread, so that your app doesn't freeze (refuse user input). The work will still take however long it takes, but your program doesn't block while waiting to finish.
You really have only put one item into the queue.
You are adding to the queue only once, so all those "perform" calls wait for the previous one to finish. If it is safe to run them concurrently, you need to add each of them to the queue separately. There's more than one way to do this, but the bottom line is each time you call .async {} you are adding one item to the queue.
dataQueue.async {
DataSyncController().performStateSyncAPICall()
}
dataQueue.async {
DataSyncController(). performRegionSyncAPICall l()
}

Completion handler? async_dispatch?

These are the methods and processes which need to happen:
Download images from Parse
Download associated data (which matches images)
Plot this data on a map
Here is my code from view did load:
override func viewDidLoad() {
imageDownload { () -> () in
print("5-----inside closure")
self.queryParseandSave(callback: self.plotImages)
}
}
Image download function:
func imageDownload(completed: #escaping FinishedDownload){
print("1-----Started Image download")
// Query for places
let query = PFQuery(className:"ViewFinderObjects")
query.whereKey("ImageVerified", equalTo: true)
query.whereKey("coordinates", nearGeoPoint:myGeoPoint)
query.limit = 10
query.findObjectsInBackground { (objects, error) -> Void in
if (error == nil) {
for object in objects! {
print("2-----inside object for block")
let imageFromParse = object["image"] as! PFFile
imageFromParse.getDataInBackground(block: {(imageData, error) -> Void in
print("Searching for Image")
if error == nil {
let obsImage:UIImage = UIImage(data: imageData!)!
self.imageToShow = obsImage
self.closestImage.append(self.imageToShow!)
print("There are \(self.closestImage.count) images in the image array")
}
})
print("3-----Completed object loop")
}
}
print("4-----Calling completed statement")
completed()
}
}
Which then calls another function queryParseandSave(callback: self.plotImages)
with the self.plotImages plotting the images on a map.
I have 1 huge issue:
self.plotImahes is always called before the images have finished downloading
I have researched async_dispatch but have no idea if this is the right thing to do.
I'm not familiar with the implementations of the query.findObjectsInBackground and imageFromParse.getDataInBackground methods, but their naming implies that they both happen asynchronously. Also judging from what you're provided above, the former retrieves the object data, while the latter does the actual image data download. If that is indeed the case, then it looks like you're calling your completion handler inside the body of the first asynchronous method instead of waiting for the second method (what appears to be the actual image download).
A couple of ideas for how to resolve this:
You could move your completion handler into the imageFromParse.getDataInBackground block, but this would only make sense if you're comfortable calling the completion block multiple times, after each image finishes downloading.
You could create your own dispatch or operation queue and wait until all tasks complete, then call the completion handler.
You could set up an observer or notification pattern that will call your completion handler at the appropriate time.
There are many different ways to address the issue, but the key thing to remember is that it sounds like you want to call your completion handler after all of the asynchronous operations have completed. Right now you're calling it after you've retrieved the objects, but all of your images are still downloading in the background when your completion handler is called.

Firebase Storage download task is not completing after the app has spent some time in the background

I am downloading an image from Firebase storage as follows:
let storage = FIRStorage.storage()
// Create a storage reference from our storage service
let storageRef = storage.reference(forURL: "MY_STORAGE_URL")
let imageRef = storageRef.child("Path_to_image")
// Download image in memory
let downloadTask = imageRef.data(withMaxSize: 1 * 1024 * 1024) {
(data, error) -> Void in
if (error != nil) {
//Handle the error
} else {
guard let imageData = data else {
print("Unable to unwrap image data.")
return
}
let downloadedImage = UIImage(data: imageData)
//Do some stuff with the image
}
}
I am also monitoring what happens with the download using the following observers:
// Observe changes in status
downloadTask.observe(.resume) { (snapshot) -> Void in
// Download resumed, also fires when the download starts
}
downloadTask.observe(.pause) { (snapshot) -> Void in
// Download paused
}
downloadTask.observe(.progress) { (snapshot) -> Void in
// Download reported progress
}
downloadTask.observe(.success) { (snapshot) -> Void in
// Download completed successfully
}
downloadTask.observe(.failure) { (snapshot) -> Void in
//Download failed
}
This all works just fine when the app is first started. However, I am getting problems if the app enters the background and I play around with some other applications (Facebook, Twitter, etc.), then bring the app back to the foreground. I also have problems if I leave the app open and running in the foreground for greater than or equal to 1 hour.
The problem is that the completion handler in let downloadTask = imageRef.data(withMaxSize: blah blah blah (in the first block of code above) is never called. If the completion handler is never called, I can never unwrap the data and attempt to use the image in my application.
Also, in the downloadTask observers, the only completion handlers that get fired are .resume and .progress. The .success or .failure events are never triggered. This seems to be a Firebase Storage bug to me, but I am not sure. Has anyone else encountered a similar issue? I don't understand why the code would work just fine from a fresh launch, but then after some time in the foreground or after some time in the background the image download stops working. Thanks in advance for any input you may have.
This is currently the expected behavior, unfortunately. Firebase Storage (at present) is foreground only: if the app is backgrounded, we haven't persisted the upload URL, and can't upload in the background nor restart it after it gets out of the background, so it probably is killed by the OS and the item isn't uploaded.
It's The Next Big Thing™ we'd like to tackle (our Android SDK makes it possible, though not easy), but unfortunately for now we haven't made more progress on this.
As a bit of a side note, your observers won't exist after the activity change--downloadTask is gone once the app is backgrounded, so when it comes back into the foreground, we basically need a method that retrieves all tasks that are currently backgrounded, and allows you to hook observers back up. Something like:
FIRStorage.storage().backgroundedTasks { (tasks) -> Void in
// tasks is an array of upload and download tasks
// not sure if it needs to be async
}

How to find and cancel a task in NSURLSession?

I'm using an NSURLSession object to load images in my application. That could be loading several images simultaneously.
In some moments I need to cancel the loading of one specific image and continue loading others.
Could you suggest the correct way to do that?
To get tasks list you can use NSURLSession's method
- (void)getTasksWithCompletionHandler:(void (^)(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks))completionHandler;
Asynchronously calls a completion callback with all outstanding data,
upload, and download tasks in a session.
Then check task.originalRequest.URL for returned tasks to find the one you want to cancel.
Based on all the answers below, I'd go for something like this:
Swift 5
func cancelTaskWithUrl(_ url: URL) {
URLSession.shared.getAllTasks { tasks in
tasks
.filter { $0.state == .running }
.filter { $0.originalRequest?.url == url }.first?
.cancel()
}
}
You also probably want to account for your task completion handler, since canceling the task will result Error in that completion handler.
Hope below code help.
-(IBAction)cancelUpload:(id)sender {
if (_uploadTask.state == NSURLSessionTaskStateRunning) {
[_uploadTask cancel];
}
}
Swift 3.0 version of #Avt's answer to get the task list. Use getTasksWithCompletionHandler.
func getTasksWithCompletionHandler(_ completionHandler: #escaping ([URLSessionDataTask],
[URLSessionUploadTask],
[URLSessionDownloadTask]) -> Void) {
}
The returned arrays contain any tasks that you have created within the
session, not including any tasks that have been invalidated after
completing, failing, or being cancelled.
I suggest two methods:
Put the list of NSURLSessionTask in an array. In case you don't know exactly how many images you would get. Though you have to know the index of session in order to cancel it.
If you get a limited number of images. Just use a set of NSURLSessionTask as global variables so you can access to cancel it anywhere in your class.
I think you should do this...
First, keep track of your requests per xib
var download_requests = [NSURLSession]()
Then, whenever you make a request, append your request to your array like so,
let s = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
if let url = NSURL(string: "http://my.url.request.com")
{
download_requests.append(s)
s.dataTaskWithURL(url)
{ (data, resp, error) -> Void in
// ....
}
}
Then whenever you want to cancel any outstanding requests, (let's say on viewDidDisappear), do
override func viewDidDisappear(animated: Bool)
{
super.viewDidDisappear(animated)
//stop all download requests
for request in download_requests
{
request.invalidateAndCancel()
}
}

Resources