I have a Calculus Video app I created based on tableviews and I am trying to add the functionality for offline saving of video files. I understand what I am trying to achieve but I am getting stumped by adding the progress bar to the specific cells:
Currently, the download is started by clicking on the accessory button. I have the following method
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
//Code to save Video to Documents directory goes here
let currentVideo = videos[indexPath.section][indexPath.row]
guard currentVideo.saved == false else {
print("Video is already saved")
return
}
guard let url = currentVideo.url else {
print("Video not found...url is invalid")
return
}
guard currentVideo.downloading == false else {
print("Video is already downloading")
return
}
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),
delegate: self,
delegateQueue: NSOperationQueue.mainQueue())
let downloadTask = session.downloadTaskWithURL(url)
downloadTask.resume()
}
Now, I am implementing the NSURLSessionDownloadDelegate methods, of which the relevant one is below
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
print(progress) //this works and shows progress
}
Now, all I want to do is to update the property
currentVideo.progress = progress
//where currentVideo is the video for the cell that was tapped
The problem is I do not know how to get the current video inside of this delegate method. I was trying to somehow figure out how to use the downloadTask.taskIdentifier or something like that but I am not able to figure it out. Can somebody please point me in the right direction?
You can try it following way.
Declare global variable under your class
var selectedIndex:NSIndexPath!
Then in accessoryButtonTappedForRowWithIndexPath method
selectedIndex = indexPath
Now, in delegate method downloadTask assign value
let currentVideo = videos[selectedIndex.section][selectedIndex.row]
currentVideo = // Your value
Related
Ok, I have looked at Displaying download progress in reusable cells but am running into an issue using URL Session to track download progress across tableview cells, after reloading the table:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(Double(totalBytesWritten) / Double(totalBytesExpectedToWrite))
itemsDownloadingNow[(downloadTask.originalRequest?.url)!] = progress
//update
for (i, prog) in itemsDownloadingNow
{
if(i.lastPathComponent == episode?.enclosure?.url.lastPathComponent)
{
//is downloading
circleProgress?.percentage = prog
}
}
}
}
This is what I've tried so far (this is in the TableViewCell class), storing the progress for each task by URL in a global dictionary: var itemsDownloadingNow = [URL: Float]()
This works if the table is NOT reloaded (the cells are shuffled), but AFTER reloading the circle progress goes back to zero/doesn't track. I don't know how to properly do this, and the only way to continuously update the circle is putting it in this URL session func.
How can I track the download progress per cell?
In an app I'm writing, I need to download a PDF from a server and display it in a UIWebView. To this end, I've got a bit of code that retrieves the PDF from an endpoint (it's not a URL, and on a desktop computer, opens up a dialogue for saving as in a browser) and loads it onto the device by grabbing the data in a class called PDFGrabber:
func getPDF(completionHandler:#escaping (URL) -> Void)
{
let theURL:String = "https://mywebsite.com/Endpoint"
let fileURL:URL = URL(string: theURL)!
var request = URLRequest(url: fileURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10.0)
let session = URLSession.shared
let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
var documentURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last
documentURL = documentURL?.appendingPathComponent("MyDocument.pdf")
do
{
try data?.write(to: documentURL!, options: .atomic)
completionHandler(documentURL!)
}
catch
{
print(error)
}
})
task.resume()
}
Then, in the table view controller showing the PDFs (let's call it PDFTableView, I can use the documentURL from the PDFGrabber when a PDF is requested (by tapping a cell in the table):
PDFGrabber.getPDF(){fileURL -> () in
DispatchQueue.main.async
{
if let resultController = self.storyboard!.instantiateViewController(withIdentifier: "PDFViewer") as? PDFViewer
{
resultController.thePDFPath = stringURL
self.present(resultController, animated: true, completion: nil)
}
}
Finally, I have another View Controller with a UIWebView called "WebView" and the attribute "thePDFPath" as a string in the PDFViewer view controller. In the viewDidLoad() method, I can say:
override func viewDidLoad()
{
let pathToPDF:URL = URL(string: thePDFPath)!
WebView.loadRequest(URLRequest(url: pathToPDF))
}
Together, this loads the PDF into the web view. However, the loading times can be a little slow, and I'd like to be able to calculate how much of the PDF has been loaded onto a user's device using a progress bar and a string. From other questions, I gather than I'd need to have my PDFGrabber class extend URLSessionDownloadDelegate, and then implement the functions. To get the amount of bytes downloaded is straightforward, since I can simply go:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten writ: Int64, totalBytesExpectedToWrite exp: Int64)
{
percentDownloaded = (Float(writ)/Float(exp)) * 100
print("Progress: " + String(describing: percentDownloaded))
}
But I'd also like to be able to open the PDFView only after the PDF is wholly downloaded after displaying its progress as a percent (I've got an overlay view that does this). Before, I could use a completion handler and wait until the PDF finished downloading, then open it.
However, this method does not allow me to access the amount of bytes downloaded; would I go about opening the view from the
urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
function, or is there something else I need to do?
Suggestions would be much appreciated; thanks!
import WebKit
here you can load your pdf in your application
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if let pdfURL = Bundle.main.url(forResource: "food-and-drink-menu-trafalgar-tavern", withExtension: "pdf", subdirectory: nil, localization: nil) {
do {
let data = try Data(contentsOf: pdfURL)
let webView = WKWebView(frame: CGRect(x:0,y:NavigationView.frame.size.height,width:view.frame.size.width, height:view.frame.size.height-NavigationView.frame.size.height))
webView.load(data, mimeType: "application/pdf", characterEncodingName:"", baseURL: pdfURL.deletingLastPathComponent())
view.addSubview(webView)
}
catch {
// catch errors here
}
}
}
I have UITableView with custom cells. I want to show progress indicator for multiple images upload.
I have tried reloadRowAtIndexPath method of UITableView but it not sufficient solution because cell is continuously blinks which looks weird.
Another one solution i found is to store reference of my progress indicator view placed in UITableViewCell in global variable and then modify it outside UITableView datasource methods, but in this solution i faced one problem which is i have to keep track of multiple progress indicator view objects of UITableViewCell which is difficult because UITableView datasource is two dimensional NSMutableArray(In short array inside array) so i don't have unique IndexPath.row because of multiple sections. So how can i manage objects of progress indicator view?
And also Is there any better solution to do this type of job?
Ok, so here is what I used in one of my projects when I could not find anything else.
Swift 3
Make a sub class of NSObject (because a sub class of URLSession won't let you set configuration and other parameters as the only designated initializer there is init()) that includes the information of the cell that started the upload process as in IndexPath and also a URLSession object.
Use this sub class to create new URLSession whenever you want to upload (I used uploadTask method of URLSession).
Create uploadTask and start uploading.
You will also have to make your own protocol methods that are called by normal protocol methods of URLSession, to send your custom sub class instead of URLSession object to the delegate you want.
Then in that delegate, you may check for the information of indexPath that is stored in the custom sub class you got from the previous step and update the appropriate cell.
The same could be achieved by using Notifications I guess.
Below is the screenshot of the sample application I wrote:
public class TestURLSession:NSObject, URLSessionTaskDelegate {
var cellIndexPath:IndexPath!
var urlSession:URLSession!
var urlSessionUploadTask:URLSessionUploadTask!
var testUrlSessionDelegate:TestURLSessionTaskDelegate!
init(configuration: URLSessionConfiguration, delegate: TestURLSessionTaskDelegate?, delegateQueue queue: OperationQueue?, indexPath:IndexPath){
super.init()
self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)
self.cellIndexPath = indexPath
self.testUrlSessionDelegate = delegate
}
func uploadTask(with request: URLRequest, from bodyData: Data) -> URLSessionUploadTask{
let uploadTask = self.urlSession.uploadTask(with: request, from: bodyData)
self.urlSessionUploadTask = uploadTask
return uploadTask
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64){
self.testUrlSessionDelegate.urlSession(self, task: self.urlSessionUploadTask, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
}
}
protocol TestURLSessionTaskDelegate : URLSessionDelegate {
func urlSession(_ session: TestURLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
}
Edits are welcome.
Here the solution which i applied, may be helpful to someone who wants same implementations as i want, without using third party library or classes.
I have created one custom UIView and design circular progress indicator using CALayer and some animations. This is not a big deal. But the thing which is difficult for me is i want this progress indicator in several cells which indicates multiple image progress in percentages.
So i have created one custom class with properties like
#property (nonatomic,retain) NSIndexPath *indexPath;
#property (nonatomic,strong) NSURLSessionTask *uploadtask;
Then i maintain one NSMutableArray which contains my custom class objects which has values for each uploadTask for currently uploading images and merged string which contains indexPath. Now i have track of my all uploading images so i have change uploaded percentage in my custom progress indicator UIView with help of indexPath values whenever i receive response from delegate method of NSURLSession.
I had a similar stuff to do where in which I wanted to download files and show progress bar. My idea was to create a Custom object which keep track of a particular download and all the cells will internally listen to the changes in this object. Each cell will have its own object uniquely identified by the task identifier. I have written a sample code in Swift 3 available in the below link (skeleton code also added)
FileDownloader
class DownLoadData: NSObject {
var fileTitle: String = ""
var downloadSource: String = ""
var downloadTask: URLSessionDownloadTask?
var taskResumeData: Data?
var downloadProgress: Float = 0.0
var isDownloading: Bool = false
var isDownloadComplete: Bool = false
var taskIdentifier: Int = 0
var groupDownloadON:Bool = false
var groupStopDownloadON:Bool = false
init(with title:String, and source:String){
self.fileTitle = title
self.downloadSource = source
super.init()
}
func startDownload(completion:#escaping (Result<Bool, Error>)->Void,progressHandler:#escaping (Float)->Void ){
}
func resumeDownload(completion:#escaping (Result<Bool, Error>)->Void,progressHandler:#escaping (Float)->Void ){
}
func pauseDownload(){
}
func stopDownload(){
if self.isDownloading{
}
}
func cleanUpHandlers(){
// remove the completion handlers from the network manager as resume is taken as a new task.
}
func handleDownloadSuccess(){
}
func handleDownloadError(){
}
}
Use URLSessionTaskDelegate method and do necessary calculation:
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
Below is a solution, that is tested for a single file upload. But you can modify it to support multiple file uploads. Make sure to add necessary IBOutlets and IBAction as necessary. The image is added in 'Assets.xcassets'.
My UI looks like below:
Below is the code for ViewController.
import UIKit
class UploadProgressViewController: UIViewController, URLSessionTaskDelegate {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var progressView: UIProgressView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
progressView.progress = 0.0
}
#IBAction func didTapOnStartUploadButton(_ sender: Any) {
startDownload()
}
func startDownload () {
// 1. Prepare data to download
var data = Data()
if let image = UIImage(named: "swift.jpg") {
data = image.pngData()!
}
// 2. Creation of request
var request = URLRequest(url: NSURL(string: "http://127.0.0.1:8000/swift.png")! as URL)
request.httpMethod = "POST"
request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")
// 3. Configuring the Session
let configuration = URLSessionConfiguration.default
let mainqueue = OperationQueue.main
// 4. Start the upload task
let session = URLSession(configuration: configuration, delegate:self, delegateQueue: mainqueue)
let dataTask = session.uploadTask(with: request, from: data)
dataTask.resume()
}
// URLSessionTaskDelegate Handling
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
let uploadProgress: Float = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
print("session \(session) uploaded \(uploadProgress * 100)%.")
progressView.progress = uploadProgress
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
print(error.debugDescription)
}
I want to identify a session within my didFinishDownloadingToURL method:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL)
{
if (session.sessionType == EnumImageRequestSession)
{
// I want to check with sessionType, but NSURLSession does not have
// any such property. How to add this type property while creating the session?
}
if (session.sessionType == EnumAudioRequestSession)
{
}
}
How to achieve this? Should I create a subclass of NSURLSession and add a sessionType property?
Here are a few options that I have used in the past.
When you start the NSURLSession, you can add it to an Dictionary or Set with a key value associated to the Session. When it completes, find the Session using it's identifier in the Set, and then you will have your associated key.
You can check the URL associated with the session.
Option 1 is my tried and true method so far, using a Set.
As NSURLSession is an NSObject subclass, you can use an Objective-C associated object, and by doing so you avoid doing housekeeping of for instance a map of sessions by type that you would otherwise manually have to create.
Here's a short example (where I'm cutting some corners with forced unwraps I actually would not in production oriented code):
import Foundation
import ObjectiveC
enum SessionType:Int {
case Audio
case Image
}
func someFunctionWhereYouCreateTheSession() {
let session:NSURLSession = NSURLSession()
objc_setAssociatedObject(session, "sessionType",
NSNumber(integer:SessionType.Audio.rawValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL)
{
let sessionType:SessionType = SessionType(rawValue:(objc_getAssociatedObject(session, "sessionType") as! NSNumber).integerValue)!
switch sessionType {
case .Audio:
print("foo")
case .Image:
print("bar")
}
}
Im working on a folders/files application where users are able to download files to local disk. Whenever a user is downloading a file, I want to show a download bar that displays progress.
to do so, I've created a protocol that allows my download class and my view controller to communicate:
protocol:
protocol DownloadResponder : class {
func downloadFinished()
func downloadProgress(current:Int64, total:Int64)
}
download class:
class fileDownloader: NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate {
//responder
var responder : MyAwesomeDownloadResponder?
init(responder : MyAwesomeDownloadResponder) {
self.responder = responder
}
...
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
println("downloaded \(100*totalBytesWritten/totalBytesExpectedToWrite)")
responder?.downloadProgress(totalBytesWritten, total: totalBytesExpectedToWrite)
}
...
}
and then in my view controller I have my download button which trigger the downloadProgress function:
func downloadProgress(current:Int64, total:Int64) {
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
var currentProgress = 100 * current / total
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.downloadLbl.text = "Downloaded \(currentProgress)%"
//set progress bar
self.progressBar.setProgress(Float(currentProgress), animated: true)
}
}
}
While printing information in the console works all the time, updating the UI was not really stable. To fix this I used the dispatch_async method that push the UI change on the main thread. However, while it always work on the first time, poping back to the previous view controller and coming back again, executing the download once more does not trigger the UI updates. The progress bar progressBar.setProgress does nothing and my label downloadLbl.text does not update itself.
Does anyone have an idea about the way to solve this?
If my question lacks information, please let me know and I'll try to add up to the existing information. Thanks!
As I didn't receive / find any solution to my problem I went back to an higher level and changed the way to communicate between my classes to handle ui changes based on background download thread progression.
Instead of using protocols, I went for Notifications and it solved my problem.
Inside the download class:
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
println("downloaded \(100*totalBytesWritten/totalBytesExpectedToWrite)")
//NOTIFICATION
// notify download progress!
var fileInfo = [NSObject:AnyObject]()
fileInfo["fileId"] = fileDownloader.storageInfo[downloadTask.taskIdentifier]!["id"] as! Int!
fileInfo["fileCurrent"] = Float(totalBytesWritten)
fileInfo["fileTotal"] = Float(totalBytesExpectedToWrite)
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.postNotificationName("DownloadProgressNotification",
object: nil,
userInfo: fileInfo)
}
inside the view controller:
override func viewDidLoad() {
super.viewDidLoad()
// ready for receiving notification
let defaultCenter = NSNotificationCenter.defaultCenter()
defaultCenter.addObserver(self,
selector: "handleCompleteDownload:",
name: "DownloadProgressNotification",
object: nil)
}
func handleCompleteDownload(notification: NSNotification) {
let tmp : [NSObject : AnyObject] = notification.userInfo!
// if notification received, change label value
var id = tmp["fileId"] as! Int!
var current = tmp["fileCurrent"] as! Float!
var total = tmp["fileTotal"] as! Float!
var floatCounter = 100 * current / total
var progressCounter = String(format: "%.f", floatCounter)
if(id == self.fileId){
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
// do some task
dispatch_async(dispatch_get_main_queue()) {
// update some UI
self.downloadLbl.text = "Downloaded \(progressCounter)%"
self.progressBar.setProgress((progressCounter as NSString).floatValue, animated: true)
}
}
}
}
hope that will help!