I'm having big problems in a project I'm currently working with.
I've been reading about URLSession various places but all of them seem to be outdated and refers to NSURLSession I thought that they would be fairly similar and they probably are but for a newbie like me I'm lost. what I do is not working and I do not like solutions I find because they all do their work in a controller..
http://swiftdeveloperblog.com/image-upload-with-progress-bar-example-in-swift/
this one for instance. I'm using the PHP script but wanted to make a networking layer I could invoke and use at will. but I'm lacking a good resource from where I could learn about how to use this api.
every place I find is similar to the link above or older. the few newer seem to also follow the pattern without really explaining how to use this api.
at the same time I'm new to the delegate pattern in fact I only know that it is something that is heavily used in this Api but I have no IDEA how or why.
Basically I need help finding my way to solve this problem here:
I've tried to do something like this:
public class NetworkPostRequestor:NSObject,NetworkPostRequestingProtocol,URLSessionTaskDelegate,URLSessionDataDelegate
{
public var _response:HTTPURLResponse
public override init()
{
_response = HTTPURLResponse()
}
public func post(data: Data, url: URL)
{
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("Keep-Alive", forHTTPHeaderField: "Connection")
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration,delegate: self, delegateQueue: OperationQueue.main)
let task = session.uploadTask(with: request, from: data)
task.resume()
}
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: #escaping (URLSession.ResponseDisposition) -> Void)
{
_response = response as! HTTPURLResponse
}
}
however I never even hit the PHPserver. the server when hit will say something like this in the terminal:
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64265 [200]: /
[Tue Mar 7 11:43:20 2017] 192.168.250.100:64266 [404]: /favicon.ico - No such file or directory
Well that is when I hit it with my browser and there is no image with it. but alt least I know that it will write something with the terminal if it hits it. Nothing happens And without a resource to teach me this api I'm afraid I will never learn how to fix this or even if I'm doing something completely wrong.
I'm using Swift 3 and Xcode 8.2.1
Edit:
I've added this method to the class and found that I hit it every single time.
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
{
_error = error.debugDescription
}
the debug description have this string "some"
I never used this exact procedure with tasks but rather use the methods with callback. I am not sure if in the background there should be much of a difference though.
So to generate the session (seems pretty close to your):
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
Then I generate the request which stupidly enough needs an URL in the constructor:
var request = URLRequest(url: URL(string: "www.nil.com")!) // can't initialize without url
request.url = nil
Adding url with query parameters (you can just set the URL in your case, I have a tool to handle a few cases):
fileprivate func injectQueryParameters(request: inout URLRequest) {
if let query = queryParameters.urlEncodedString {
let toReturn = endpoint.url + "?" + query
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
} else {
let toReturn = endpoint.url
if let url = URL(string: toReturn) {
request.url = url
} else {
print("Cannot prepare url: \(toReturn)")
}
}
}
Then the form parameters. We mostly use JSON but anything goes here:
fileprivate func injectFormParameters( request: inout URLRequest) {
if let data = rawFormData {
request.httpBody = data
} else if let data = formParameters.urlEncodedString?.data(using: .utf8) {
request.httpBody = data
}
}
And the headers:
fileprivate func injectHeaders(request: inout URLRequest) {
headers._parameters.forEach { (key, value) in
if let stringValue = value as? String {
request.setValue(stringValue, forHTTPHeaderField: key)
}
}
}
So in the end the whole call looks something like:
class func performRequest(request: URLRequest, callback: (([String: Any]?, NSError?) -> Void)?) {
let configuration = URLSessionConfiguration.default
let session = URLSession(configuration: configuration, delegate: nil, delegateQueue: OperationQueue.main)
let task = session.dataTask(with: request) { data, response, error in
// Response is sent here
if let data = data {
callback?((try? JSONSerialization.jsonObject(with: data, options: .allowFragments)) as [String: Any]?, error)
} else {
callback?(nil, error)
}
}
task.resume()
}
I hope this puts you on the right track. In general you do have a few open source libraries you might be interested in. Alamofire is probably still used in most cases.
Related
I am making a HTTP get request in swift where I am getting an outdated response for some reason..I have compared the response in Postman with the one I printed in the Xcode and I'm getting this..
Response Headers in Postman:
Response Headers printed in Xcode Console:
see the sections highlighted in green, both the calls are made at the same time. This is a live API of my project and as there is no change in the current live data I am not showing JSON response. But as you can see in the response headers, the value for field "Date" in both Postman and Xcode is different.. When I do it in Postman, it is giving me new response every time. But in Xcode I'm getting the same response all day. I don't know why This is happening. And after erasing the contents of simulator (or deleting the app from simulator) and reinstalling the project again it gave me a new updated response. But it's again repeated...
Code in RequestManager Class:
import Foundation
class RequestManager {
class func callTheGetAPI(urlString: String, closure: #escaping (Data?)->Void) {
let url = URL.init(string: urlString)
var urlRequest = URLRequest(url: url!)
urlRequest.httpMethod = "GET"
urlRequest.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
if (error != nil) {
print(error!.localizedDescription)
} else {
let httpResponse = response as? HTTPURLResponse
print(httpResponse!.allHeaderFields)
closure(data)
}
}
dataTask.resume()
}
}
code in my ViewController:
override func viewDidLoad() {
super.viewDidLoad()
getRecommendedData()
}
func getRecommendedData() {
let url = APIManager.API_01
RequestManager.callTheGetAPI(urlString: url, closure: { response in
do {
if (try JSONSerialization.jsonObject(with: response!, options: .mutableContainers) as? [[String : Any]]) != nil{
// print(json)
}
} catch let error {
print(error.localizedDescription)
}
})
}
This is all I have guys.. If anyone knows what's happening or if I'm doing something wrong.. Please help..
Replace below line
var urlRequest = URLRequest(url: url!)
With
var urlRequest = URLRequest(url: url!, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 60)
From the documentation reloadIgnoringCacheData: Specifies that the
data for the URL load should be loaded from the origin source. No
existing local cache data, regardless of its freshness or validity,
should be used to satisfy a URL load request.
By default it uses caches.
I am trying to pass authorization key in header of a URLRequest. But at the server end the key is not received. The same API when called from postman working fine. Any other key in the header is working fine, even authorizations key is visible at server end.
Here is my code:
let headers = [
"authorization": "token abcd"
]
var request = URLRequest.init(url: NSURL(string:
"http://127.0.0.1:7000/api/channels?filter=contributed")! as URL)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
let config = URLSessionConfiguration.default
config.httpAdditionalHeaders = headers
let session = URLSession.init(configuration: config)
let dataTask = session.dataTask(with: request, completionHandler: { (data, response, error) -> Void in
if (error != nil) {
print(error ?? "")
} else {
let httpResponse = response as? HTTPURLResponse
print(httpResponse ?? "")
}
})
As you can see, I tried to set the token in both session config and request but none is working.
This seems to be working:
// Set the security header
private var credentials: String {
return "\(participantId):\(password)"
}
private var basicAuthHeader: String {
return "Basic \(credentials)"
}
func getSettings(participantId: Int, password: String) -> Bool {
self.participantId = participantId
self.password = password
let path = "/settings/\(participantId)"
guard let url = URL(string: "\(BASE_URL)\(path)") else {
Log.e("Invalid URL string, could not convert to URL")
return false
}
var urlRequest = URLRequest(url: url)
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.setValue(basicAuthHeader, forHTTPHeaderField: "Authorization")
urlRequest.setValue(APP_FILE_NAME, forHTTPHeaderField: "User-Agent")
// This is a synchronous wrapper extension around URLSession.dataTask()
let (data, response, error) = URLSession.shared.synchronousDataTask(with: urlRequest)
// Process the result...
}
Note: code written by my coworker. Thanks John!
Looks like the problem is that you are modifying Authorization header using httpAdditionalHeaders which is something you should not do.
From the Doc
An NSURLSession object is designed to handle various aspects of the HTTP protocol for you. As a result, you should not modify the following headers:
Authorization,
Connection,
Host,
Proxy-Authenticate,
Proxy-Authorization,
WWW-Authenticate
Removing the line config.httpAdditionalHeaders = headers
should fix the issue.
If you want token to be hardcoded, I guess it has to be like this:
urlRequest.httpMethod = "GET"
urlRequest.setValue("Token <Your Token>", forHTTPHeaderField: "Authorization")
I found the same thing: setting the header field Authorization just didn't do the trick.
Here's the solution I settled on (which works well):
I added the URLSessionDelegate protocol to my current class. This unfortunately means inheriting from NSObject.
Then, when defining my URLSession, I set its delegate to 'self'.
Finally, I provide an authentication challenge handler.
In code, this all looks like:
public class SomeHTTPTask: NSObject, URLSessionDelegate {
public init() {
... initialize variables ...
super.init()
... now you are free to call methods on self ...
}
public func httpTask(withURL url: URL) {
let request = URLRequest(url: url)
... set up request ...
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: request) {data, response, error in
... now you have a result ...
}
}
public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let user = Credentials.sharedInstance.userId, let password = Credentials.sharedInstance.password else {
completionHandler(.performDefaultHandling, nil)
return
}
let userCredential = URLCredential(user: user,
password: password,
persistence: .permanent)
completionHandler(.useCredential, userCredential)
}
}
Hopefully, the bits and pieces are self-explanatory. It's just an authentication challenge handler that provides credentials, if it can. The underlying URLSession will deal with the details, wither it's an NTLM or Basic auth, those sorts of things.
In the end, this seems a solid solution. At least, it worked for me.
Here's a nice reference document from Apple if you like reading that kind of thing.
I have created an Authentication class (I am treating this as a model) which accepts login credentials and a closure. The closure returns the network request's response to the controller. I just want to know if this is a better way of doing this, if not what is the best way of doing this?
Note: I am using SwiftyJson library
Model function
func login(userName: String, password: String, completionHandler: (response: JSON) -> ()) {
let device = UIDevice().identifierForVendor
let postData = "login=\(userName)&password=\(password)&deviceid=\(device)"
let request = NSMutableURLRequest(URL: NSURL(string: APP_SERVICE_LOGIN)!)
request.HTTPMethod = HTTP_REQUEST_POST
request.HTTPBody = postData.dataUsingEncoding(NSUTF8StringEncoding)
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, error) -> Void in
if (error == nil) {
completionHandler(response: JSON(data: data!))
} else {
print("Error = \(error)")
print("Response = \(response)")
}
}
task.resume()
}
Controller
override func viewDidLoad() {
super.viewDidLoad()
authentication.login("test#test.com", password: "test", completionHandler: {(response: JSON) -> () in
print(response)
})
}
Thanks
What you are doing is correct way of networking.
The only thing I would advise you to have is a separate networking wrapper which takes header, body and completion block and does all the networking on behalf of others.
That way you do no need to deal with NSMutableURLRequest, NSURLSession etc. time and agains. Call your custom network class with a simple API and you are good. It also helps debugging centred to one place!
I'm learning to do a basic networking call, and following a tutorial. The tutorial goes through concurrency, and downloading the JSON data in the background thread. I was wondering why in the sharedSession.downloadTaskWithURL method the queryURL would be passed as the NSURL object as opposed to the baseURL. I feel like I'm missing something pretty obvious!
func searchRecipeData() {
let baseURL = NSURL(string: "http://api.recipes.com/v1/api/recipes?_app_id=\(apiID)&_app_key=\(apiKey)")
let queryURL = NSURL(string: "&q=onion+soup", relativeToURL: baseURL)!
let sharedSession = NSURLSession.sharedSession()
let downloadData: NSURLSessionDownloadTask = sharedSession.downloadTaskWithURL(queryURL, completionHandler: { (location: NSURL!, response: NSURLResponse!, error: NSError!) -> Void in
if (error == nil) {
let data = NSData(contentsOfURL: baseURL!)
println(data)
}
})
// Resumes it even though it hasn't started yet
downloadData.resume()
}
Take a look at the second parameter where you create queryURL. You are passing the baseURL constant. What happens is that the '&q=onion+soup' query parameter is told to be relative to the baseURL. The queryURL constant is the full URL, and it is then passed to downloadTaskWithURL.
I am pretty new to programming in IOS with Cocoa and I am using Swift. I fetch data from a JSON API using an NSURLSession data session with custom delegate, not closures. The reason for using custom delegate is that I have to do basic authentication and I also inject a custom cache-control header to control caching behavior (my API doesn’t include any caching related headers in the response at all).
All this works perfectly but only for requests for which the URLSession:dataTask:didReceiveData: method is called only once. As soon as I get larger responses (some 20-30kBytes) that call the didReceivedData method several times, the URLSession:dataTask:willCacheResponse:completionHandler: method doesn’t get called at all, and therefore my response doesn’t get cached. Re-issuing the same request within 5 minutes will issue a request to the server again, which doesn’t happen for requests whose responses only call didReceiveData one single time. The URLSession:task:didCompleteWithError: method is correctly called and proceeded in all cases.
The documentation of the URLSession:dataTask:willCacheResponse:completionHandler: method (https://developer.apple.com/library/IOs/documentation/Foundation/Reference/NSURLSessionDataDelegate_protocol/index.html#//apple_ref/occ/intfm/NSURLSessionDataDelegate/URLSession:dataTask:willCacheResponse:completionHandler:) says this method is only called if the NSURLProtocol handling the request decides to do so, but I don’t really understand what to do to make this happen.
Any feedback and ideas are very welcome!
This is the code that issues the HTTP request:
let config = NSURLSessionConfiguration.defaultSessionConfiguration()
config.URLCache = NSURLCache.sharedURLCache()
//config.URLCache = NSURLCache(memoryCapacity: 512000000, diskCapacity: 1000000000, diskPath: "urlCache")
let urlString = apiUrlForFilter(filter, withMode: mode, withLimit: limit, withOffset: offset)
let url = NSURL(string: urlString)
var policy: NSURLRequestCachePolicy?
if ignoreCache == true {
policy = .ReloadIgnoringLocalCacheData
} else {
policy = .UseProtocolCachePolicy
}
let request = NSURLRequest(URL: url!, cachePolicy: policy!, timeoutInterval: 20)
let session = NSURLSession(configuration: config, delegate: self, delegateQueue: nil)
let task = session.dataTaskWithRequest(request)
task.resume()
I have the following delegate functions implemented:
URLSession:didReceiveChallenge:completionHandler: to handle SSL certificate trust,
URLSession:task:didReceiveChallenge:completionHandler: to handle basic authentication
URLSession:dataTask:didReceiveResponse:completionHandler: to read some specific headers and save them as instance variables
additionally, the important ones with code:
URLSession:dataTask:didReceiveData: to accumulate data as it arrives for larger HTTP request responses:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, didReceiveData data: NSData) {
if receivedData == nil {
receivedData = NSMutableData()
}
receivedData!.appendData(data)
println("did receive data: \(receivedData!.length) bytes")
}
URLSession:dataTask:willCacheResponse:completionHandler: to inject my own Cache-Control header:
func URLSession(session: NSURLSession, dataTask: NSURLSessionDataTask, willCacheResponse proposedResponse: NSCachedURLResponse, completionHandler: (NSCachedURLResponse!) -> Void) {
println("willCacheResponse was called")
let response: NSURLResponse = proposedResponse.response
let httpResponse = response as NSHTTPURLResponse
var headers = httpResponse.allHeaderFields
var modifiedHeaders = headers
modifiedHeaders.updateValue("max-age=300", forKey: "Cache-Control")
let modifiedResponse = NSHTTPURLResponse(URL: httpResponse.URL!, statusCode: httpResponse.statusCode, HTTPVersion: "HTTP/1.1", headerFields: modifiedHeaders)
let cachedResponse = NSCachedURLResponse(response: modifiedResponse!, data: proposedResponse.data, userInfo: proposedResponse.userInfo, storagePolicy: proposedResponse.storagePolicy)
completionHandler(cachedResponse)
}
URLSession:task:didCompleteWithError: to check the complete response for errors and call a callback closure this class gets by initialization to further proceed the result data:
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
session.finishTasksAndInvalidate()
if error != nil {
var string: String?
if errorString != nil {
string = errorString
} else {
string = Helpers.NSURLErrorDomainErrorForCode(error!.code)
}
errorCallback(string!)
return
}
if receivedData == nil {
errorCallback("the query returned an empty result")
return
}
var jsonError: NSError?
let results: AnyObject! = NSJSONSerialization.JSONObjectWithData(receivedData!, options: NSJSONReadingOptions.AllowFragments, error: nil)
if results == nil {
errorCallback("the data returned was not valid JSON")
return
}
let jsonParsed = JSONValue.fromObject(results)
if let parsedAPIError = jsonParsed!["error"]?.string {
errorCallback("API error: \(parsedAPIError)")
return
}
callback(jsonParsed!, self.serverTime!)
}