swift: nsurlsession downloading files - ios

There is a tableViewController with 5 cells. When I click on a cell, the download starts, if the file is not found.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",indexPath.row))
if fileManager.fileExists(atPath: destinationURLForFile.path){ self)
}else{
var downloadTask: URLSessionDownloadTask!
index = indexPath.row
let url = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
downloadTask = backgroundSession.downloadTask(with: url)
downloadTask.resume()
}}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL){
let path = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
let documentDirectoryPath:String = path[0]
let fileManager = FileManager()
let destinationURLForFile = URL(fileURLWithPath: documentDirectoryPath.appendingFormat("/file%d.pdf",index))
do {
try fileManager.moveItem(at: location, to: destinationURLForFile)
}catch{
print("An error occurred while moving file to destination url")
}
}
The problem is that downloading one of the files stops if I download 2 files at a time. How to fix it?

A couple of thoughts:
A single, numeric index property is obviously insufficient to keep track of the multiple downloads that might be in progress. You need some structure to keep track of the correlation between downloads and their eventual file names in the Documents folder. It might be:
struct Download {
enum Status {
case notStarted
case started
case finished
case failed(Error?)
}
/// URL of resource on web server
let remoteURL: URL
/// URL of file in Documents folder
let localURL: URL
/// The status of the download
var status: Status
}
Now that you have a type to keep track of the state of an individual download, create an array of those Download objects:
var downloads = [Download]()
You might populate that in viewDidLoad, or something like that:
override func viewDidLoad() {
super.viewDidLoad()
// create the `Download` objects, e.g. I'll create one here
let remoteURL = URL(string: "http://publications.gbdirect.co.uk/c_book/thecbook.pdf")!
let fileURL = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("file0.pdf")
let status: Download.Status
if try! fileURL.checkResourceIsReachable() {
status = .finished
} else {
status = .notStarted
}
downloads.append(Download(remoteURL: remoteURL, localURL: fileURL, status: status))
// since you're dealing with background session (e.g. tasks may have been previously
// scheduled), let's iterate through any pending tasks, updating status accordingly
backgroundSession.getAllTasks { tasks in
for task in tasks {
guard let index = self.downloads.index(where: { $0.remoteURL == task.originalRequest?.url }) else {
print("cannot find download for \(task.originalRequest?.url)")
return
}
self.downloads[index].status = .started
}
}
}
When the download is done, you can now just look up that download in our array of downloads in order to determine the file URL:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
guard let index = downloads.index(where: { $0.remoteURL == downloadTask.originalRequest?.url }) else {
print("cannot find download for \(downloadTask.originalRequest?.url)")
return
}
do {
try FileManager.default.moveItem(at: location, to: downloads[index].localURL)
downloads[index].status = .finished
} catch {
print("An error occurred while moving file to destination url: \(error.localizedDescription)")
downloads[index].status = .failed(error)
}
}
It's worth noting that the logic that says "if the file doesn't exist, then start download" is, most likely, insufficient. Sure, if the file exists, then the download is done. But what if a download has been started already, but hasn't yet finished? You probably do not want to start a new download if a previously initiated download simply hasn't yet finished.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row > 0 {
let index = indexPath.row - 1 // for some reason you're looking at indexes greater than zero, so let's adjust our index for a zero-based index within our array
switch downloads[index].status {
case .notStarted:
let downloadTask = backgroundSession.downloadTask(with: downloads[index].remoteURL)
downloads[index].status = .started
downloadTask.resume()
default:
break
}
}
}
Now, I don't want to get too lost in the details of these code snippets above, but rather I want to make sure you grok the basic concept, namely that you can't have a single numeric index, but rather you need some collection (an array or dictionary) to keep track of all of the various downloads that may be in progress at any given time.

You cannot download two files at a time if you are using single variables (index and downloadTask). Whenever the user selects the second cell, a new value for index is set, so using that value in urlSession:downloadTask:didFinishDownloadingTo: is a mistake when it is being called by the first task.
You need to keep that values in a collection, for example an array of tuples, keeping the index, the task and any other info about the file, for example the file path.

Related

Second cached video being used for all cells?

I attempted the following caching mechanism to cache videos which are added in cells in my app:
import Foundation
public enum Result<T> {
case success(T)
case failure(NSError)
}
class CacheManager {
static let shared = CacheManager()
private let fileManager = FileManager.default
private lazy var mainDirectoryUrl: URL = {
let documentsUrl = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first!
return documentsUrl
}()
func getFileWith(stringUrl: String, completionHandler: #escaping (Result<URL>) -> Void ) {
let file = directoryFor(stringUrl: stringUrl)
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
DispatchQueue.global().async {
if let videoData = NSData(contentsOf: URL(string: stringUrl)!) {
videoData.write(to: file, atomically: true)
DispatchQueue.main.async {
completionHandler(Result.success(file))
}
} else {
DispatchQueue.main.async {
let error = NSError(domain: "SomeErrorDomain", code: -2001 /* some error code */, userInfo: ["description": "Can't download video"])
completionHandler(Result.failure(error))
}
}
}
}
private func directoryFor(stringUrl: String) -> URL {
let fileURL = URL(string: stringUrl)!.lastPathComponent
let file = self.mainDirectoryUrl.appendingPathComponent(fileURL)
return file
}
}
Used like this for each cell if at that index it is a video:
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
switch result {
case .success(let url):
let player = AVPlayer(url: url)
let playerLayer = AVPlayerLayer(player: player)
playerLayer.frame = CGRect(x: -8, y: 0, width: 138, height: 217)
cell.imageOrVideoView.layer.addSublayer(playerLayer)//seems to use the the first video for all videos...
cell.profImage.sd_setImage(with: URL(string: "\(self.postArray[indexPath.item].user.profileImageUrlString!)"), placeholderImage: UIImage(named: "media"))
break;
// do some magic with path to saved video
case .failure(let error):
print(error, " failure in the Cache of video")
break;
// handle errror
}
}
The problem with this is that now the second video URL is used as the video (which I don't play) for every single video cell.
Do I need to reload cache? This seems wrong...
I have found that this seems to be the problematic code:
//return file path if already exists in cache directory
guard !fileManager.fileExists(atPath: file.path) else {
completionHandler(Result.success(file))
return
}
When commenting out the guard each cell does get the correct video. The problem is that it does not cache it. How can I fix this?
Edit:
I found something weird which may be a symptom of the problem. if at the index path the cell should be a video, then I print video else it must be a image print image. When looking in the console however, I see that image gets printed 5 times and video gets printed 2, even though there are 11 cells in the collection view. Also I can confirm that regardless of which it is I print "in here" which gets printed 7 times. Again, this all when (when you scroll), there are 11 cells all of which should be different but of course all the video ones have the second videos first frame.
What happens is when the first cell is dequeued you get it with it's video layer added successfully then when you scroll this callback
CacheManager.shared.getFileWith(stringUrl: videoURL) { result in
is very slow so the previous cell content appears at that time making the fact that it's the second cell in all cells and that because of cell dequeuing
You have to clear the previous layers before the above line which is caused by
cell.imageOrVideoView.layer.addSublayer(playerLayer)
Edit: after the dequeue line do
cell.imageOrVideoView.layer.sublayers?.forEach {
if $0 is AVPlayerLayer {
$0.removeFromSuperlayer()
}
}
The problem seemed to be tied to using the cache manager for each cell. and the fact that I download a video for each cell. Instead used the thumbnail for each image which I then had to save.

How to disable TableView cell images download when they get into view

I have a TableView with ImageViews inside each cell. I want the images to get loaded once and remain like that but it seems that the images get loaded (downloaded, I'm getting them from an external API) as they get into visible area for user. It seems like a lazy load or something like that and I would like to disable it because if I scroll down then come back up most of the images get misplaced.
TableViewController.swift
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
BusinessLayer.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
return dataLayerRiot.getChampionThumbnailImage(championId: championId)
}
DataLayerRiot.swift
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
Anyone know how to disable them loading as they get into visible area for the user and just have them "stored"?
EDIT
I am dequeuing the cell using the default way
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Match", for: indexPath) as? TableViewCell
...
}
Is there a better way of doing it?
EDIT 2
I also need to specify that I am unable to install libraries because this is a university project and I am able to work only on university's MACs (because I don't own one) therefore I am unable to install packages without administrator privileges.
You should save a task at memory like:
let task = = session.dataTask() {}
And after you can cancel it anywhere by:
task.cancel()
Alternatively, if the object session is a URLSession instance, you can cancel it by:
session.invalidateAndCancel()
Try SDWebImage for lazy loading the images in the UITableViewCell or UICollectionViewCell. Install it through cocoapods into your project.
It is an asynchronous memory + disk image caching with automatic cache expiration handling.
https://github.com/SDWebImage/SDWebImage
Code:
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
cell?.mainChampImageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))
It sounds like you could benefit from doing some image caching. There are multiple ways to go about doing so, but from your example, it doesn't look like you need to go through the trouble of adding an entire library to do so. You can do it in a simple manner using NSCache.
I created a class called ImageCache, and in this case it is a singleton, so that the cache is accessible throughout the entire application.
import UIKit
class ImageCache: NSObject {
static let sharedImageCache = ImageCache()
// Initialize cache, specifying that your key type is AnyObject
// and your value type is AnyObject. This is because NSCache requires
// class types, not value types so we can't use <URL, UIImage>
let imageCache = NSCache<AnyObject, AnyObject>()
// Here we store the image, with the url as the key
func add(image: UIImage, for url: URL) {
// we cast url as AnyObject because URL is not a class type, it's a value type
imageCache.setObject(image, forKey: url as AnyObject)
}
// This allows us to access the image from cache with the URL as the key
// (e.g. cache[URL])
func fetchImage(for url: URL) -> UIImage? {
var image: UIImage?
// Casting url for the same reason as before, but we also want the result
// as an image, so we cast that as well
image = imageCache.object(forKey: url as AnyObject) as? UIImage
return image
}
}
So now we have some relatively simple caching in place. Now for how to use it:
func getChampionThumbnailImage (championId: Int) -> UIImage {
var image: UIImage!
let urlString = ApiHelper.getChampionThumbnailImageApiLink(championId: championId)
let url = URL(string: urlString)
// Before, downloading the image, we check the cache to see if it exists and is stored.
// If so, we can grab that image from the cache and avoid downloading it again.
if let cachedImage = ImageCache.sharedImageCache.fetchImage(for: url) {
image = cachedImage
return image
}
let session = URLSession.shared
let semaphore = DispatchSemaphore(value: 0)
session.dataTask(with: url!) {(data, response, error) in
if error != nil {
print("ERROR")
semaphore.signal()
}
else {
image = UIImage(data: data!)!
// Once the image is successfully downloaded the first time, add it to
// the cache for later retrieval
ImageCache.sharedImageCache.add(image: image, for: url!)
semaphore.signal()
}
}.resume()
semaphore.wait()
session.finishTasksAndInvalidate()
return image
}
The reason the images are re-downloading is because a table view doesn't have unlimited cells. What happens is, as you scroll down, the cells that go off the screen are then recycled and re-used, so when you scroll back up, the images have to be grabbed again because they've been emptied out.
You can avoid downloading the images again by implementing caching.
Another way you can avoid having incorrect images is setting your image view to nil before you re-download the image. For example:
cell?.mainChampImageView = nil
cell?.mainChampImageView.image = businessLayer.getChampionThumbnailImage(championId: mainChampion.key)
All of the above, along with making sure that you are dequeuing cells properly should address your issue.

Adding reference images to ARKit from the app

I was learning to use ARKIT and I was wondering if there is a way to add reference images(images to be recognised) from within the app(based on the user's choice). As per the documentation, this can be done by adding the reference images to the Assets(during the development phase) which limits the usability of the app. I was wondering if there is a way where we can download/add these images based on the user's choice and use these images as reference image(within the app).
If you have a look at the documentation for: ARReferenceImage you will note that there are two methods of generating ARReferenceImages manually:
init(CGImage, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
init(CVPixelBuffer, orientation: CGImagePropertyOrientation, physicalWidth: CGFloat)
The one that you will need if you are downloading from a Server is the first one, which requires the use of a CGImage.
So any image(s) which you download will need to be converted using this method.
To download images from a Server first you will need to use a URLSession to download these to a location on your device e.g. the Documents Directory.
A simple example of this would look like so:
/// Downloads An Image From A Remote URL
func downloadImageTask(){
//1. Get The URL Of The Image
guard let url = URL(string: "http://www.blackmirrorz.tech/images/BlackMirrorz/blackMirrorzLogo.png") else { return }
//2. Create The Download Session
let downloadSession = URLSession(configuration: URLSession.shared.configuration, delegate: self, delegateQueue: nil)
//3. Create The Download Task & Run It
let downloadTask = downloadSession.downloadTask(with: url)
downloadTask.resume()
}
}
Having created the URLSession you would then need to register for the URLSessionDownloadDelegate and the following method:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
Whereby the location parameter refers to the:
A file URL for the temporary file. Because the file is temporary, you
must either open the file for reading or move it to a permanent
location in your app’s sandbox container directory before returning
from this delegate method.
As such your callback might look like so:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
//1. Create The Filename
let fileURL = getDocumentsDirectory().appendingPathComponent("image.png")
//2. Copy It To The Documents Directory
do {
try FileManager.default.copyItem(at: location, to: fileURL)
print("Successfuly Saved File \(fileURL)")
} catch {
print("Error Saving: \(error)")
}
}
Whereby I use the following function to get the users Documents Directory:
/// Returns The Documents Directory
///
/// - Returns: URL
func getDocumentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return documentsDirectory
}
Now we have downloaded the images we would then create a function to retrieve these and return a Set of ARReferenceImage which is needed by ARWorldTrackingConfiguration.
/// Creates A Set Of ARReferenceImages From All PNG Content In The Documents Directory
///
/// - Returns: Set<ARReferenceImage>
func loadedImagesFromDirectoryContents() -> Set<ARReferenceImage>?{
var index = 0
var customReferenceSet = Set<ARReferenceImage>()
let documentsDirectory = getDocumentsDirectory()
do {
let directoryContents = try FileManager.default.contentsOfDirectory(at: documentsDirectory, includingPropertiesForKeys: nil, options: [])
let filteredContents = directoryContents.filter{ $0.pathExtension == "png" }
filteredContents.forEach { (url) in
do{
//1. Create A Data Object From Our URL
let imageData = try Data(contentsOf: url)
guard let image = UIImage(data: imageData) else { return }
//2. Convert The UIImage To A CGImage
guard let cgImage = image.cgImage else { return }
//3. Get The Width Of The Image
let imageWidth = CGFloat(cgImage.width)
//4. Create A Custom AR Reference Image With A Unique Name
let customARReferenceImage = ARReferenceImage(cgImage, orientation: CGImagePropertyOrientation.up, physicalWidth: imageWidth)
customARReferenceImage.name = "MyCustomARImage\(index)"
//4. Insert The Reference Image Into Our Set
customReferenceSet.insert(customARReferenceImage)
print("ARReference Image == \(customARReferenceImage)")
index += 1
}catch{
print("Error Generating Images == \(error)")
}
}
} catch {
print("Error Reading Directory Contents == \(error)")
}
//5. Return The Set
return customReferenceSet
}
So to put this last function into place you would do the following:
let detectionImages = loadedImagesFromDirectoryContents()
configuration.detectionImages = detectionImages
augmentedRealitySession.run(configuration, options: [.resetTracking, .removeExistingAnchors])
Hope it helps...

How do you close open files using Swift?

I am downloading ~1300 images. Those are small images total size is around ~500KB. However, after downloading and putting them into userDefault, I get error as below:
libsystem_network.dylib: nw_route_get_ifindex :: socket(PF_ROUTE, SOCK_RAW, PF_ROUTE) failed: [24] Too many open files
Assumingely, downloaded png images are not being closed.
I already extended cache size via below:
// Configuring max network request cache size
let memoryCapacity = 30 * 1024 * 1024 // 30MB
let diskCapacity = 30 * 1024 * 1024 // 30MB
let urlCache = URLCache(memoryCapacity: memoryCapacity, diskCapacity: diskCapacity, diskPath: "myDiscPath")
URLCache.shared = urlCache
And this is the approach I got to store images:
func storeImages (){
for i in stride(from: 0, to: Cur.count, by: 1) {
// Saving into userDefault
saveIconsToDefault(row: i)
}
}
I get the error after all of them being added into userDefault. So, I know they are there.
EDIT:
FUNCTIONS:
func getImageFromWeb(_ urlString: String, closure: #escaping (UIImage?) -> ()) {
guard let url = URL(string: urlString) else {
return closure(nil)
}
let task = URLSession(configuration: .default).dataTask(with: url) { (data, response, error) in
guard error == nil else {
print("error: \(String(describing: error))")
return closure(nil)
}
guard response != nil else {
print("no response")
return closure(nil)
}
guard data != nil else {
print("no data")
return closure(nil)
}
DispatchQueue.main.async {
closure(UIImage(data: data!))
}
}; task.resume()
}
func getIcon (id: String, completion: #escaping (UIImage) -> Void) {
var icon = UIImage()
let imageUrl = "https://files/static/img/\(id).png"
getImageFromWeb(imageUrl) { (image) in
if verifyUrl(urlString: imageUrl) == true {
if let image = image {
icon = image
completion(icon)
}
} else {
if let image = UIImage(named: "no_image_icon") {
icon = image
completion(icon)
}
}
}
}
USAGE:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CurrencyCell", for: indexPath) as? CurrencyCell else { return UITableViewCell() }
if currencies.count > 0 {
let noVal = currencies[indexPath.row].rank ?? "N/A"
let nameVal = currencies[indexPath.row].name ?? "N/A"
let priceVal = currencies[indexPath.row].price_usd ?? "N/A"
getIcon(id: currencies[indexPath.row].id!, completion: { (retImg) in
cell.configureCell(no: noVal, name: nameVal, price: priceVal, img: retImg)
})
}
return cell
}
The URLSession(configuration: .default) syntax is creating a new URLSession for each request. Create a single URLSession (saving it in some property) and then reuse it for all of the requests. Or, if you're really not doing any custom configuration of the URLSession, just use URLSession.shared:
let task = URLSession.shared.dataTask(with: url) { data, response, error in
...
}
task.resume()
You mention that you're saving 1300 images in UserDefaults. That's not the right place to store that type of data nor for that quantity of files. I'd suggest you use the "Caches" folder as outlined in the File System Programming Guide: The Library Directory Stores App-Specific Files.
let cacheURL = try! FileManager.default
.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent("images")
// create your subdirectory before you try to save files into it
try? FileManager.default.createDirectory(at: cacheURL, withIntermediateDirectories: true)
Do not be tempted to store them in the "Documents" folder, either. For more information, see iOS Storage Best Practices.

Dispatch After Download is Finished in Swift?

I am downloading files in swift, and the download session is triggered by a button on each table view cell. However, I do not want the next download (if someone presses the download button on another cell) to happen until after the previous one is finished.
Is there a way that I can use something like dispatch_after to accomplish this?
Here is my code where the downloading occur, if it helps at all.
//FUNCTION TO DOWNLOAD THE PDF
//PASS THE ONLINE PDF URL AS NSURL
//ASYNC REQUEST
let defaultSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
var dataTask: NSURLSessionDataTask?
var temp_name = String()
var temp_index = Int()
var temp_indexPath = NSIndexPath()
lazy var downloadsSession: NSURLSession = {
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
return session
}()
func getUrl(name: String) -> NSURL?{
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as NSURL!
return documentsUrl.URLByAppendingPathComponent(name)
}
func getIndex() -> Int?{
return temp_index
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
if let originalURL = downloadTask.originalRequest?.URL?.absoluteString,
destinationURL = getUrl(temp_name){
let fileManager = NSFileManager.defaultManager()
do {
try fileManager.removeItemAtURL(destinationURL)
} catch {
// Non-fatal: file probably doesn't exist
}
do {
try fileManager.copyItemAtURL(location, toURL: destinationURL)
} catch let error as NSError {
print("Could not copy file to disk: \(error.localizedDescription)")
}
}
if let url = downloadTask.originalRequest?.URL?.absoluteString {
activeDownloads[url] = nil
if let trackIndex = getIndex() {
dispatch_async(dispatch_get_main_queue(), {
defaults.setBool(false, forKey: self.temp_name + "_downloading")
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: trackIndex, inSection: 0)], withRowAnimation: .None)
})
}
}
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if let downloadUrl = downloadTask.originalRequest?.URL?.absoluteString,
download = activeDownloads[downloadUrl] {
download.progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite)
if let trackIndex = getIndex(), let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: trackIndex, inSection: 0)) as? MainTableViewCell {
dispatch_async(dispatch_get_main_queue(), {
cell.progress.progress = download.progress
if(download.progress < 1.0){
cell.progress.hidden = false
}
else{
cell.progress.hidden = true
}
})
}
}
}
// Action triggered by UIButton (in this case the download button)
//Access tag, which is the IndexPath.row, using sender.tag
#IBAction func downloadFile(sender: UIButton){
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as! MainTableViewCell
cell.downloadButton.hidden = true
cell.progress.progress = 0
cell.progress.hidden = false
let isAvailable = true
let key = names[sender.tag] + "_offline"
defaults.setValue(isAvailable, forKey: key)
let name = (names[sender.tag])
let fileName = name + ".pdf"
let attachment = attachments[sender.tag]
temp_name = fileName
temp_index = sender.tag
temp_indexPath = indexPath
let destinationURL = getUrl(temp_name)!
defaults.setValue(destinationURL.path!, forKey: name + "_path")
defaults.synchronize()
defaults.setBool(true, forKey: name + "_downloading")
let urlString = attachment
let url = NSURL(string: urlString)
let download = PDFDownload(url: urlString)
download.downloadTask = downloadsSession.downloadTaskWithURL(url!)
download.downloadTask!.resume()
download.isDownloading = true
activeDownloads[download.url] = download
}
There is a boolean that stores whether or not a download session is occurring, so maybe there is a way that I can use that? Wait until the boolean is false to execute my code?
#Deepak kumar answer is correct,But adding dependency for each operation is not good idea.
you can do it in a simpler way. only 3 steps required.
create NSOperationQueue object.
then set the property maxConcurrentOpeations to 1.
then add operations to queue , it will perform the operations one by one.
You can use NSOperationQueue to accomplish this. Create one operationqueue and one NSOperation object to store previous operation which was added to operation queue before the current operation. on every click on tableviewcell's button create new NSOperation instance and before adding it to operationqueue do the followings.
1- check if tempoperation is nil. then assign current operation to it and then add to operation queue.
2. else add dependency on tempoperation first then assign current operation to it and then add to operation queue.
This way each task will start after the completion of previous task. Hope this will help you. :)

Resources