I'm having a problem detecting when data is being received using NSURLSession. The equivalent code with NSURLConnection does work, but that's not included here.
In this example, I'm doing a request to google.com. The completionHandler works and "complete" is printed (also the data, etc if you change the code).
However didReceiveData isn't triggered and "received data" is never printed.
I've been through the docs and done a ton of searching and I think this looks right, but I can't seem to get it to work. Definitely would appreciate any help with this.
(I need to use didReceiveData because I'm going to parsing a streaming json api.)
Thanks!
import UIKit
class ViewController: UIViewController, NSURLSessionDelegate, NSURLSessionDataDelegate, NSURLSessionTaskDelegate {
override func viewDidAppear(animated: Bool) {
let session = NSURLSession.sharedSession()
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!, completionHandler: { (data, response, error) -> Void in
print("complete")
})
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("received data")
}
}
There were two issues.
When the session is created, you must define a delegate. That was the main reason didReceiveData wasn't being called.
The second issue is that if you use a completionHandler block, then all the delegates functions are bypassed. In the code for NSURlSession, it says
extension NSURLSession {
/*
* data task convenience methods. These methods create tasks that
* bypass the normal delegate calls for response and data delivery,
* and provide a simple cancelable asynchronous interface to receiving
* data. Errors will be returned in the NSURLErrorDomain,
* see <Foundation/NSURLError.h>. The delegate, if any, will still be
* called for authentication challenges.
*/
You must implement each delegate function you need to check for completion, errors, etc.
The updated code is below:
import UIKit
class ViewController: UIViewController, NSURLSessionDelegate {
override func viewDidAppear(animated: Bool) {
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!)
task.resume()
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
print("received data")
}
}
You can get data in completionHandler only. Why do you want to use didReceiveData?
Below code will show you how you can get the received data
override func viewDidAppear(animated: Bool) {
let session = NSURLSession.sharedSession()
var task = session.dataTaskWithURL(NSURL(string: "https://google.com")!, completionHandler: { (data, response, error) -> Void in
if NSJSONSerialization.isValidJSONObject(data){
if let jsonParam = try? NSJSONSerialization.dataWithJSONObject(dictData, options: []){
print("Result Data : \(jsonParam)")
}
}
})
task.resume()
}
Related
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)
In the Combine framework, I have found following text
The Combine framework provides a declarative approach for how your app
processes events. Rather than potentially implementing multiple
delegate callbacks or completion handler
Can somebody tell me what is the difference between completion handler and callback in Swift?
A delegate callback is when you have a delegate that you know in advance implements a method (e.g. because it adopts a protocol), and you call that method by name.
A completion handler is when someone hands you a function and you just call it blindly by reference.
to be clear actually you can achieve the same functionality with both ways however the there are completely different approach for designing your app
let me clarify with simple example the difference between both with the same function is making network call
delegate protocol
// enum to define the request type
enum RequestTypes {
case UserRegister
case UserLogin
}
protocol ServiceDelegate {
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes)
}
// you can also add default impl to the methods here
extension ServiceDelegate {
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes){}
}
class BaseService<ResponseModel: Codable> {
var session: URLSession!
var delegate: ServiceDelegate?
// MARK: Rebuilt Methods
func FireRequest(){
// Request Preparation
let serviceUrl = URL(string: /* your url */)!
var request = URLRequest(url: serviceUrl)
request.httpMethod = "GET"
// Firing the request
session = URLSession.init(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {/* handle error or call delegate error method here */ return }
delegate?.didCompleteRequest(responseModel: object, tag: .UserLogin)
}
}
}.resume()
}
}
class ViewController: UIViewController, ServiceDelegate {
override func viewDidLoad() {
super.viewDidLoad()
fetchNewData()
}
func fetchNewData(){
let service = BaseService<YourModel>()
service.delegate = self
service.FireRequest()
}
func didCompleteRequest(responseModel: AnyObject, tag: RequestTypes) {
if tag == /* the tag you are waiting */ .UserLogin {
// YourModel is available here
}
}
}
completion handler
class BaseService<ResponseModel: Codable> {
var session: URLSession!
// MARK: Rebuilt Methods
func FireRequest(completion: ((ResponseModel?) -> Void)?){
// Request Preparation
let serviceUrl = URL(string: /* your url */)!
var request = URLRequest(url: serviceUrl)
request.httpMethod = "GET"
// Firing the request
session = URLSession.init(configuration: URLSessionConfiguration.default)
session.dataTask(with: request) { (data, response, error) in
if let data = data {
do {
guard let object = try? JSONDecoder().decode(ResponseModel.self , from: data) else {/* handle error or call delegate error method here */ return }
DispatchQueue.main.async {
completion?(object)
}
}
}
}.resume()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
fetchNewData()
}
func fetchNewData(){
let service = BaseService<YourModel>()
service.FireRequest(completion: { [weak self] (response) in
// yourModel Available here once the request completed
})
}
}
A delegate callback is one to one communication between various ViewControllers and classes. It basically lets you know that a particular change has been done in particular view or any where else and now you can make change after this action.
While completion handler is a block executed after completing a particular process or task.
Callback is a way to sending data back to some other function on some particular occasion. there are 2 ways to implement callbacks in swift.
Using Protocols / Delegate
Using Completion Handler
Using Protocols / Delegate Example:
Declare Protocol
protocol MyDelegate {
public method(param: String);
}
Your ViewController should extend the delegate
class YourViewController: MyDelegate {
// Your Other methods
func method(param: String) {
// Do your stuff
}
}
Now in your other classes you can send callback to ViewController through delegate object like
delegate.method(param: "your_param");
Using Completion Handler Example:
public func method(param: String, completionHandler: #escaping (_ param: String) -> Void)
{
...
// now you can send data back to the caller function using completionHandler on some particular occasion
completionHandler("param");
}
We can call this function like
method(param: String, completionHandler: { (result, alreadyUserId) in
// here you will receive callback
});
Callbacks and Completion Handlers are synonymous when referring to asynchronous methods.
I’ve found the main difference being in how its used in defining what’s returned to the caller where a callback is used when referring to a method where the scope is returned to the previous calling method and a completion handler refers to a method when it returns some Result type to the caller.
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.
I am using NSURLSession for API call. Once API gets hit, before getting response I am moving to another view controller. How to stop all NSURLSession running in the current view controller? Kindly guide me.
My Sample code is not working. If I move to another UIViewController, that new session creates and working fine but previous UIViewControllers's session resuming. How to stop that Session? Kindly guide me.
Sample Get API Cal:
var testSession = NSURLSession.sharedSession()
var testTask = NSURLSessionDataTask()
override func viewDidAppear(animated: Bool) {
testTask = testSession.dataTaskWithURL(NSURL(string: "http://httpbin.org/get")!, completionHandler: { (data, response, error) -> Void in
do{
let str = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! [String:AnyObject]
print(str)
}
catch {
print("json error: \(error)")
}
})
testTask.resume()
}
override func viewDidDisappear(animated: Bool) {
testTask.cancel()
testSession.finishTasksAndInvalidate()
}
Don't use the shared session. Use a real session that you create explicitly. Then call invalidateAndCancel on the session when you no longer need any of its pending requests.
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.