I have a class, AWSUtil, and I would like to be able to get the progress of image uploads and downloads using NSURLSessions
class AWSUtil : NSObject, NSURLSessionDelegate, NSURLSessionDownloadDelegate
I'm able to set up the sessions, and they work
func sessionTest(url: NSURL){
let task = NSURLSession(
configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),
delegate: self,
delegateQueue: NSOperationQueue.mainQueue()
).dataTaskWithURL(url){(data, response, error) in
//code
}
task!.resume()
}
But my problem is that the delegate methods, such as NSURLSession didWriteData are not being called.
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64){
//never called
}
I've tried implementing different NSURLSession protocols and making the class not conform to NSObject, but neither work. No matter what delegates I implement, or what delegate methods I put in the class, none of them are called.
I would assume it's because the object is being deallocated before they are called, but I'm not sure. If I wanted to make a call on one of the functions, I would use
func awsTest(url: NSURL){
let aws = AWSUtil()
aws.sessionTest(url)
}
But none of the NSURLSessionDownloadDelegate methods are being called. Is there any way to fix this, or is there a workaround?
You're calling dataTaskWithURL. This is a data task method. Data tasks have a protocol called NSURLSessionDataDelegate. Override those methods, e.g. URLSession:dataTask:....
didWriteData is called when using download methods, such as downloadTaskWithURL:, and you're not calling a download method.
If you look at https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/#//apple_ref/occ/instm/NSURLSession/downloadTaskWithURL:, in the contents area on the left you'll see that data, download, and upload tasks are broken out separately. Each has their corresponding delegate methods.
Related
I am using URLSessionConfiguration.background for uploading and downloading content while my app is not foreground. The code that I'm using to create a background session is as below:
private var session: URLSession!
private override init() {
super.init()
let config = URLSessionConfiguration.background(withIdentifier: Constants.backgroundSessionIdentifer)
config.sessionSendsLaunchEvents = true
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
In application delegate, I receive a completion handler and store it as a property of UploadDownloadService which is a singleton.
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
Logger.debug(#function)
UploadDownloadService.shared.backgroundCompletionHandler = completionHandler
}
I'm executing the completion handler in urlSessionDidFinishEvents as apple demonstrated. My code is given below:
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
Logger.debug(#function)
delegate?.urlSessionDidFinishEventsCalled()
DispatchQueue.main.async {
[weak self] in
guard let backgroundCompletionHandler = self?.backgroundCompletionHandler else {
return
}
Logger.debug("Executing background completion handler.")
backgroundCompletionHandler()
self?.backgroundCompletionHandler = nil
self?.delegate?.didCallBackgroundCompletionHandler()
}
}
Now what I've found is urlSession(_ session:, downloadTask:, didFinishDownloadingTo location:) is called before urlSessionDidFinishEvents so I am sure that my downloaded file is saved to a permanent location before I call the completion handler inside urlSessionDidFinishEvents.
But I'm confused about the upload case. I didn't find any delegate method that is called before urlSessionDidFinishEvents method. The only delegate method I found for upload that is called when the app is awake is urlSession(_ session:, task:, didCompleteWithError:). Which is called after urlSessionDidFinishEvents.
Now in this scenario, I've two questions in mind.
1. From where should I call the completion handler so that I'm sure all of my post-processing is done after upload(In this case I want to query the database, update and send a message. I'm building a chat application)? Is it okay to delay the completion handler call while I do my processings and call it from outside of the urlSessionDidFinishEvents method?
2. How much time do I get approximately after I call the completion handler?
Note: I've done a lot of googling before posting, found a single related question here. But the only answer(as of now) on that post doesn't answer my question. So I've decided to ask the question elaborately(describing my scenario) 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")
}
}
I'm trying to write default behaviour for a delegate method using a Swift extension as below, but it is never called. Does anyone know why or how to do it the right way?
extension NSURLSessionDelegate {
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
//default behaviour here
}
}
Adding override does not work either.
According to this,
Apple's default implementation looks like:
extension NSURLSessionDelegate {
func URLSession(session: NSURLSession, didBecomeInvalidWithError error: NSError?) { }
func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) { }
}
My DataTask calls typically look like this:
let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()
sessionConfiguration.HTTPCookieStorage = NSHTTPCookieStorage.sharedHTTPCookieStorage()
let session = NSURLSession(configuration: sessionConfiguration)
let requestURL = NSURL(string:"https:www.google.com/blabla")
session.dataTaskWithURL(requestURL!, completionHandler: completion).resume()
Where completion will typically be a Swift closure received via parameter.
I need to implement the URLSession(... didReceiveChallenge ...) function for all nsurlsessiontask implementations throughout my app, but can't set my session's delegate as I need to use the completionHandler (as mentioned in my comment below).
You can extends the NSURLSessionDelegate protocol for adding default implementation, but your NSURLSession objects needs a delegate.
This delegate can only be set using +sessionWithConfiguration:delegate:delegateQueue: (since the delegate property is read only), so your only way to set it is to subclass NSURLSession, override +sessionWithConfiguration: and call the initializer with the delegate property. The issue here is that you have to replace all your NSURLSession objects to MyCustomSessionClass. objects.
I suggest you to create a SessionCreator class which will conforms to NSURLSessionDelegate protocol and will create NSURLSessionobjects. You still have to replace the creation of your objects, but at least the object isn't the delegate of itself.
public class SessionCreator:NSObject,NSURLSessionDelegate {
//MARK: - Singleton method
class var sharedInstance :SessionCreator {
struct Singleton {
static let instance = SessionCreator()
}
return Singleton.instance
}
//MARK: - Public class method
public class func createSessionWithConfiguration (configuration:NSURLSessionConfiguration) -> NSURLSession {
return sharedInstance.createSessionWithConfiguration(configuration)
}
//MARK: - Private methods
private func createSessionWithConfiguration (configuration:NSURLSessionConfiguration) -> NSURLSession {
return NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
}
//MARK: - NSURLSessionDelegate protocol conformance
public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
// Always called since it's the delegate of all NSURLSession created using createSessionWithConfiguration
}
}
// Let create a NSURLSession object :
let session = SessionCreator.createSessionWithConfiguration(NSURLSessionConfiguration())
I have background downloading zip file:
if let url = NSURL(string: urlstring)
{
let config = NSURLSessionConfiguration.backgroundSessionConfigurationWithIdentifier((NSUUID().UUIDString))
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.downloadTaskWithURL(url)
session.sessionDescription = filepath
if let sessionId = session.configuration.identifier
{
print("start zip session: " + sessionId)
}
task.resume()
}
}
it works cool if you have internet connection but if you lose it during downloading app just wait and URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) will not be called never.
How it can be handle?
Something like time for response from server
The question is old, but still unanswered, so here is one workaround.
Some clarifications first
In general there is a property of URLSessionConfiguration called waitsForConnectivity which can be set to false where URLSessionTask will then directly fail on connection lost. However if true then URLSessionTaskDelegate will receive a callback on the urlSession(_:taskIsWaitingForConnectivity:) method.
However
Background tasks such as DownloadTask always wait for connectivity and ignore waitsForConnectivity property of URLSessionConfiguration. They also DON'T trigger urlSession(_:taskIsWaitingForConnectivity:) callback, so there is no official way to listen for connectivity drop-out on download task.
Workaround
If you listening for the download progress you'll notice that a call to the method is done few times in a second. Therefore we can conclude that if the progress callback is not called for more than 5 seconds, then there could be a connectivity drop issue. So the workaround is to make additional property to the URLSessionDownloadDelegate delegate and store the last update of the progress. Then have interval function to periodically check whether this property were not updated soon.
Something like:
class Downloader: NSObject, URLSessionTaskDelegate, URLSessionDownloadDelegate {
var lastUpdate: Date;
var downloadTask: URLSessionDownloadTask?;
public var session : URLSession {
get {
let config = URLSessionConfiguration.background(
withIdentifier: "\(Bundle.main.bundleIdentifier!).downloader");
config.isDiscretionary = true;
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue());
}
}
override init() {
self.lastUpdate = Date();
super.init();
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Handle download completition
// ...
}
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten writ: Int64,
totalBytesExpectedToWrite exp: Int64)
{
let progress = 100 * writ / exp;
// Do something with the progress
// ...
self.lastUpdate = Date();
}
}
var downloader = Downloader();
let url = "http://...";
var request = URLRequest(url: URL(string: url)!);
currentWorker.downloadTask = downloader.session.downloadTask(with: request);
currentWorker.downloadTask!.resume();
// Schedule timer for every 5 secs
var timer = NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "CheckInternetDrop", userInfo: nil, repeats: true);
func CheckInternetDrop(){
let interval = Date().timeIntervalSinceDate(downloader.lastUpdate);
if (interval > 5) {
print("connection dropped");
}
}
Very simple example...
According to apple docs on pausing and resuming downloads :
Also, downloads that use a background configuration will handle resumption automatically, so manual resuming is only needed for non-background downloads.
Besides, the reason waitsForConnectivity is being ignored is stated in the downloading files in the background :
If your app is in the background, the system may suspend your app while the download is performed in another process.
This is why even if you were to build a timer in a similar fashion to Dimitar Atanasov it will not work once the app goes in the background since it will be suspended.
I've tested the background downloading myself with network failures and the system resumes without fail, hence no extra code is required.
You can set timeouts for your requests:
config.timeoutIntervalForRequest = <desired value>
config.timeoutIntervalForResource = <desired value>
Documentation:
timeoutIntervalForRequest
timeoutIntervalForResource