I am making an app that has the need to make HTTP Request in many of its ViewControllers.
I ended up copy and pasting these codes in to each of the ViewControllers and listen to the delegates callback of NSURLConnectionDelegate and NSURLConnectionDataDelegate
func makeRequest()
{
//Base64
var username = "testUsername";
var password = "testPassword";
var loginString = NSString(format: "%#:%#", username, password);
var loginData: NSData = loginString.dataUsingEncoding(NSUTF8StringEncoding)!;
var base64LoginString = loginData.base64EncodedStringWithOptions(nil);
var url: NSURL = NSURL(string: self.HTTP_REQUEST_STRING)!;
var urlRequest: NSMutableURLRequest = NSMutableURLRequest(URL: url);
urlRequest.setValue("Basic \(base64LoginString)", forHTTPHeaderField: "Authorization");
urlRequest.HTTPMethod = "POST";
urlConnection = NSURLConnection(request: urlRequest, delegate: self)!;
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!)
{
self.resultData.appendData(data);
}
func connectionDidFinishLoading(connection: NSURLConnection!)
{
//Do Something
}
func connection(connection: NSURLConnection, didFailWithError error: NSError)
{
//Do Something
}
I am wondering if there is a better approach than this, rather than copy and pasting the codes into every ViewControllers?
Is it possible to put these codes into a class? But then, how do we know if the connection has finished loading?
I am sorry for this question, I lack the knowledge of good Object Oriented design.
Thank you
To be up to date, you should be using NSURLSession as your request class. You're not necessarily required to listen in for the delegation callbacks as there is a closure callback that will provide you with and error and data according to how you configured your session. Regarding placement, it depends on your code and what you want. You can place this code in the viewController if it makes sense, some people create proxy classes to make all their requests and report statuses back to them. It all depends on flexibility, robustness and structure of your application. If you're making the same network request from 3 different viewControllers, it's likely that your should be placing the network request in a type of proxy class to prevent duplicate code. Look into the proxy and singleton design patterns if you'd like to know more about code design.
Here's a nice tutorial on NSURLSession to get you started:
Raywenderlich tutorial
Proxy Design Pattern
Singleton Design Pattern
Like others have mentioned, you have far more learning to do than a single answer on SO can provide. In the mean time, take a look at Alamofire, a fairly comprehensive networking library built around NSURLSession. In it you will find code for creating requests and much more. Alamofire provides a prebuilt, OO way of performing network requests, suitable for one off requests or an entire network manager class.
Related
I have an app which uses URLSession-based networking and URLCache for storing network requests on disk. I noticed that when the storage size of URLCache reaches the diskCapacity, the eviction strategy seems to be to remove all entries, which is a problem in my use case. So I decided to write an URLCache subclass which would provide a custom storage for cached responses and which would implement LRU eviction strategy with better control.
As URLCache's documentation states, subclassing for this purpose should be supported:
The URLCache class is meant to be used as-is, but you can subclass it when you have specific needs. For example, you might want to screen which responses are cached, or reimplement the storage mechanism for security or other reasons.
However, I ran into problems with trying to use this new URLCache subclass with URLSession networking.
I have a test resource which I fetch using HTTP GET. The response headers contain:
Cache-Control: public, max-age=30
Etag: <some-value>
When using the standard, non-subclassed URLCache, the first request loads the data from network as expected (verified with HTTP proxy). The second request doesn't go to the network, if done within first 30 seconds, as expected. Subsequent requests after 30 seconds cause conditional GETs with Etag, as expected.
When using a URLCache subclass, all requests load the data from network - max-age doesn't seem to matter, and no conditional GETs are made.
It seems that the URLCache does something special to the CachedURLResponse instances after they're loaded from its internal storage, and this something affects how URLSession handles the HTTP caching logic. What am I missing here?
I've written a very minimal URLCache subclass implementation to demonstrate this problem. This class stores and loads CachedURLResponse instances using NSKeyedArchiver / NSKeyedUnarchiver, and it supports only zero or one response. Note that there are no calls to super - this is by design, since I want to use my own storage.
class CustomURLCache: URLCache {
let cachedResponseFileURL = URL(filePath: NSTemporaryDirectory().appending("entry.data"))
// MARK: Internal storage
func read() -> CachedURLResponse? {
guard let data = try? Data(contentsOf: cachedResponseFileURL) else { return nil }
return try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as! CachedURLResponse
}
func store(_ cachedResponse: CachedURLResponse) {
try! (try! NSKeyedArchiver.archivedData(withRootObject: cachedResponse, requiringSecureCoding: false)).write(to: cachedResponseFileURL)
}
// MARK: URLCache Overrides
override func cachedResponse(for request: URLRequest) -> CachedURLResponse? {
read()
}
override func getCachedResponse(for dataTask: URLSessionDataTask, completionHandler: #escaping (CachedURLResponse?) -> Void) {
completionHandler(read())
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for request: URLRequest) {
store(cachedResponse)
}
override func storeCachedResponse(_ cachedResponse: CachedURLResponse, for dataTask: URLSessionDataTask) {
store(cachedResponse)
}
}
My test case:
func test() {
let useEvictingCache = false
let config = URLSessionConfiguration.default
if useEvictingCache {
config.urlCache = CustomURLCache()
} else {
config.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 1024 * 1024 * 100)
}
self.urlSession = URLSession(configuration: config)
let url = URL(string: "https://example.com/my-test-resource")!
self.urlSession?.dataTask(with: URLRequest(url: url), completionHandler: { data, response, error in
if let data {
print("GOT DATA with \(data.count) bytes")
} else if let error {
print("GOT ERROR \(error)")
}
}).resume()
}
Tested on iOS 16.2.
Received a response for my question at Apple's Developer Forums from Quinn “The Eskimo”:
My experience is that subclassing Foundation’s URL loading system classes puts you on a path of pain [1]. If I were in your shoes, I’d do your custom caching above the Foundation URL loading system layer.
[1] Way back in the day the Foundation URL loading system was implemented in Objective-C and existed within the Foundation framework. In that world, subclasses mostly worked. Shortly thereafter — and I’m talking before the introduction of NSURLSession here — the core implementation changed languages and moved to CFNetwork. Since then, the classes you see in Foundation are basically thin wrappers around (private) CFNetwork types. That’s generally OK, except for the impact on subclassing.
So it sounds like one should read the URLCache documentation re: subclassing with a grain of salt.
One of my view controllers decodes and prints html from a web page. I've done searches on stackoverflow and in example project on github and it seems that people are using Alamofire with Swiftsoup to do this.
I'm a beginner but I am trying to understand why I would need AlamoFire when I can just use URLSessions? Is it better to use Alamofire?
My use case is simple, I think. If I use Alamofire,
let getURL = "https://www.someurl.com/extension"
Alamofire.request(getURL, method: .post, parameters: nil, encoding: URLEncoding.default).validate(contentType: ["application/x-www-form-urlencoded"]).response { (response) in
if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
do {
parseHTML()
}
}
}
If I use URLSessions, I think it would like this:
let httpURL = URL(string: "https://www.someurl.com/extension")!
let httpTask = URLSession.shared.dataTask(with: httpURL) {
(data, response, error) in
guard let validData = data, error == nil else {
DispatchQueue.main.async(execute: {
print("Error getting paragraph\n") })
return
}
var results = String(data: data!, encoding: String.Encoding.utf8) ?? "Unable to read Paragraph HTML\n"
DispatchQueue.main.async(execute: {
print("Correctly read from Paragraph HTML\n")
parseHTML()
})
}
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async(execute: {
httpTask.resume()
})
Side question: Is Swiftsoup the go to for decoding HTML? Is there something built in that can be used instead?
Alamofire/AFNetworking (AFNetworking is the objective-c version) gained traction as an alternative to Apple's NSURLConnection class, that was much more low level and involved a lot of boilerplate code. It was not as easy to establish a download task or anything with NSURLConnection, AFNetworking (at the time) made it easier to perform the tasks like in your question without having to write too much code.
Around iOS7, Apple released NSURLSession to replace NSURLConnection, which made it quite similar to how AlamoFire do things. At this point personally, I feel that using NSURLSession/URLSession is fine and straightforward enough. Maybe AlamoFire is a bit easier to use but overall they are similar. The only times I end up moving towards AlamoFire these days is when I face some type of limitation.
So tl;dr, pre iOS7, AFNetworking was a much easier and straightforward way of working with download tasks. Post iOS7 URLSessions became easier to work with and set up.
I'm trying to parse csv file from my ios project(swift 2.3) and found this website. In the tutorial code, it has the following section of code :
if let content = String(contentsOfURL: contentsOfURL,
encoding: encoding, error: error) {
...........
}
And I'm not sure what it does. Does it create a String object?
Does it create a String object?
Yes, it creates a string from the contents of the URL given by contentsOfURL and using the character encoding given by encoding. It's analogous to the following Objective-C code:
NSString *content = [NSString stringWithContentsOfURL:contentsOfURL
encoding:encoding
error:&error];
The if let part is a form of conditional statement. let is used to assign a value to an immutable variable. Using it in a conditional as in your example only allows the body of the conditional statement to execute if that assignment succeeds. So, if some error occurs while the data at the given URL is being fetched or if the string cannot be created for some reason, the condition fails and the body isn't executed. The whole snippet might be written like this in Objective-C:
NSString *content = [NSString stringWithContentsOfURL:contentsOfURL
encoding:encoding
error:&error];
if (content != nil) {
// do something with content
}
That code creates a string, but it does it by fetching the contents of a URL. Usually that URL points to a resource on the Internet. In that case it's a very bad way to fetch a string, since it is a synchronous network call that can hang or fail. It's a very bad idea to do synchronous networking calls on the main thread.
You could wrap that code in a GCD call to a background queue, but instead I'd suggest using NSURLSession and submitting a data task. Your search terms would be NSURLSession (or just URLSession in Swift 3) and the function func dataTask(with url: URL). (It might be easier search on it's Objective-C name, dataTaskWithURL since Google searches don't work very well with special characters.)
Take a look at a GitHub project I created called Async_demo. It has a singleton class called DownloadManager that downloads a blob of data from a specified URL. It's written to return the data as a Data object, but it would be a simple matter to convert that result from Data to a String.
The key bit of code is this:
typealias DataClosure = (Data?, Error?) -> Void
func downloadFileAtURL(_ url: URL, completion: #escaping DataClosure) {
//We create a URLRequest that does not allow caching so you can see the download take place
let request = URLRequest(url: url,
cachePolicy: .reloadIgnoringLocalAndRemoteCacheData,
timeoutInterval: 30.0)
let dataTask = URLSession.shared.dataTask(with: request) {
//------------------------------------------
//This is the completion handler, which runs LATER,
//after downloadFileAtURL has returned.
data, response, error in
//Perform the completion handler on the main thread
DispatchQueue.main.async() {
//Call the copmletion handler that was passed to us
completion(data, error)
}
//------------------------------------------
}
dataTask.resume()
//When we get here the data task will NOT have completed yet!
}
In one of my apps I need to geocode address string. At first I considered using CLGeocoder. However, after I tried it I stumbled upon a problem which I described in this question.
The solution was to use Google's Geocoding APIs instead. I have now switched to them and managed to get them working by having the following functions:
func startConnection(){
self.data = NSMutableData()
let urlString = "https://maps.googleapis.com/maps/api/geocode/json?address=\(searchBar.text!)&key=MYKEY"
let linkUrl:NSURL = NSURL(string:urlString.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())!)!
let request: NSURLRequest = NSURLRequest(URL: linkUrl)
let connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)!
connection.start()
}
func connection(connection: NSURLConnection!, didReceiveData data: NSData!){
self.data.appendData(data)
}
func connectionDidFinishLoading(connection: NSURLConnection!) {
do {
if let json = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers) as? [String: AnyObject] {
print(json)
}
}
catch {
print("error1")
}
}
This works great and resolves the problem which I had with CLGeocoder. However, in addition to extracting the coordinates of place, I need to also use Google's Timezone APIs to extract the timezone for each place.
Doing this with the NSURLConnection or NSURLSession seems to me a bit difficult as I would need to keep track of which session/connection returns. So, I would like to have some solution which uses completion handlers.
I have tried using Alamofire framework (using the correct branch for Swift 2.0). However, it seems like request() function is the wrong one to use in this case. I have tried:
let parameters = ["address":searchBar.text!,"key":"MYKEY"]
Alamofire.request(.GET, "https://maps.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.AllowFragments) { _, _, JSON in
print(JSON)
}
And all I am getting printed is "SUCCESS". I hope that I am doing something wrong and it can be fixed because I would really like to be able to use closures instead of delegate calls.
My questions are:
Is it possible to use Alamofire with Google Geocoding APIs?
If so, can you please tell me what am I doing wrong?
If it is not possible, can you please suggest me how to design a system with NSURSessions or NSURLConnections which would allow me to use completion handlers for each call instead of delegates?
P.S. I am aware that I can use synchronous requests but I would really like to avoid using that option
Update
It was suggested that adding .MutableContainers as an option should make responseJSON work. I tried the code below:
let apiKey = "MYKEY"
var parameters = ["key":apiKey,"components":"locality:\(searchBar.text!)"]
Alamofire.request(.GET, "https://maps.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.MutableContainers) { one, two, JSON in
print(JSON)
}
And all I get printed is "SUCCESS".
Ok, I have finally figured this out (with the help from #cnoon). The value which is returned is of type Result. I couldn't find documentation for it, but the source code is available here.
In order to retrieve JSON below implementation can be used:
Alamofire.request(.GET, "https://mapss.googleapis.com/maps/api/geocode/json", parameters: parameters).responseJSON(options:.MutableContainers) { _, _, JSON in
switch JSON {
case .Failure(_, let error):
self.error = error
break
case .Success(let value):
print(value)
break
}
}
The value printed is the correct representation of response from Geocoding APIs.
I wanted to know if it's possible to load the text in a label from a URL.
I was going to try and use NSURL to pull in a .txt file stored online.
Would anyone know how to implement this with swift?
Thanks
If you don't want to use sessions, you can also use the simpler NSURLConnection Class, something like this:
let url = NSURL(string: "https://wordpress.org/plugins/about/readme.txt")
let request = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {(response, data, error) in
println(NSString(data: data, encoding: NSUTF8StringEncoding))
dispatch_async(dispatch_get_main_queue()) {
// Do stuff on the UI thread
self.textField.text = NSString(data: data, encoding: NSUTF8StringEncoding)! as String
return
}
}
Yes it's possible. Yes, I know how to do it. However, you haven't exerted any effort whatsoever to solve your problem yourself.
Search on NSURLConnection. Unless you need https or login, you could use the class method sendAsynchronousRequest:queue:completionHandler:, which is very easy to use. You should be able to find examples of using it on the net.
You could also use a library like Alamofire. That makes it even easier.