Why does my URLDownloadTask not call didFinishDownloadtingTo? - swift

Most of my programming experience is in Shell and Python. I'm fairly new to Swift, like "3 days ago" new. I just can't figure out why didFinishDownloadtingTo is not called when my downloadTask completes. Here's my AppDelegate.swift file:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate, URLSessionDownloadDelegate {
#IBOutlet weak var window: NSWindow!
#IBOutlet var progressind: NSProgressIndicator!
#IBOutlet var outputtext: NSTextField!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
let requestURL: URL = URL(string: "https://www.apple.com")!
let urlRequest: URLRequest = URLRequest(url: requestURL as URL)
let session = URLSession.shared
let downloads = session.downloadTask(with: urlRequest)
print("starting download...")
downloads.resume()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
print("download finished!")
}
}
I'm using Swift 3, and I just cannot find enough documentation on it to figure this out on my own.
I went a little crazy declaring classes trying to figure out why it wasn't working right, so I'm sure there are also some errors there.
It appears to download the file successfully. I've tried with several URLs. The "Disk" and "Network" sections of the debug menu appear consistent with downloading a file of the size at every URL I've tested with.

The thing is that when you use NSURLSession(URLSession in Swift 3) you have to choose if you want to use a delegate or a completion handler, in case of use both, only the completion handler gets called. In your case the delegate is not set so you can't see the call to the delegate methods. Instead you should specify the delegate using the another initializer of NSURLSession like in the following code:
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, URLSessionDelegate, URLSessionDownloadDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
let requestURL: URL = URL(string: "https://www.apple.com")!
let urlRequest: URLRequest = URLRequest(url: requestURL as URL)
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue.main)
let downloads = session.downloadTask(with: urlRequest)
print("starting download...")
downloads.resume()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL){
print("download finished!")
}
}
And then you should see the delegate method called properly.
I hope this help you

The key lies in the word "delegate". It's like the dog in the night-time in the Sherlock Holmes story. The dog did nothing in the night. The word "delegate" never appears in your code!
But it needs to. didFinishDownloadingTo is a delegate method. You can only receive this method if you are the delegate of the NSURLSession. You need to set self as its delegate. The runtime doesn't magically read your mind and know that you intend this object as the delegate; you have to tell it.

Related

Am unable to receive HTTP responses with UIViewController set as URLSessionDelegate

Wed 5/18 Additional Info added at Step 5
I am able to create a URLSesion, build a request with a file to upload and successfully call it from my app. On my server side, the proper script is called, uploaded file is saved, etc,. However, I am not receiving the HTTP responses, data, etc.
Actually had this working without the delegate, when the HTTP response functions were within the task itself. But am now trying to expand functionality and am missing something while trying implement the delegate.
The trimmed code is below, and it all works, with the exception of setting up UIViewController as the URLSession delegate. Just trying to figure out why my UIViewController is not receiving the HTTP responses.
Below is the code for:
UIViewController
Class which creates the upload session (UploadService)
Extension for
UIViewController which I want to use to process the responses
How the previous task looked, when it worked. Before I tried to implement the delegate.
Used print to confirm that my UIViewConroller is the delegate, yet it still receives no HTTP response, data, or error messages
UIViewController
class UploadInv : UIViewController {
var xFile : XFile?
...create UI....
let uploadService = UploadService()
lazy var uploadSession: URLSession = {
let configuration = URLSessionConfiguration.default
return URLSession(configuration: configuration, delegate: self, delegateQueue: .main)
}()
override func viewWillAppear(_ animated: Bool) {
...
}
override func viewDidLoad() {
super.viewDidLoad()
uploadService.uploadSession = uploadSession
... code the lays out all buttons, labels, etc...
}
#objc func buttonAction(sender: UIButton!) {
guard let theButton = sender else { return}
let myTag = theButton.tag
switch myTag {
//button to start upload
case ButtType.up.rawValue:
uploadService.start(upFile: xFile!, script: "uploadOrig.pl", upLoadInvClass: self)
uploadService.task?.resume()
//button to select file to upload
case ButtType.file.rawValue:
... file xFile with file info
}
}
UploadService
class UploadService {
var uploadSession : URLSession!
var task: URLSessionUploadTask?
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
}
}
extension
extension UploadInv: UIDocumentPickerDelegate, URLSessionDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
... file xFile info for upload ....
... http request created ....
}
// Below are the three simple functions which I would handle
// responses the server, but these never seem to get called.
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let err = error {
print("Error: \(err.localizedDescription)")
}
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: (URLSession.ResponseDisposition) -> Void) {
print("didReceive response")
completionHandler(URLSession.ResponseDisposition.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
print("didReceive data")
if let responseText = String(data: data, encoding: .utf8) {
print(responseText)
}
}
}
Pre-Delegate model which worked
class UploadService {
var uploadSession = URLSession.shared
func start(upFile: XFile, script: String, upLoadInvClass: UploadInv) {
var request = upFile.makeUrlReq(upFile: upFile, script: script)
uploadSession.uploadTask(with: request, from: request.httpBody )
{ (data, response, error) in
if let response = response {
upLoadInvClass.upResp(resp: response)
}
if let error = error {
upLoadInvClass.upErr(error: error)
}
if let data = data {
upLoadInvClass.upData(data: data)
}
}.resume()
}
}
Step 5:
task = uploadSession.uploadTask(with: request, from: request.httpBody! )
print("\(uploadSession.delegate)")
task?.resume()
For other newbies also stuck on this, it turns out there's more than one delegate to look at. There are:
URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, and more. So obviously I was using the wrong one, might have been fell trap to "autocomplete." Nevertheless, I have to make sure I read more documentation on the subject.
Thanks to Scott who "passively/aggressively" gave me the answer, here, while still allowing me to "think." I mean that as a compliment. He told me to add the line:
assert(uploadSession.delegate! is URLSessionDataDelegate)

Call view controller function from delegate class

I'm relatively new to swift and I'm having issues trying to call a function in a view controller from a delegate I have defined. How can I call the function in my view controller from this delegate? This is a mixed project consisting of mostly Objective-C code with only one Swift controller. The function is inside of the Swift controller. Below is the delegate class:
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//convert to https
let http = request.url!
var comps = URLComponents(url: http, resolvingAgainstBaseURL: false)!
comps.scheme = "https"
let httpsUrl = comps.url!
ViewControllerFunction(url: httpsUrl)
}
I get an error Use of unresolved identifier 'ViewControllerFunction'. I've tried creating an instance of the view controller but don't think that's the correct way to do it as this view controller also has an audio player (it also didn't work).
Here is where I call the delegate from a function inside the view controller:
let urlString = "https://urlthatredirects.com"
let config = URLSessionConfiguration.default
let url = URL(string: urlString)
//set delegate value equal to SessionDelegate to handle 302 redirect
let delegate = DelegateToHandle302()
//establish url session
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
//set task with url
let dataTask = session.dataTask(with: url!)
//init call
dataTask.resume()
I'm following part of an example on how to get the final URL from a redirection (https://gist.github.com/mgersty/b565ba4c9e9422637f15f52a5317f07e). My view controllers "header" is:
#objc class AudioPlayerController: UIViewController{........}
I hope I've provided enough info to allow anyone to assist me in figuring out what I'm doing wrong. The only thing I need to do is call that function and pass the redirection URL to it.
I'm a bit confused about what you're trying to do, and why you're trying to call a delegate method back into the VC rather than using the completion handler; but I think you've got your delegate pattern back-to-front. I'm assuming the idea is:
the view controller instiagtes the URL session
the url sessions passes of the result of the URLSession to the DelegateToHandle302 to process
DelegateToHandle302 then tries to run a method back in the view controller that launched it.
If this is the case you actually need the VC to be the delegate of the DelegateToHandle302 class, not the other way around.
So within your view controller
let handlerFor302 = DelegateToHandle302()
handlerFor302.delegate = self.
let session = URLSession(configuration: config, delegate: handlerFor302, delegateQueue: nil
//etc... as before
Create a protocol for the delegate to adopt, which defines the desired function
protocol URLProcessor {
func ViewControllerFunction(url: URL)
}
The adopt the protocol in your view controller and implement the method
extension MyViewController: URLProcessor {
func ViewControllerFunction(url: URL) { .... do whatever ...}
and then use the delegate with the protocol method in your DelegateToHandle302
class DelegateToHandle302:NSObject, URLSessionTaskDelegate {
weak var delegate: URLProcessor?
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
//Process the output
delegate?.ViewControllerFunction(url: httpsUrl)
}

URLSession downloadTask behavior when running in the background?

I have an app that needs to download a file which may be rather large (perhaps as large as 20 MB). I've been reading up on URLSession downloadTasks and how they work when the app goes to the background or is terminated by iOS. I'd like for the download to continue and from what I've read, that's possible. I found a blog post here that discusses this topic in some detail.
Based on what I've read, I first created a download manager class that looks like this:
class DownloadManager : NSObject, URLSessionDownloadDelegate, URLSessionTaskDelegate {
static var shared = DownloadManager()
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
private override init() {
}
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
DispatchQueue.main.async {
if let completionHandler = self.backgroundSessionCompletionHandler {
self.backgroundSessionCompletionHandler = nil
completionHandler()
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
if let sessionId = session.configuration.identifier {
log.info("Download task finished for session ID: \(sessionId), task ID: \(downloadTask.taskIdentifier); file was downloaded to \(location)")
do {
// just for testing purposes
try FileManager.default.removeItem(at: location)
print("Deleted downloaded file from \(location)")
} catch {
print(error)
}
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
if totalBytesExpectedToWrite > 0 {
let progress = Float(totalBytesWritten) / Float(totalBytesExpectedToWrite)
let progressPercentage = progress * 100
print("Download with task identifier: \(downloadTask.taskIdentifier) is \(progressPercentage)% complete...")
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print("Task failed with error: \(error)")
} else {
print("Task completed successfully.")
}
}
}
I also add this method in my AppDelegate:
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: #escaping () -> Void) {
DownloadManager.shared.backgroundSessionCompletionHandler = completionHandler
// if the app gets terminated, I need to reconstruct the URLSessionConfiguration and the URLSession in order to "re-connect" to the previous URLSession instance and process the completed download tasks
// for now, I'm just putting the app in the background (not terminating it) so I've commented out the lines below
//let config = URLSessionConfiguration.background(withIdentifier: identifier)
//let session = URLSession(configuration: config, delegate: DownloadManager.shared, delegateQueue: OperationQueue.main)
// since my app hasn't been terminated, my existing URLSession should still be around and doesn't need to be re-created
let session = DownloadManager.shared.session
session.getTasksWithCompletionHandler { (dataTasks, uploadTasks, downloadTasks) -> Void in
// downloadTasks = [URLSessionDownloadTask]
print("There are \(downloadTasks.count) download tasks associated with this session.")
for downloadTask in downloadTasks {
print("downloadTask.taskIdentifier = \(downloadTask.taskIdentifier)")
}
}
}
Finally, I start my test download like this:
let session = DownloadManager.shared.session
// this is a 100MB PDF file that I'm using for testing
let testUrl = URL(string: "https://scholar.princeton.edu/sites/default/files/oversize_pdf_test_0.pdf")!
let task = session.downloadTask(with: testUrl)
// I think I'll ultimately need to persist the session ID, task ID and a file path for use in the delegate methods once the download has completed
task.resume()
When I run this code and start my download, I see the delegate methods being called but I also see a message that says:
A background URLSession with identifier com.example.testapp.background already exists!
I think this is happening because of the following call in application:handleEventsForBackgroundURLSession:completionHandler:
let session = DownloadManager.shared.session
The getter for the session property in my DownloadManager class (which I took directly from the blog post cited previously) is always trying to create a new URLSession using the background configuration. As I understand it, if my app had been terminated, then this would be the appropriate behavior to "reconnect" to the original URLSession. But since may app is not being terminated but rather just going to the background, when the call to application:handleEventsForBackgroundURLSession:completionHandler: happens, I should be referencing the existing instance of URLSession. At least I think that's what the problem is. Can anyone clarify this behavior for me? Thanks!
Your problem is that you are creating a new session every time you reference the session variable:
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
return URLSession(configuration: config, delegate: self, delegateQueue: OperationQueue())
}
}
Instead, keep the session as an instance variable, and just get it:
class DownloadManager:NSObject {
static var shared = DownloadManager()
var delegate = DownloadManagerSessionDelegate()
var session:URLSession
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
override init() {
session = URLSession(configuration: config, delegate: delegate, delegateQueue: OperationQueue())
super.init()
}
}
class DownloadManagerSessionDelegate: NSObject, URLSessionDelegate {
// implement here
}
When I do this in a playground, it shows that repeated calls give the same session, and no error:
The session doesn't live in-process, it's part of the OS. You're incrementing reference count every time you access your session variable as written, which causes the error.

Renamed issues in Swift

I'm working on programming with Swift for the first time, and in doing so I'm following along with this tutorial. Unfortunately it looks like the tutorial is a little outdated and most of the code is throwing Buildtime errors. The most reoccurring error is the NSURLSession has been renamed to URLSession. I've tried letting Swift fix it, but in many cases it just starts throwing warnings.I'm also getting a Value type HomeModel has no member'parseJSON' error as well as a NSDat is not implicitly convertible to data error. From what I can tell, it looks like the NSURL is no longer used, but I'm not sure about the other two. Seeing how this is the first Swift project I've worked on, I'm not sure how to fix these. Can someone provide some insight on how to fix these mistakes?
here is the code:
import Foundation
protocol HomeModelProtocal: class {
func itemsDownloaded(items: NSArray)
}
class HomeModel: NSObject, NSURLSessionDataDelegate {
//properties
weak var delegate: HomeModelProtocal!
var data : NSMutableData = NSMutableData()
let urlPath: String = "http://testurl.com/service.php" //this will be changed to the path where service.php lives
func downloadItems() {
let url: NSURL = NSURL(string: urlPath)!
var session: NSURLSession!
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session.dataTaskWithURL(url)
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.data.appendData(data);
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON()
}
}
}
Several basic types have dropped the "NS" prefix in Swift 3.0. Earlier in swift 2.2, we used to have NSUserDefaults, NSURLSession, NSFileManager etc. Now, most of them dropped their prefix "NS" and changed to UserDefaults, URLSession, FileManager etc.
Your code contains a lot of types with 'NS' prefix. By simply removing it, your code can be converted to Swift 3. Your converted code looks like as shown below:
protocol HomeModelProtocal: class {
func itemsDownloaded(items: NSArray)
}
class HomeModel: NSObject, URLSessionDataDelegate {
//properties
weak var delegate: HomeModelProtocal!
var data : Data = Data()
let urlPath: String = "http://testurl.com/service.php" //this will be changed to the path where service.php lives
func downloadItems() {
let url: URL = URL(string: urlPath)!
var session: URLSession!
let configuration = URLSessionConfiguration.default
session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: url)
task.resume()
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.data.append(data);
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil {
print("Failed to download data")
}else {
print("Data downloaded")
self.parseJSON() // This class doesn't have a function parseJSON(). So, it's giving you an error like this
}
}
}
Also, I don't see any function called parseJSON() in your class. I believe you have to add it.

NSURLSession does not follow redirection

For a screen scraping project I'm using NSURLSession in Swift to read an HTML page. But already the start fails because the returned page gives a redirection to a new webpage and my code doesn't follow that. I thought redirection would work by default, if no delegate is set for the session. But neither case does the redirection. Here's my test project:
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSURLSessionTaskDelegate {
#IBOutlet weak var window: NSWindow!
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse,
newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(request);
}
func applicationDidFinishLaunching(aNotification: NSNotification) {
var url = NSURL(string: "https://banking.dkb.de");
let defaultConfigObject = NSURLSessionConfiguration.defaultSessionConfiguration();
let session = NSURLSession(configuration: defaultConfigObject, delegate: self, delegateQueue: nil);
let task = session.dataTaskWithURL(url!, completionHandler: { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
let text = NSString(data: data, encoding: NSUTF8StringEncoding);
// text contains here: <head><meta HTTP-EQUIV="REFRESH" content="0; url=/dkb/"></head>
if var document = NSXMLDocument(data: data, options: Int(NSXMLDocumentTidyHTML), error: nil) {
if let forms = document.nodesForXPath("//form[#name='login']", error: nil) where forms.count > 0 {
let form = forms[0] as! NSXMLNode;
}
}
});
task.resume();
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}
}
What do I have to do to make redirection work here?
Update:
On further investigation I found out that it must have to do with the result from the server. For instance using https://www.google.com indeed triggers the redirection delegate. However, since any browser can handle also redirection from the bank address, there must be a different approach in place to properly handle that and I'd like to learn how.
NSURLSession supports 302 redirect and https://www.google.com uses it.
On the other hand, https://banking.dkb.de/ uses meta tag as described. It returns 200(OK) as status code so you must handle it reading meta tag.