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. :)
Related
I'm making an application with one Core Data entity, Park, which is decoded from the data returned from an API request and stores the image url and local file/download location (if it has been downloaded) for each image as attributes. I created a computed property that returns a dictionary of ImageInfoObjects (which is a struct that basically just bundles the information together) based on the stored attributes. The first time I run the app, everything works fine but when I close the app and run it again it gives me the error "the file couldn't be opened because there is no such file". So I know there must be an issue with the way I'm storing the file paths in Core Data, or the way I'm reading them back to display the images. Any help would be appreciated. Code snippets below.
The method which catches the error:
func displayPhoto(_ object: ImageInfoObject, imageView: UIImageView) {
guard let location = object.downloadLocation else { return }
do {
let imageData = try Data(contentsOf: location)
let image = UIImage(data: imageData)
DispatchQueue.main.async {
imageView.image = image
}
} catch (let error) {
print(error)
}
}
The URLSessionDownloadDelegate method which is called once each image is finished downloading:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
let sourceURL = downloadTask.originalRequest?.url,
let download = self.photoDownloads[sourceURL] else {
fatalError()
}
let lastPathComponent = sourceURL.lastPathComponent
let destinationURL = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.copyItem(at: location, to: destinationURL)
let index = download.imageInfoObject.index
let newImageInfoObject = ImageInfoObject(url: sourceURL, index: index, downloadLocation: destinationURL)
self.park?.photoInfoObjects[index] = newImageInfoObject
switch newImageInfoObject.index {
case 1:
displayPhoto(newImageInfoObject, imageView: self.photo1View)
case 2:
displayPhoto(newImageInfoObject, imageView: self.photo2View)
default:
break
}
try context?.save()
} catch {
print(error)
}
}
The computed property that stores and retrieves the paths from the CoreData entity. The NSManaged attributes are self.photo1_url, self.photo1_location, self.photo2_url, and self.photo2_location.
public var photoInfoObjects: Dictionary<Int, ImageInfoObject> {
get {
var dictionary: Dictionary<Int, ImageInfoObject> = [:]
var object: ImageInfoObject
if let photo1_url = self.photo1_url,
let url = URL(string: photo1_url) {
if let photo1_location = self.photo1_location {
let location = URL(fileURLWithPath: photo1_location)
object = ImageInfoObject(url: url, index: 1, downloadLocation: location)
object.isDownloaded = true
} else {
object = ImageInfoObject(url: url, index: 1)
}
dictionary[1] = object
}
var object2: ImageInfoObject
if let photo2_url = self.photo2_url,
let url = URL(string: photo2_url) {
if let photo2_location = self.photo2_location {
let location = URL(fileURLWithPath: photo2_location)
object2 = ImageInfoObject(url: url, index: 2, downloadLocation: location)
object2.isDownloaded = true
} else {
object2 = ImageInfoObject(url: url, index: 2)
}
dictionary[2] = object2
}
return dictionary
}
set {
self.photo1_url = newValue[1]?.url.absoluteString
self.photo1_location = newValue[1]?.downloadLocation?.path
self.photo2_url = newValue[2]?.url.absoluteString
self.photo2_location = newValue[2]?.downloadLocation?.path
}
}
This question already has answers here:
Observe progress of Data download in Swift?
(1 answer)
get progress from dataTaskWithURL in swift
(6 answers)
Closed 4 years ago.
I am implementing a ViewController to display a PDF previously downloaded from my server and stored locally on the device, it works correctly, but to download the PDF takes too much time and I would like to implement a progress-bar.
My code is the following, where I have tried to implement the #IBOutlet weak var downloadBar: UIProgressView! .
As I get the time it takes for the download, so I eat my code reaches 100% and the download does not end yet.
class PDFViewController: UIViewController {
#IBOutlet weak var pdfView: PDFView!
#IBOutlet weak var downloadBar: UIProgressView!
//*******
var downloader = Timer()
var minValue = 0
var maxValue = 100
//********
var namePDF:String?
override func viewDidLoad() {
super.viewDidLoad()
downloadBar.setProgress(0, animated: false)
if let pdfUrl = URL(string: "https://miserver.com/\(namePDF!).pdf") {
print(pdfUrl)
// then lets create your document folder url
let documentsDirectoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// lets create your destination file url
let destinationUrl = documentsDirectoryURL.appendingPathComponent(pdfUrl.lastPathComponent)
print(destinationUrl)
// to check if it exists before downloading it
if FileManager.default.fileExists(atPath: destinationUrl.path) {
print("The file already exists at path")
/************** show pdf ****************/
let pdfUrl = destinationUrl.path
let rutafile = URL(fileURLWithPath: pdfUrl)
print(pdfUrl)
if let document = PDFDocument(url: rutafile) {
pdfView.autoScales = true
pdfView.document = document
}
/************** end show pdf ****************/
// if the file doesn't exist
} else {
print("file doesn't exist")
downloader = Timer.scheduledTimer(timeInterval: 0.06, target: self, selector: (#selector(PDFViewController.updater)), userInfo: nil, repeats: true)
downloadBar.setProgress(0, animated: false)
// you can use NSURLSession.sharedSession to download the data asynchronously
URLSession.shared.downloadTask(with: pdfUrl, completionHandler: { (location, response, error) -> Void in
guard let location = location, error == nil else { return }
do {
// after downloading your file you need to move it to your destination url
try FileManager.default.moveItem(at: location, to: destinationUrl)
print("File moved to documents folder")
print("file has already been downloaded")
/************** show pdf ****************/
let pdfUrl = destinationUrl.path
let rutafile = URL(fileURLWithPath: pdfUrl)
print(pdfUrl)
if let document = PDFDocument(url: rutafile) {
self.pdfView.autoScales = true
self.pdfView.document = document
}
/************** show pdf ****************/
} catch let error as NSError {
print(error.localizedDescription)
}
}).resume()
}
}
}
#objc func updater() {
if minValue != maxValue {
minValue += 1
downloadBar.progress = Float(minValue) / Float(maxValue)
print(Float(minValue) / Float(maxValue))
} else {
minValue = 0
downloader.invalidate()
}
}
}
From already thank you very much
You can implement the URLSessionDownloadDelegate protocol. And then use the following method:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
self.downloadBar.setProgress(progress, animated: false)
}
}
This will only update progress bar when new bytes are written. And provide an accurate estimate of your download progress. Hope this helps :)
I know this type of question has been asked 1e7 times but I have come across a specific issue that I don't think has been covered/is blatantly obvious but I am too novice to fix it on my own.
I have the following code snippet within my cellForRowAt method in a TableViewController:
let currentDictionary = parser.parsedData[indexPath.row] as Dictionary<String,String>
let urlString = currentDictionary["media:content"]
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.thumbnailImageView.image = UIImage(data: data!)
}
}
}
Which executes fine, downloads the images and assigns them to the UIImageView of each tableViewCell.
There is a finite delay when scrolling the table as the images are downloaded 'on the fly' so to speak.
What I want to do is pre-download all these images and save them in a data structure so they are fetched from URL's less frequently.
I have tried the following implementation:
var thumbnail = UIImage()
for item in parser.parsedData {
let currentDictionary = item as Dictionary<String,String>
let title = currentDictionary["title"]
let link = currentDictionary["link"]
let urlString = currentDictionary["media:content"]
let url = NSURL(string: urlString!)
if urlString != nil {
let url = NSURL(string: urlString!)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url! as URL)
DispatchQueue.main.sync {
thumbnail = UIImage(data: data!)!
}
}
}
var newsArticle: News!
newsArticle = News(title: title!, link: link!, thumbnail: thumbnail)
news.append(newsArticle)
Where news is my data structure. This code also executes fine, however each thumbnail is a 0x0 sized image, size {0, 0} orientation 0 scale 1.000000, according to the console output.
Does anyone have any ideas how to download these images but not immediately assign them to a UIImageView, rather store them for later use?
The problem is that you create your newsArticle before the global dispatch queue even started to process your url. Therefore, thumbnail is still the empty UIImage() created in the very first line.
You'll have to create the thumbnail inside the inner dispatch closure, like:
for item in parser.parsedData {
guard let currentDictionary = item as? Dictionary<String,String> else { continue /* or some error handling */ }
guard let title = currentDictionary["title"] else { continue /* or some error handling */ }
guard let link = currentDictionary["link"] else { continue /* or some error handling */ }
guard let urlString = currentDictionary["media:content"] else { continue /* or some error handling */ }
guard let url = URL(string: urlString) else { continue /* or some error handling */ }
DispatchQueue.global().async {
if let data = try? Data(contentsOf: url) {
DispatchQueue.main.sync {
if let thumbnail = UIImage(data: data) {
let newsArticle = News(title: title, link: link, thumbnail: thumbnail)
news.append(newsArticle)
}
}
}
}
}
By the way, your very first code (cellForRow...) is also broken: You must not reference the cell inside the dispatch closure:
DispatchQueue.main.async {
// Never do this
cell.thumbnailImageView.image = UIImage(data: data!)
}
Instead, reference the IndexPath, retrieve the cell inside the clousure, and go on with that cell. But as you already mentioned, there are many many entries on stackoverflow regarding this issue.
I am creating a new Downloader object (simply to download PDF files) and I need to call a method in my ViewController which show the message that the file is successfully downloaded. I am using the answer by Ahmet Akkök on this question and when I try to use yourOwnObject.showDownloadCompleted() it can't find the method.
ViewController:
override func viewDidLoad(){
super.viewDidLoad();
let pdfURL = "exampleToPFD.com/mypdf.pdf";
let url = NSURL(string: pdfURL);
let d = Downloader(yourOwnObject: self);
d.download(url!);
}
func showDownloadComplete(){
print("done");
}
Downloader:
import Foundation
class Downloader : NSObject, NSURLSessionDownloadDelegate {
var url : NSURL?
// will be used to do whatever is needed once download is complete
var yourOwnObject : NSObject?
var downloaded = false;
var documentDestination = "";
init(yourOwnObject : NSObject){
self.yourOwnObject = yourOwnObject
}
// is called once the download is complete
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
//copy downloaded data to your documents directory with same names as source file
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first
let destinationUrl = documentsUrl!.URLByAppendingPathComponent(url!.lastPathComponent!)
let dataFromURL = NSData(contentsOfURL: location)
dataFromURL!.writeToURL(destinationUrl, atomically: true)
// now it is time to do what is needed to be done after the download
print("download done...");
// call to the parent method here
documentDestination = destinationUrl.absoluteString;
print("DestURL" + (destinationUrl.absoluteString));
}
// method to be called to download
func download(url: NSURL) {
self.url = url
// download identifier can be customized. I used the "ulr.absoluteString"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(url.absoluteString)
let session = NSURLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url);
task.resume();
}
}
You can use delegate to achieve that. You can try out following
Downloader.swift
import Foundation
protocol DownloadDelegate {
func downloadCompleted()
}
class Downloader : NSObject, NSURLSessionDownloadDelegate{
var url : NSURL?
var downloadDelegate : DownloadDelegate!
// will be used to do whatever is needed once download is complete
var downloaded = false;
var documentDestination = "";
//is called once the download is complete
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL){
//copy downloaded data to your documents directory with same names as source file
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first
let destinationUrl = documentsUrl!.URLByAppendingPathComponent(url!.lastPathComponent!)
let dataFromURL = NSData(contentsOfURL: location)
dataFromURL!.writeToURL(destinationUrl, atomically: true)
//now it is time to do what is needed to be done after the download
print("download done...");
downloadDelegate. downloadCompleted()
documentDestination = destinationUrl.absoluteString;
print("DestURL" + (destinationUrl.absoluteString));
}
//this is to track progress
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64){
print((String)(totalBytesWritten)+"/"+(String)(totalBytesExpectedToWrite));
}
// if there is an error during download this will be called
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?){
if(error != nil){
//handle the error
print("Download completed with error: \(error!.localizedDescription)");
}
}
//method to be called to download
func download(url: NSURL){
self.url = url
//download identifier can be customized. I used the "ulr.absoluteString"
let sessionConfig = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier(url.absoluteString)
let session = NSURLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url);
task.resume();
}
}
ViewController :
class ViewController: UIViewController,DownloadDelegate{
override func viewDidLoad(){
super.viewDidLoad();
// Do any additional setup after loading the view, typically from a nib.
let pdfURL = "exampleToPFD.com/mypdf.pdf";
let url = NSURL(string: pdfURL);
let d = Downloader();
d.downloadDelegate = self
d.download(url!);
showToast("Download Started...");
}
func downloadCompleted() {
//download completed
}
}
My button code below download a file from a URL, I need to link it with a Progress View to show the Downloading Progress.
#IBAction func btnStream(sender: UIButton) {
// First you need to create your audio url
if let audioUrl = NSURL(string: "http://website.com/file.mp3") {
// then lets create your document folder url
let documentsUrl = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as! NSURL
// lets create your destination file url
let destinationUrl = documentsUrl.URLByAppendingPathComponent(audioUrl.lastPathComponent!)
println(destinationUrl)
// to check if it exists before downloading it
if NSFileManager().fileExistsAtPath(destinationUrl.path!) {
println("The file already exists at path")
// if the file doesn't exist
} else {
// just download the data from your url
if let myAudioDataFromUrl = NSData(contentsOfURL: audioUrl){
// after downloading your data you need to save it to your destination url
if myAudioDataFromUrl.writeToURL(destinationUrl, atomically: true) {
println("file saved")
} else {
println("error saving file")
}
}
}
}
}
How can i link my downloading progress with a Progress View in Swift?
Here is complete working example for you:
import UIKit
class ViewController: UIViewController, NSURLSessionDownloadDelegate {
#IBOutlet weak var progressBar: UIProgressView!
#IBOutlet weak var progressCount: UILabel!
var task : NSURLSessionTask!
var percentageWritten:Float = 0.0
var taskTotalBytesWritten = 0
var taskTotalBytesExpectedToWrite = 0
lazy var session : NSURLSession = {
let config = NSURLSessionConfiguration.ephemeralSessionConfiguration()
config.allowsCellularAccess = false
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: NSOperationQueue.mainQueue())
return session
}()
override func viewDidLoad() {
progressBar.setProgress(0.0, animated: true) //set progressBar to 0 at start
}
#IBAction func doElaborateHTTP (sender:AnyObject!) {
progressCount.text = "0%"
if self.task != nil {
return
}
let s = "http://www.qdtricks.com/wp-content/uploads/2015/02/hd-wallpapers-1080p-for-mobile.png"
let url = NSURL(string:s)!
let req = NSMutableURLRequest(URL:url)
let task = self.session.downloadTaskWithRequest(req)
self.task = task
task.resume()
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64) {
println("downloaded \(100*writ/exp)")
taskTotalBytesWritten = Int(writ)
taskTotalBytesExpectedToWrite = Int(exp)
percentageWritten = Float(taskTotalBytesWritten) / Float(taskTotalBytesExpectedToWrite)
progressBar.progress = percentageWritten
progressCount.text = String(format: "%.01f", percentageWritten*100) + "%"
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
// unused in this example
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
println("completed: error: \(error)")
}
// this is the only required NSURLSessionDownloadDelegate method
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
let documentsDirectoryURL = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as! NSURL
println("Finished downloading!")
println(documentsDirectoryURL)
var err:NSError?
// Here you can move your downloaded file
if NSFileManager().moveItemAtURL(location, toURL: documentsDirectoryURL.URLByAppendingPathComponent(downloadTask.response!.suggestedFilename!), error: &err) {
println("File saved")
} else {
if let err = err {
println("File not saved.\n\(err.description)")
}
}
}
}
You can use NSURLSessionDownloadDelegate to achieve this whose method will be called when user downloading data.
This will show you the process into progressCount label and the progressBar will show process as count will increment. you can modify this as per your need.
You can download this example from HERE.
Check this tutorial. It's in Objective-C, but it will be easy to convert to Swift.
The principle is to implement some NSURLConnectionDataDelegatefunctions on your VC :
connection:didReceiveResponse -> You can retrieve the size of the file that will be downloaded and estimate the download percentage with it.
connection:didReceiveData -> It's the refresh function, it will be called multiple times during the download. You can compare the size of your incomplete NSData object with the size of the file.
connectionDidFinishLoading -> This method is called at the end of the download process.
Hope it helps, and don't hesitate to comment if you have some troubles converting Obj-C to Swift.