Identify NSURLSession in completion block - ios

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")
}
}

Related

'Invalid argument' when trying to use a background URLSession for a Download Task

I am working on an application that requires to download a certain number of files to be able to work offline. Obviously, download tasks are preferred to be done with the app in the background. I implemented an URLSession with a background configuration following Apple's documentation available here : https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background. I also followed a tutorial on raywenderlich: https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started.
Basically, what I've done looks like this (I've made my class a Singleton but I have the same problem either way):
public final class DownloadService: NSObject {
static let shared = DownloadService()
static let identifier = "downloadService"
private var urlSession: URLSession!
var backgroundCompletionHandler: (() -> Void)? // This is attributed in the handleEventsForBackgroundURLSession delegate method in the AppDelegate
private override init() {
super.init()
let config = URLSessionConfiguration.background(withIdentifier: DownloadService.identifier)
config.isDiscretionary = true
urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)
}
}
extension DownloadService: URLSessionDelegate {
// Delegate method called when the background session is finished.
public func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
guard let completionHandler = self.backgroundCompletionHandler else {
Logger.fault("No completion for bg session", category: .network)
return
}
Logger.log("Complete background session", category: .network)
// This must be executed on the main thread
// Executes things such as updating the app preview in recent apps view
completionHandler()
}
}
}
extension DownloadService: URLSessionDownloadDelegate {
// Delegate method called when a download task is finished
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
// Perform
guard let sourceUrl = downloadTask.originalRequest?.url else {
return
}
Logger.log("Received file: %#", sourceUrl.lastPathComponent, category:.network)
// Check and save file
saveFile(originalFileURL: sourceUrl, downloadedTo: location)
}
}
And I start the download using:
/// Download file using a previously created URLSession.
/// - parameter filename: Name of the file.
/// - parameter baseURL: URL where the files are located.
/// - parameter size: Expected filesize in Bytes.
private func download(file filename: String, from baseURL: String, size: Int64) {
guard let url = URL(string: baseURL)?.appendingPathComponent(filename) else { return }
let task = urlSession.downloadTask(with: url)
task.countOfBytesClientExpectsToSend = 0
task.countOfBytesClientExpectsToReceive = size
task.resume()
}
My problem is that everything works fine when the app is in foreground, but whenever I put the app in the background or lock the screen, I have an error saying:
Task <46648342-7D13-4D1F-96A1-FDAE4C1F8475>.<362> finished with error [22] Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument"
I have tried playing a bit with the URLSessionConfiguration, specifically the isDiscretionary parameter which is set to false by default, and it seems that setting it to true, as advised by Apple's documentation, even blocks the download from proceeding with the app in the foreground, resulting to the same error 'Invalid argument'.
I wonder if this parameter has anything to do with my problem, or if there's something I've misunderstood?
The exemple on raywenderlich provided above also works the same way, using isDiscretionary seems to make the download fail everytime.
I am using Xcode 11.3.1 with Swift 5 and targeting iOS13.
Let me know if any other information is needed and thank you for your help!
So, I was trying to do it with a simulator. Either by running from Xcode with the debugger, or by installing the app into the simulator (without the debugger since it affects the application lifecycle).
I tried to run it on a real device (iPad), and there's no sign of this error whatsoever! Setting isDiscretionary seems to work as intended so I'm not sure that this parameter was causing the issue on a simulator.

show progress indicator for multiple image upload in UITableViewCell

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)
}

Default implementation of protocol method with Swift extension

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())

How to add NSURL session progress to table view cell

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

Delegate methods not called in implementation of NSURLSessionData

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.

Resources