Preventing URLSession redirect in Swift - ios

I need to fetch a redirecting URL but prevent redirection in Swift. From other posts and Apple docs I understand I must implement the delegate method URLSession(session:, task:, willPerformHTTPRedirection response:, request:, completionHandler:) and return nil via the completion closure. But I can't find examples in swift, nor figure out the right way to do it. The code below reproduces my issue in playground: the delegate does not seem to get executed.
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
class MySession: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
// trying to follow instructions at https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSessionTaskDelegate_protocol/index.html#//apple_ref/occ/intfm/NSURLSessionTaskDelegate/URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:
// to prevent redirection -- DOES NOT SEEM TO GET CALLED
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
println("in URLSession delegate") // NEVER PRINTS
completionHandler(nil) // NO EFFECT
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, success: (response: String!) -> Void) {
var session = NSURLSession.sharedSession()
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html><head><title>Object moved</title>", options: .RegularExpressionSearch) != nil {
println("success: redirection was prevented") // SHOULD PRINT THIS
} else {
println("failure: redirection went through") // INSTEAD PRINTS THIS
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer") // ex. redirecting link
Please be gentle, I am a newbie. Thank you in advance for any assistance!
UPDATE: With many thanks to #nate I got it to work. The key insight is that in order for the delegate to be called, one must pass the delegate class to the NSURLSession() initializer, rather than using NSURLSession.sharedSession(). Passing nil as the delegate yields the customary behavior (with redirection). Here is working version of the code:
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(continueIndefinitely: true)
class MySession: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate {
// to prevent redirection
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(nil)
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool, success: (response: String!) -> Void) {
var myDelegate: MySession? = nil
if noRedirect {
myDelegate = MySession()
}
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url, noRedirect: true) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html>\n<head><title>Bitly</title>", options: .RegularExpressionSearch) != nil {
println("success: redirection was prevented")
} else {
println("failure: redirection went through")
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer")

You have two things standing in your way with your current implementation.
You never set the delegate property on the NSURLSession instance that you're using to make the request. Without the delegate property set, your delegate methods won't ever be called. Instead of getting NSURLSession.sharedSession(), look at the NSURLSession(configuration:delegate:delegateQueue:) initializer. The first and last parameters can be NSURLSessionConfiguration.defaultSessionConfiguration() and nil, respectively, see below for more about the delegate.
Note that when you use the variant of session.dataTaskWithURL that has a completion handler, delegate methods that handle response and data delivery will be ignored, but authentication and redirection handlers are still used.
You'll have to refactor somewhat to use MySession as a delegate, since you're using class methods to make the request. You need an instance to use as the session's delegate.
I took a short and incomplete route to having the delegate pick up on the redirect with this alternate code—you'll need to refactor as in #3 to make sure you can still call your callback:
class func getDataFromServerWithSuccess(myURL: String, success: (response: String!) -> Void) {
let delegate = MySession()
var session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: delegate, delegateQueue: nil)
let task = session.dataTaskWithURL(NSURL(string: myURL)!) {
// ...
}
task.resume()
}
Hope that helps!

I also found the solution helpful, but I am using Swift 4.2 in my current project.
So, here is an adapted shorter version of the solution above, that also works with Swift 4.2 and Xcode 10.
import Foundation
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
class MySession: NSObject, URLSessionTaskDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: #escaping (URLRequest?) -> Void) {
completionHandler(nil)
}
}
func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool) {
let myDelegate: MySession? = noRedirect ? MySession() : nil
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTask(with: URL(string:myURL)!) { (data, response, error) in
// OMITTING ERROR CHECKING FOR BREVITY
if let data = data {
if let dataString = String(bytes: data, encoding: .utf8) {
print(dataString)
if dataString.contains("Bitly") == true {
print("success: redirection was prevented")
} else {
print("failure: redirection went through")
}
}
}
}
loadDataTask.resume()
}
getDataFromServerWithSuccess(myURL: "http://bitly.com/filmenczer", noRedirect: true)

Minor adjustments to the solutions proposed above. This works with Swift 2 in XCode 7.
import UIKit
import Foundation
import XCPlayground
XCPSetExecutionShouldContinueIndefinitely(true)
class MySession: NSObject, NSURLSessionDelegate {
// to prevent redirection
func URLSession(session: NSURLSession, task: NSURLSessionTask, willPerformHTTPRedirection response: NSHTTPURLResponse, newRequest request: NSURLRequest, completionHandler: (NSURLRequest!) -> Void) {
completionHandler(nil)
}
// fetch data from URL with NSURLSession
class func getDataFromServerWithSuccess(myURL: String, noRedirect: Bool, success: (response: String!) -> Void) {
var myDelegate: MySession? = nil
if noRedirect {
myDelegate = MySession()
}
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: myDelegate, delegateQueue: nil)
let loadDataTask = session.dataTaskWithURL(NSURL(string: myURL)!) { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
// OMITTING ERROR CHECKING FOR BREVITY
success(response: NSString(data: data!, encoding: NSASCIIStringEncoding) as! String)
}
loadDataTask.resume()
}
// extract data from redirect
class func getRedirectionInfo(url: String) {
getDataFromServerWithSuccess(url, noRedirect: true) {(data) -> Void in
if let html = data {
if html.rangeOfString("<html>\n<head><title>Bitly</title>", options: .RegularExpressionSearch) != nil {
print("success: redirection was prevented")
} else {
print("failure: redirection went through")
}
}
}
}
}
MySession.getRedirectionInfo("http://bit.ly/filmenczer")

Related

How can I chain completion handlers if one method also has a return value?

I have 2 methods I need to call, the second method must be executed using the result of the first method and the second method also returns a value.
I have put together a simple playground that demonstrates a simple version of the flow
import UIKit
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
completion("some.access.token")
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> URLSessionTask {
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
completion(.failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
completion(.success((data, response)))
}
})
task.resume()
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
I need to use the token returned by AuthTokenLoader as a header on the request sent by dispatch method in my Networking class.
Networking also returns a task so this request can be cancelled.
As I cannot return from inside the completion block of the AuthTokenLoader load completion, I unsure how to achieve this.
You can create a wrapper for your task and return that instead.
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
Your entire playground would become
protocol TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void)
}
protocol Client {
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task
}
class AuthTokenLoader: TokenLoader {
func load(_ key: String, completion: #escaping (String?) -> Void) {
print("was called")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
completion("some.access.token")
}
}
}
protocol Task {
func cancel()
}
class URLSessionTaskWrapper: Task {
private var completion: ((Result<(Data, HTTPURLResponse), Error>) -> Void)?
var wrapped: URLSessionTask?
init(_ completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) {
self.completion = completion
}
func complete(with result: Result<(Data, HTTPURLResponse), Error>) {
completion?(result)
}
func cancel() {
preventFurtherCompletions()
wrapped?.cancel()
}
private func preventFurtherCompletions() {
completion = nil
}
}
class Networking: Client {
private let loader: TokenLoader
init(loader: TokenLoader) {
self.loader = loader
}
func dispatch(_ request: URLRequest, completion: #escaping (Result<(Data, HTTPURLResponse), Error>) -> Void) -> Task {
let task = URLSessionTaskWrapper(completion)
loader.load("token") { token in
task.wrapped = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
if let error = error {
task.complete(with: .failure(error))
} else if let data = data, let response = response as? HTTPURLResponse {
task.complete(with: .success((data, response)))
}
})
task.wrapped?.resume()
}
return task
}
}
let loader = AuthTokenLoader()
let client = Networking(loader: loader)
let request = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
client.dispatch(.init(url: request), completion: { print($0) })
Turns out it was harder to do in Combine than I thought. Like most people, I'm still quite new to this. Would happily accept edits from people who know better :)
The general principle is that, instead of taking a completion block, your functions should return a Publisher, that you can then choose to do things with, and chain together.
So your token loader can look like this:
protocol TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error>
}
Instead of taking a completion block, you now return a publisher which will send you a string on success, or an error if something goes wrong.
And your implementation, well I wasn't sure what you were planning on doing in there but here's an example of sorts:
class AuthTokenLoader: TokenLoader {
func load(_ key: String) -> AnyPublisher<String, Error> {
print("was called")
// Do async stuff to create your token, ending in a result
let result = Result<String, Error>.success("some.access.token")
return result.publisher.eraseToAnyPublisher()
}
}
Your client can look like this:
protocol Client {
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error>
}
Now this is the complicated bit. What you want to do is take the publisher from your token loader, and when it gives a result, make your URL request, and then make another publisher from that URL request. URLSession can give you a publisher for a data task, and there is a flatMap operator which is supposed to allow you to turn the results of one publisher into a new publisher, but you get stuck in the weeds of the type system so the code is uglier than it ought to be:
func dispatch(_ request: URLRequest) -> AnyPublisher<Data, Error> {
return loader.load("someKey")
.flatMap {
token -> AnyPublisher<Data, Error> in
var finalRequest = request
finalRequest.setValue(token, forHTTPHeaderField: "x-token")
return URLSession.shared.dataTaskPublisher(for: finalRequest)
.map { $0.data }
.mapError { $0 as Error }
.eraseToAnyPublisher()
}.eraseToAnyPublisher()
}
You'd use this code like so:
let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
let sub = client.dispatch(request)
.sink(receiveCompletion: {
switch $0 {
case .finished: print("Done")
case .failure(let error): print("Error :\(error)")
}
}, receiveValue: { data in
print("Data: \(data)")
})
sub is an AnyCancellable, so if it is deallocated or you call cancel on it, this passes back up the chain and it will cancel the URL task for you.
If you want to do things with the data then there are operators for mapping or decoding or whatever which makes the whole thing very nice to work with.

How to mock URL struct?

I am writing some code to be able to make testing easier. After researching, I found a good way of making URLSession testable is to make it conform to a protocol and use that protocol in the class I need to test. I applied the same method to URL. However, I now need to downcast the url parameter as a URL type. This seems a bit "dangerous" to me. What is the proper way to make URL testable? How can I also mock the URLSessionDataTask type returned by dataTask?
import Foundation
protocol URLSessionForApiRequest {
func dataTask(with url: URL, completionHandler: #escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
}
protocol URLForApiRequest {
init?(string: String)
}
extension URLSession: URLSessionForApiRequest {}
extension URL: URLForApiRequest {}
class ApiRequest {
class func makeRequest(url: URLForApiRequest, urlSession: URLSessionForApiRequest) {
guard let url = url as? URL else { return }
let task = urlSession.dataTask(with: url) { _, _, _ in print("DONE") }
task.resume()
}
}
You want to make a protocol that wraps the function that you are going to call, then make a concrete implementation and mock implementation that returns whatever you initialize it with. Here is an example:
import UIKit
import PlaygroundSupport
protocol RequestProvider {
func request(from: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void)
}
class ApiRequest: RequestProvider {
func request(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume
}
}
class MockApiRequest: RequestProvider {
enum Response {
case value(Data, URLResponse), error(Error)
}
private let response: Response
init(response: Response) {
self.response = response
}
func request(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> Void) {
switch response {
case .value(let data, let response):
completion(data, response, nil)
case .error(let error):
completion(nil, nil, error)
}
}
}
class SomeClassThatMakesAnAPIRequest {
private let requestProvider: RequestProvider
init(requestProvider: RequestProvider) {
self.requestProvider = requestProvider
}
//Use the requestProvider here and it uses either the Mock or the "real" API provider based don what you injected
}

Cannot call value of non-function type 'URLSession' error with mockURLSession

I'm trying to unit test URLSession delegates with mockData. This is the delegate function that is being tested:
urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
This is the unit test so far:
func test_urlSession(){
let mockSession = MockURLSession(mockResponse: MockURLSession().successHttpURLResponse(request: self.urlRequest!) as! HTTPURLResponse)
//error here
sut?.urlSession(mockSession, task: MockURLSessionDataTask, didCompleteWithError: Error)
}
Whenever I try to inject the mockURLSession as a parameter the error:
Cannot call value of non-function type 'URLSession'
I'm testing for responses (ie 404, 200) that's why I'm injecting the mockURLSession with mocked responses. Any idea on how to inject the mockUrlSession into the delegate?
Edit___
protocol URLSessionDataTaskProtocol {
func resume()
}
protocol URLSessionProtocol {
typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
func dataTask(with request: Request, completionHandler: #escaping DataTaskResult) -> URLSessionDataTaskProtocol
}
extension URLSession: URLSessionProtocol{
func dataTask(with request: Request, completionHandler: #escaping URLSessionProtocol.DataTaskResult) -> URLSessionDataTaskProtocol {
let task:URLSessionDataTask = dataTask(with: request, completionHandler: {
(data:Data?, response:URLResponse?, error:Error?) in completionHandler(data,response,error) }) as! URLSessionDataTask;
return task as URLSessionDataTaskProtocol
}
}
extension URLSessionDataTask: URLSessionDataTaskProtocol {}
class MockURLSession: URLSessionProtocol {
typealias DataTaskResult = (Data?, URLResponse?, Error?) -> Void
var nextDataTask = MockURLSessionDataTask()
var nextData: Data?
var nextError: Error?
private (set) var lastURL: URL?
private var mockResponse: HTTPURLResponse?
init() { }
init(mockResponse: HTTPURLResponse) {
self.mockResponse = mockResponse
}
func successHttpURLResponse(request: Request) -> URLResponse {
return HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: nil)!
}
func wrongHttpURLResponse(request: Request, statusCode:Int) -> URLResponse {
return HTTPURLResponse(url: request.url!, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: nil)!
}
func dataTask(with request: Request, completionHandler: #escaping DataTaskResult) -> URLSessionDataTaskProtocol {
lastURL = request.url
nextDataTask.resume()
if let mockResponse = mockResponse {
completionHandler(nextData, mockResponse, nextError)
}
else {
//default case is success
completionHandler(nextData, successHttpURLResponse(request: request), nextError)
}
return nextDataTask
}
}
class MockURLSessionDataTask: URLSessionDataTaskProtocol {
private (set) var resumeWasCalled = false
func resume() {
resumeWasCalled = true
}
The signature of the delegate method is
urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
So the first argument, session, must be of type URLSession.
When you can control the signature of a function, then you can say, "Let's not use URLSession. Let's use URLSessionProtocol instead. Then we can substitute any type that conforms to that protocol."
But that's not the true for this case. It has to be an URLSession.
The workaround is to use partial mocking. Make a test double that inherits from URLSession. I would say, "Change MockURLSession's base class," but I don't know if you're using it in other tests. You may want to create a new test double to test the delegate method.
For more on partial mocking, see https://qualitycoding.org/swift-partial-mock/

Custom NSURLProtocol with NSURLSession

I'm trying to implement this tutorial which implements a custom NSURLProtocol with NSURLConnection.
https://www.raywenderlich.com/76735/using-nsurlprotocol-swift
It works as expected, but now that NSURLConnection is deprecated in iOS9, I'm trying to convert it to NSURLSession.
Unfortunatly it didn't work.
I'm loading a website in uiwebview, if I use NSURLConnection it loads and everything work as expected, all http requests from the webview is captured, but not when using NSURLSession.
Any help is appreciated.
here is my code
import UIKit
class MyProtocol: NSURLProtocol, NSURLSessionDataDelegate, NSURLSessionTaskDelegate, NSURLSessionDelegate {
//var connection: NSURLConnection!
var mutableData: NSMutableData!
var response: NSURLResponse!
var dataSession: NSURLSessionDataTask!
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
if NSURLProtocol.propertyForKey("MyURLProtocolHandledKey", inRequest: request) != nil {
return false
}
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override class func requestIsCacheEquivalent(aRequest: NSURLRequest,
toRequest bRequest: NSURLRequest) -> Bool {
return super.requestIsCacheEquivalent(aRequest, toRequest:bRequest)
}
override func startLoading() {
let newRequest = self.request.mutableCopy() as! NSMutableURLRequest
NSURLProtocol.setProperty(true, forKey: "MyURLProtocolHandledKey", inRequest: newRequest)
self.dataSession = NSURLSession.sharedSession().dataTaskWithRequest(newRequest)
dataSession.resume()
self.mutableData = NSMutableData()
}
override func stopLoading() {
print("Data task stop")
self.dataSession.cancel()
self.mutableData = nil
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveResponse response: NSURLResponse, completionHandler: (NSURLSessionResponseDisposition) -> Void) {
self.response = response
self.mutableData = NSMutableData()
print(mutableData)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.client?.URLProtocol(self, didLoadData: data)
self.mutableData.appendData(data)
}
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if (error == nil)
{
self.client!.URLProtocolDidFinishLoading(self)
self.saveCachedResponse()
}
else
{
self.client?.URLProtocol(self, didFailWithError: error!)
}
}
func saveCachedResponse () {
let timeStamp = NSDate()
let urlString = self.request.URL?.absoluteString
let dataString = NSString(data: self.mutableData, encoding: NSUTF8StringEncoding) as NSString?
print("TiemStamp:\(timeStamp)\nURL: \(urlString)\n\nDATA:\(dataString)\n\n")
}
}
I've solved it.
Here is the code if anyone needs it.
import Foundation
class MyProtocol1: NSURLProtocol, NSURLSessionDataDelegate, NSURLSessionTaskDelegate
{
private var dataTask:NSURLSessionDataTask?
private var urlResponse:NSURLResponse?
private var receivedData:NSMutableData?
class var CustomKey:String {
return "myCustomKey"
}
// MARK: NSURLProtocol
override class func canInitWithRequest(request: NSURLRequest) -> Bool {
if (NSURLProtocol.propertyForKey(MyProtocol1.CustomKey, inRequest: request) != nil) {
return false
}
return true
}
override class func canonicalRequestForRequest(request: NSURLRequest) -> NSURLRequest {
return request
}
override func startLoading() {
let newRequest = self.request.mutableCopy() as! NSMutableURLRequest
NSURLProtocol.setProperty("true", forKey: MyProtocol1.CustomKey, inRequest: newRequest)
let defaultConfigObj = NSURLSessionConfiguration.defaultSessionConfiguration()
let defaultSession = NSURLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)
self.dataTask = defaultSession.dataTaskWithRequest(newRequest)
self.dataTask!.resume()
}
override func stopLoading() {
self.dataTask?.cancel()
self.dataTask = nil
self.receivedData = nil
self.urlResponse = nil
}
// MARK: NSURLSessionDataDelegate
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask,
didReceiveResponse response: NSURLResponse,
completionHandler: (NSURLSessionResponseDisposition) -> Void) {
self.client?.URLProtocol(self, didReceiveResponse: response, cacheStoragePolicy: .NotAllowed)
self.urlResponse = response
self.receivedData = NSMutableData()
completionHandler(.Allow)
}
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
self.client?.URLProtocol(self, didLoadData: data)
self.receivedData?.appendData(data)
}
// MARK: NSURLSessionTaskDelegate
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
if error != nil && error!.code != NSURLErrorCancelled {
self.client?.URLProtocol(self, didFailWithError: error!)
} else {
saveCachedResponse()
self.client?.URLProtocolDidFinishLoading(self)
}
}
// MARK: Private methods
/**
Do whatever with the data here
*/
func saveCachedResponse () {
let timeStamp = NSDate()
let urlString = self.request.URL?.absoluteString
let dataString = NSString(data: self.receivedData!, encoding: NSUTF8StringEncoding) as NSString?
print("TimeStamp:\(timeStamp)\nURL: \(urlString)\n\nDATA:\(dataString)\n\n")
}
}
Swift 3 version:
// CustomURLProtocol.swift
class CustomURLProtocol: URLProtocol, URLSessionDataDelegate, URLSessionTaskDelegate {
private var dataTask: URLSessionDataTask?
private var urlResponse: URLResponse?
private var receivedData: NSMutableData?
class var CustomHeaderSet: String {
return "CustomHeaderSet"
}
// MARK: NSURLProtocol
override class func canInit(with request: URLRequest) -> Bool {
guard let host = request.url?.host, host == "your domain.com" else {
return false
}
if (URLProtocol.property(forKey: CustomURLProtocol.CustomHeaderSet, in: request as URLRequest) != nil) {
return false
}
return true
}
override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}
override func startLoading() {
let mutableRequest = NSMutableURLRequest.init(url: self.request.url!, cachePolicy: NSURLRequest.CachePolicy.useProtocolCachePolicy, timeoutInterval: 240.0)//self.request as! NSMutableURLRequest
//Add User Agent
var userAgentValueString = "myApp"
mutableRequest.setValue(userAgentValueString, forHTTPHeaderField: "User-Agent")
print(mutableRequest.allHTTPHeaderFields ?? "")
URLProtocol.setProperty("true", forKey: CustomURLProtocol.CustomHeaderSet, in: mutableRequest)
let defaultConfigObj = URLSessionConfiguration.default
let defaultSession = URLSession(configuration: defaultConfigObj, delegate: self, delegateQueue: nil)
self.dataTask = defaultSession.dataTask(with: mutableRequest as URLRequest)
self.dataTask!.resume()
}
override func stopLoading() {
self.dataTask?.cancel()
self.dataTask = nil
self.receivedData = nil
self.urlResponse = nil
}
// MARK: NSURLSessionDataDelegate
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: #escaping (URLSession.ResponseDisposition) -> Void) {
self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
self.urlResponse = response
self.receivedData = NSMutableData()
completionHandler(.allow)
}
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.client?.urlProtocol(self, didLoad: data as Data)
self.receivedData?.append(data as Data)
}
// MARK: NSURLSessionTaskDelegate
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if error != nil { //&& error.code != NSURLErrorCancelled {
self.client?.urlProtocol(self, didFailWithError: error!)
} else {
//saveCachedResponse()
self.client?.urlProtocolDidFinishLoading(self)
}
}
}
The problem you are having with your code is that you are using the the NSURLSession.sharedSession to contain your data task. By using the shared session, you are not able to change the session delegate so none of your delegate routines are going to be invoked.
You will need to create a custom session with your protocol established as the delegate for the session. Then, when asked to start loading you can create a data task in that session.
From the documentation of URLSession:
Important
The session object keeps a strong reference to the delegate until your app exits or explicitly invalidates the session. If you don’t invalidate the session, your app leaks memory until it exits.
Also:
Note
Be careful to not create more sessions than you need. For example, if you have several parts of your app that need a similarly configured session, create one session and share it among them.
So I would move the creation of the URLSession from the startLoading method to the URLProtocol subclass initializer:
class MyURLProtocol: URLProtocol, URLSessionDataDelegate,URLSessionTaskDelegate {
override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
super.init(request: request, cachedResponse: cachedResponse, client: client)
defaultSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
}
private var defaultSession: URLSession?

NSURLConnection Delegate Methods Not Called In Generic Class

I have a test class that tries to reach to google with a NSURLConnection. If I try to make it generic, the NSURLConnectionDataDelegate methods are never called.
class Remote<T: NSObject>: NSObject, NSURLConnectionDelegate, NSURLConnectionDataDelegate {
//class Remote: NSObject, NSURLConnectionDelegate, NSURLConnectionDataDelegate {
var data = NSMutableData()
func connect(query:NSString) {
var url = NSURL(string:"http://www.google.com")!
var request = NSURLRequest(URL: url)
var conn = NSURLConnection(request: request, delegate: self, startImmediately: true)
}
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
LF.log("didReceiveResponse")
}
func connection(connection: NSURLConnection!, didReceiveData conData: NSData!) {
LF.log("didReceiveData")
self.data.appendData(conData)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
LF.log("didFinished")
//println(self.data)
}
deinit {
println("deiniting")
}
}
To test it (comment/uncomment the first/second line to compare):
let remote = Remote<NSObject>()
//let remote = Remote()
remote.connect("")
Any idea please?
Update1: to answer comment 1, it's a REST client that handles network connection and parsing for you. I'd write a blog about this later (since it's still under development), but to give you the idea here's some demo code from my project:
let client = ICRestClient<ICCategoryModel>(api:IC.api.category_list)
client.func_array = {
(results: [ICCategoryModel]?, error: NSError?) -> Void in
block!(results, error)
}
client.execute()
And ICCategoryModel is like:
class ICSubCategoryModel: ICModel {
var name: String?
var category_id: Int = 0
}
The idea is that you pass the API URL in, you get an array (or error) with some reflected objects instead of Dictionary. It's from my LSwift library and supports various authentications methods (buildin-parameters, cookie, header, authentication challenge etc.)
One of the problem is that I can't access the delegate object of NSURLConnection. I came up with a solution by creating another RemoteDelegate class, which is not generic type, and set it as the delegate of "conn". It works for now but it's just a work-around, and I'm still looking for the answer to the question.
My delegate class:
class LRestConnectionDelegate: NSObject {
var func_done: ((NSURLResponse?, NSData!, NSError!) -> Void)?
var credential: NSURLCredential?
var response: NSURLResponse?
var data: NSMutableData = NSMutableData()
func connection(connection: NSURLConnection, willSendRequestForAuthenticationChallenge challenge: NSURLAuthenticationChallenge) {
if challenge.previousFailureCount > 0 {
challenge.sender.cancelAuthenticationChallenge(challenge)
} else if let credential = credential {
challenge.sender.useCredential(credential, forAuthenticationChallenge:challenge)
} else {
LF.log("REST connection will challenge", connection)
}
}
func connection(connection: NSURLConnection, didReceiveResponse a_response: NSURLResponse) {
//LF.log("CONNECTION response", response)
response = a_response
}
func connection(connection: NSURLConnection, didReceiveData data_received: NSData) {
//LF.log("CONNECTION data", data.length)
data.appendData(data_received)
}
func connectionDidFinishLoading(connection: NSURLConnection) {
//LF.log("CONNECTION finished", connection)
if func_done != nil {
func_done!(response, data, nil)
}
}
func connection(connection: NSURLConnection, didFailWithError error: NSError) {
//LF.log("CONNECTION failed", error)
if let func_done = func_done {
func_done(response, nil, error)
}
}
deinit {
//LF.log("DELEGATE deinit", self)
}
}
And this works in class LRestClient<T: LFModel>:
let delegate = LRestConnectionDelegate()
delegate.credential = credential
delegate.func_done = func_done
connection = NSURLConnection(request:request, delegate:delegate, startImmediately:true)
Because you're using NSURLConnection synchronously, you have to schedule the operation in the main run loop. Add the following code to the end of your connect function:
conn?.scheduleInRunLoop(NSRunLoop.currentRunLoop, forMode: NSDefaultRunLoopMode)
Alternatively, set startImmediately to NO and call conn?.start().

Resources