I'm starting a download session in a view controller:
class MyController: UIViewController {
func startDownload(withURL url: URL) {
downloadSession = URLSession(configuration: URLSessionConfiguration.default,
delegate: self,
delegateQueue: OperationQueue.main)
downloadTask = downloadSession.dataTask(with: url)
downloadTask.resume()
}
}
The delegate methods are defined in an extension:
extension MyController: URLSessionDelegate {
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data) {
...
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
...
}
}
Why are these delegate methods never called?
You only declare conformance to URLSessionDelegate in your extension. If you declare conformance to URLSessionDataDelegate instead, your delegate methods will be called.
Related
Firstly, I already checked similar existing questions, and none of the answers apply.
NSURLSession delegates not called
URLSessionDelegate Function Not Being Called
I am trying to download a file using URLSessionDownloadTask, like so
class MyNetworkManager : NSObject
{
static let instance = MyNetworkManager()
var downloadSession : URLSession?
init()
{
super.init()
let downloadConfiguration = URLSessionConfiguration.default
downloadSession = URLSession(configuration: downloadConfiguration, delegate: self, delegateQueue: nil)
}
func download(_ url : URL)
{
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
let downloadTask = downloadSession?.downloadTask(with: urlRequest)
downloadTask?.resume()
}
}
extension MyNetworkManager : URLSessionDelegate
{
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
//
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL)
{
//
}
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?)
{
//
}
}
However, no URLSessionDelegate methods are called.
Normally, delegate methods are not called if you create a task with completion handler - that is not the case, I'm only using URLRequest as parameter when creating a task.
Session's delegate is properly set, and after calling downloadTask?.resume() its state property is running
MyNetworkManager is a singleton, I'm using it like so
MyNetworkManager.instance.download(someURL)
so an instance is definitely retained.
Am I missing something here?
You must conform to the relevant protocols, e.g.:
extension MyNetworkManager: URLSessionDelegate {
// this is intentionally blank
// obviously, if you implement any delegate methods for this protocol, put them here
}
extension MyNetworkManager: URLSessionDownloadDelegate {
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64) {
print(#function)
}
func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL) {
print(#function)
}
}
extension MyNetworkManager: URLSessionTaskDelegate {
func urlSession(_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?) {
print(#function, error ?? "No error")
}
}
If you don’t conform to URLSessionDownloadDelegate, it won’t call URLSessionDownloadDelegate methods.
I'm trying to show a loading while a PDF ins't already been shown on the screen. The problem is, my loading always stops before the document is already rendered, sometimes it can't take 2 or 3 seconds and I need to know when the PDF is already rendered to stop the activyIndicator. IS IT possible using PDFKIT? My code:
class PDFViewController: URLSessionDownloadDelegate {
#IBOutlet weak var pdfView: PDFView!
var pdfDocument: PDFDocument!
override func viewDidLoad() {
super.viewDidLoad()
setupPDFView()
setupNavigationBar()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.startLoading()
}
override func viewDidAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadPDF()
}
private func loadPDF() {
DispatchQueue.main.async {
guard let url = URL(string: self.viewModel.pdfURL) else {
self.showAlert(with: self.viewModel.strings.invalidInvoice)
return
}
let urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
let downloadTask = urlSession.downloadTask(with: url)
downloadTask.resume()
}
}
private func setupPDFView() {
self.pdfView.displayMode = .singlePageContinuous
self.pdfView.autoScales = true
}
func startLoading() {
guard let window = UIApplication.shared.keyWindow,
!window.subviews.contains(where: { $0 is LoadingView }) else { return }
let loadingView = LoadingView(frame: window.bounds)
window.addSubview(loadingView)
Thread.performUIUpdate {
loadingView.startAnimation()
}
}
func stopLoading() {
guard let window = UIApplication.shared.keyWindow,
let view = window.subviews.first(where: { $0 is LoadingView }),
let loadingView = view as? LoadingView else { return }
Thread.performUIUpdate {
loadingView.stopAnimation()
loadingView.removeFromSuperview()
}
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
self.pdfDocument = PDFDocument(url: location)
if let pdfDocument = self.pdfDocument {
self.pdfView.document = pdfDocument
}
self?.stopLoading()
}
}
}
This seems like a really bad idea:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
DispatchQueue.main.async {
self.pdfDocument = PDFDocument(url: location)
The problem is that while you are getting off the current thread and the asynchronous code starts, the method finishes and the temporary document at location can be destroyed. I would suggest writing like this:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let pdf = PDFDocument(url: location)
DispatchQueue.main.async {
self.pdfDocument = pdf
I'm not saying that that will solve your issue, but it seems a lot less dangerous than what you're doing.
As for your actual issue, I would suggest registering for a notification such as this one and see whether it arrives at the right moment.
You have to observe the PDFViewVisiblePagesChanged notification to know when a PDFView pages are visible, add your code in the method.
NotificationCenter.default.addObserver(self, selector: #selector(pdfViewVisiblePagesChanged(sender:)), name: .PDFViewVisiblePagesChanged, object: nil)
#objc func pdfViewVisiblePagesChanged(sender: Notification) {
//Add your code here
}
I wrote two applications, first using transferUserInfo, which caused too much lag (I believe because it sends stuff in background). I switched to sendMessage and was very happy with the results (faster response time). When attempted to run the application on my real iPhone and Apple Watch, I received Transfer timed out. Here's the full code and example of one of the debugs:
iPhone:
// ViewController.swift
import UIKit
import Foundation
import WatchConnectivity
class WatchManager: UIViewController, WCSessionDelegate {
var counter = 0
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
override func viewDidLoad(){
super.viewDidLoad()
watchSession = WCSession.default
}
private func sendDict(_ dict: [String: Any]) {
self.watchSession?.sendMessage(dict, replyHandler: nil, errorHandler: {error in print(error.localizedDescription)})
}
#IBOutlet weak var transferButton: UIButton!
#IBOutlet weak var label: UILabel!
#IBAction func dataTransfer(_ sender: Any) {
sendDict(["DataKey": counter])
counter+=1
print("sent")
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
public func sessionDidBecomeInactive(_ session: WCSession) {
print("session did become inactive")
}
public func sessionDidDeactivate(_ session: WCSession) {
print("session did deactivate")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("phone received app context: ", message)
if let temperature = message["DataKey"] as? String {
DispatchQueue.main.async {
self.transferButton.setTitle(temperature, for: .normal)
self.label.text=temperature
}
}
}
}
Apple Watch:
// InterfaceController.swift
import WatchKit
import Foundation
import WatchConnectivity
class InterfaceController: WKInterfaceController {
var watchSession: WCSession? {
didSet {
if let session = watchSession {
session.delegate = self
session.activate()
}
}
}
#IBOutlet weak var temperatureLabel: WKInterfaceButton!
private func sendDict(_ dict: [String: Any]) {
self.watchSession?.sendMessage(dict, replyHandler: nil, errorHandler: {error in print(error.localizedDescription)})
}
#IBAction func button() {
let urg = ["DataKey":UUID().uuidString]
sendDict(urg)
print("watch sent app context \(urg)")
}
}
extension InterfaceController: WCSessionDelegate {
#if os(iOS)
public func sessionDidBecomeInactive(_ session: WCSession) { }
public func sessionDidDeactivate(_ session: WCSession) {
session.activate()
}
#endif
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
print("Session activation did complete")
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("watch received app context: ", message)
if let temperature = message["DataKey"] as? Int {
self.temperatureLabel.setTitle(String(temperature))
}
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
watchSession = WCSession.default
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
Debug Example:
Session activation did complete
watch sent app context ["DataKey": "AF793FC6-7A16-4D7D-9A3B-D3BB960EC9D9"]
2019-01-13 21:07:43.524717-0800 testApp WatchKit Extension[1240:1178401] [WC] -[WCSession onqueue_handleMessageCompletionWithError:withMessageID:] C385FF5F-5EA1-478B-A930-54066C2F0B0F due to WCErrorCodeTransferTimedOut -> IDSErrorTypeTimedOut -> IDSResponseTimedOut
2019-01-13 21:07:43.525001-0800 testApp WatchKit Extension[1240:1178401] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] C385FF5F-5EA1-478B-A930-54066C2F0B0F errorHandler: YES with WCErrorCodeTransferTimedOut -> IDSErrorTypeTimedOut -> IDSResponseTimedOut
Transfer timed out.
I want to implement downloading functionality which can show completed status of downloading task with the percentage. And I'm able to do that but the problem is when the app is moving to the background and come back to the foreground at that time the delegate method didWriteData is not called in iOS12. Can anyone please help me? Here is my code
protocol DownloadDelagate {
func downloadingProgress(value:Float)
func downloadCompleted(identifier: Int,url: URL)
}
class DownloadManager : NSObject, URLSessionDelegate, URLSessionDownloadDelegate {
static var shared = DownloadManager()
var delegate: DownloadDelagate?
var backgroundSessionCompletionHandler: (() -> Void)?
var session : URLSession {
get {
let config = URLSessionConfiguration.background(withIdentifier: "\(Bundle.main.bundleIdentifier!).background")
config.isDiscretionary = true
config.sessionSendsLaunchEvents = true
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) {
delegate?.downloadCompleted(identifier: downloadTask.taskIdentifier, url: location)
}
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
delegate?.downloadingProgress(value: progressPercentage)
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.")
}
}
}
Based on this thread this is a bug in NSURLSesstion. Currently there are known workaround for this (approved by Apple Engineers):
var session: URLSession?
...
func applicationDidBecomeActive(_ application: UIApplication) {
session?.getAllTasks { tasks in
tasks.first?.resume() // It is enough to call resume() on only one task
// If it didn't work, you can try to resume all
// tasks.forEach { $0.resume() }
}
}
Please try your code in AppDelegate's applicationWillEnterForeground(). You can make changes here when the app makes transition from Background to Active state.
I have a simple struct where I use NSURLSession to get the data from the web. I would like to extend it to provide progress of downloading. I cannot use struct as a NSURLSession delegate because it needs to be a NSObject so I have created simple class where I pass to NSURLSession delegate. This is short version of my code:
struct Resource {
private var progressDelegate: ProgressSessionDelegate?
private var session: NSURLSession!
init() {
self.progressDelegate = ProgressSessionDelegate()
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
session = NSURLSession(configuration: configuration, delegate: self.progressDelegate!, delegateQueue: nil)
}
func loadAsynchronous(callback: A? -> ()) {
session.dataTaskWithURL(resourceURL) {
data, response, error in
let json = data.flatMap {
try? NSJSONSerialization.JSONObjectWithData($0, options: NSJSONReadingOptions())
}
callback(mycalback)
}.resume()
}
}
and thats my cass which should handle the delegate methods:
class ProgressSessionDelegate: NSObject, NSURLSessionDelegate, NSURLSessionDataDelegate {
var expectedContentLength = 0
var currentLength = 0
override init() {
super.init()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
expectedContentLength = Int(response.expectedContentLength)
print("0: \(expectedContentLength)")
completionHandler(NSURLSessionResponseDisposition.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.currentLength += data.length
let percentageDownloaded = Float(self.currentLength) / Float(self.expectedContentLength)
print("1: \(percentageDownloaded)")
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
print("Downloaded")
}
}
The problem is that the delegate methods are not called.
When I move the code from Resource and ProgressSessionDelegate struct to my ViewController and set up ViewController as a NSURLSession delegate all is working fine. I think the problem is in a way how the class is stored is struct.
Any help would be most appreciated.