Just want to say, this article is intended primarily for beginners. There will be no best practice, creation of services, repositories and other code optimization. I'll tell you about the basics of working with queries and show application with examples.
So, we needed to process data from the server. This task is present in almost all mobile applications.
There is a native tool for this - URLSession , but working with it is a bit more difficult than we would like. To facilitate this process, there is a framework Alamofire - a wrapper over URLSession, which greatly simplifies life when working with the server.
We use CocoaPods . working with him is very easy and quick.
Add to the Podfile :
pod 'Alamofire'
To use Alamofire version 4+ , the following requirements are required:
We also need to add use_frameworks!
.
This is what the minimum Podfile will look like:
platform :ios, '9.0' use_frameworks! target 'Networking' do pod 'Alamofire' end
By default, the application does not allow access to HTTP connections, only HTTPS is available. But so far a lot of sites have not switched to https.
We will work with the server http://jsonplaceholder.typicode.com , and it works on http. Therefore, we need to open access to it.
For training, we will open access to all sites. Opening for one site in this article will not be considered.
Open Info.plist and add App Transport Security Settings to it and inside this parameter you need to add Allow Arbitrary Loads , with the value YES .
It should look like this:
! [Info.plist] (/ Users / zdaecqzezdaecq / Downloads / Working with queries using Alamofire / info_plist.png)
Or here is the Source code that needs to be added:
right-click on Info.plist -> Open as -> Source code
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
Open the project.
Don't forget that we need to open Networking. xcworkspace , not Networking. xcodeproj , which was created after pod install
Open the ViewController.swift file and replace its code with the following:
import UIKit import Alamofire class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() request("http://jsonplaceholder.typicode.com/posts").responseJSON { response in print(response) } print("viewDidLoad ended") } }
Run the project.
The console will display:
viewDidLoad ended SUCCESS: ( { body = "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"; id = 1; title = "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"; userId = 1; }, ...
Congratulations! You made the first request to the server and received a response from it with the result.
We need to access the new feature, because these are not our files, but a separate library:
import Alamofire
Actually the request method itself:
request
Next, the first parameter is the URL to which the request will be made:
"http://jsonplaceholder.typicode.com/posts"
The responseJSON
method says that we need a response from the server in JSON format.
Further in the downloader we get a response from the server and output it to the console:
{ response in print(response) }
It is important to note that the code in this coolizer occurs asynchronously and will be executed after exiting viewDidLoad, thus the string viewDidLoad ended
is output to the console earlier.
In fact, we made a GET request, but did not indicate it anywhere. Starting with Alamofire 4, the GET request is executed by default. We can explicitly specify it by replacing the corresponding code with the following:
request("http://jsonplaceholder.typicode.com/posts", method: .get)
As you already understood in the parameter method: the request method is transmitted and it depends on it how we will communicate with the server. Most often we will:
data from the server.
You can read more about these and other HTTP methods on Wikipedia:
The request
function is a global function, so we can call it through Alamofire.request
or just request
.
This is the complete query with all the parameters:
request(URLConvertible, method: HTTPMethod, parameters: Parameters?, encoding: ParameterEncoding, headers: HTTPHeaders?)
Consider more:
The first parameter is the path to the request and it accepts URLConvertible
. (Your CEP)
If we look at its implementation, we will see that this is a protocol with one function:
public protocol URLConvertible { func asURL() throws -> URL }
and it is already implemented for the following data types:
In this regard, we can pass any of the above types as a parameter or create our own data type with the implementation of this protocol.
This is enum, with all possible types of queries:
public enum HTTPMethod: String { case options = "OPTIONS" case get = "GET" case head = "HEAD" case post = "POST" case put = "PUT" case patch = "PATCH" case delete = "DELETE" case trace = "TRACE" case connect = "CONNECT" }
As we already figured out: the default .get
There is nothing complicated, go ahead.
This is a simple dictionary:
public typealias Parameters = [String: Any]
Through the parameters, we will transmit data to the server (for example, to modify or create objects).
This is also a protocol with one function:
public protocol ParameterEncoding { func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest }
It is necessary to determine in what form we encode our parameters. Different servers and requests require a specific encoding.
This protocol is implemented:
By default we have URLEncoding.default
.
Basically, this parameter is not used, but sometimes it is needed, in particular, JSONEncoding.default
for encoding in JSON format and PropertyListEncoding.default
in XML.
I noticed that Int
not sent without JSONEncoding.default
, but maybe it was in Alamofire 3, or maybe because of the server. Just keep that in mind.
This is also a Dictionary, but of a different typing:
public typealias HTTPHeaders = [String: String]
Headers (headers) will be needed mainly for authorization.
More about wikipedia titles:
At the output, we get an object of type DataRequest
- the request itself. We can save it, pass it as a parameter to another function if necessary, tune and send. About this further.
The response from the server may come with both the result and the error. In order to distinguish them, the response has parameters such as statusCode
and contentType
.
We can check these parameters manually, and we can set up a request, so that he will immediately tell us, the answer came with an error or with a result.
If we did not set up validation, then
responseJSON.response?.statusCode
we will have the status of the response code, and in
responseJSON.result.value
there will be a result if the answer came without error, and in
responseJSON.result.error
if with an error.
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in guard let statusCode = responseJSON.response?.statusCode else { return } print("statusCode: ", statusCode) if (200..<300).contains(statusCode) { let value = responseJSON.result.value print("value: ", value ?? "nil") } else { print("error") } }
More information about state codes on wikipedia:
For this, DataRequest
has 4 methods:
Consider only the last one, because we will have enough of it for 95% of requests.
Let's look at its implementation:
public func validate() -> Self { return validate(statusCode: self.acceptableStatusCodes).validate(contentType: self.acceptableContentTypes) }
We see that it consists of two other validations:
self.acceptableStatusCodes
- returns an array of status codes (Int) from range 200 .. <300self.acceptableContentTypes
- returns an array of valid headers (String)DataResponse
has a result
parameter that can tell us whether the response came with an error or with a result.
So apply validation for the query:
request("http://jsonplaceholder.typicode.com/posts").validate().responseJSON { responseJSON in switch responseJSON.result { case .success(let value): print(value) case .failure(let error): print(error) } }
If we do not have a request ( validate()
), then the result
will always be equal to .success
, except for errors due to the lack of Internet.
You can handle the answer in both ways, but I strongly recommend using the request validation setting - there will be fewer errors!
The response from the server is most often in the form of a single object or an array of objects.
If we look at the type of the result of the response, we will see the type Any
. To get something out of it - we need to bring it to the desired format.
In the logs, we noticed that we have a dictionary array, so we’ll bring to it:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in switch responseJSON.result { case .success(let value): print("value", value) guard let jsonArray = responseJSON.result.value as? [[String: Any]] else { return } print("array: ", jsonArray) print("1 object: ", jsonArray[0]) print("id: ", jsonArray[0]["id"]!) case .failure(let error): print(error) } }
After that, as shown above, we can do anything, for example, create an object and save it, so that it is more convenient to work with the data.
In a separate file, create a Post structure:
struct Post { var id: Int var title: String var body: String var userId: String }
This will look like parsing into an array of objects:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in switch responseJSON.result { case .success(let value): guard let jsonArray = value as? Array<[String: Any]> else { return } var posts: [Post] = [] for jsonObject in jsonArray { guard let id = jsonObject["id"] as? Int, let title = jsonObject["title"] as? String, let body = jsonObject["body"] as? String, let userId = jsonObject["userId"] as? String else { return } let post = Post(id: id, title: title, body: body, userId: userId) posts.append(post) } print(posts) case .failure(let error): print(error) } }
Parsing an object inside a request looks very bad + we will always have to copy these lines for each request. To get rid of this, create the init?(json: [String: Any])
constructor init?(json: [String: Any])
:
init?(json: [String: Any]) { guard let id = json["id"] as? Int, let title = json["title"] as? String, let body = json["body"] as? String, let userId = json["userId"] as? String else { return nil } self.id = id self.title = title self.body = body self.userId = userId }
It can return nil if the server does not return something to us.
And then the query method looks much clearer and more pleasant:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in switch responseJSON.result { case .success(let value): guard let jsonArray = value as? Array<[String: Any]> else { return } var posts: [Post] = [] for jsonObject in jsonArray { guard let post = Post(json: jsonObject) else { return } posts.append(post) } print(posts) case .failure(let error): print(error) } }
Let's go even further and in Post we will add an array processing method:
static func getArray(from jsonArray: Any) -> [Post]? { guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil } var posts: [Post] = [] for jsonObject in jsonArray { if let post = Post(json: jsonObject) { posts.append(post) } } return posts }
Then the query method will take the following form:
request("http://jsonplaceholder.typicode.com/posts").responseJSON { responseJSON in switch responseJSON.result { case .success(let value): guard let posts = Post.getArray(from: value) else { return } print(posts) case .failure(let error): print(error) } }
Final option of Post.swift file:
import Foundation struct Post { var id: Int var title: String var body: String var userId: String init?(json: [String: Any]) { guard let id = json["id"] as? Int, let title = json["title"] as? String, let body = json["body"] as? String, let userId = json["userId"] as? String else { return nil } self.id = id self.title = title self.body = body self.userId = userId } static func getArray(from jsonArray: Any) -> [Post]? { guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil } var posts: [Post] = [] for jsonObject in jsonArray { if let post = Post(json: jsonObject) { posts.append(post) } } return posts } }
For those who have already figured out how to work with flatMap, the getArray
function can be written like this:
static func getArray(from jsonArray: Any) -> [Post]? { guard let jsonArray = jsonArray as? Array<[String: Any]> else { return nil } return jsonArray.flatMap { Post(json: $0) } }
We learned how to send a request and receive a response in the form of JSON using responseJSON . Now we analyze in what form we can get the answer.
The answer will come to us in the form of Data . Often, pictures come this way, but even our previous request can be received in the form of Data:
request("http://jsonplaceholder.typicode.com/posts").responseData { responseData in switch responseData.result { case .success(let value): guard let string = String(data: value, encoding: .utf8) else { return } print(string) case .failure(let error): print(error) } }
In the example, we get the answer and convert it to a string. It is inconvenient to receive data from it, as from a Dictionary, but there are parsers that will make an object out of stock.
Everything is simple here. The answer will come in the form of a JSON string. In fact, it does what we wrote above in responseData :
request("http://jsonplaceholder.typicode.com/posts").responseString { responseString in switch responseString.result { case .success(let value): print(value) case .failure(let error): print(error) } }
You can say this is the basic method. It does not process the data from the server in any way, gives it out in the form in which they came. It does not have a result
property, and therefore a construction like switch response.result
will not work here. Everything will have to be done manually. We rarely need it, but you need to know about it.
request("http://jsonplaceholder.typicode.com/posts").response { response in guard let data = response.data, let string = String(data: data, encoding: .utf8) else { return } print(string) }
A string is displayed if the answer came without error.
There is another method .responsePropertyList
. It is needed to get the parsed plist file. I have not used it yet and have not found a test server to give an example. Just know that it is, or you can deal with it yourself by analogy with others.
Sometimes we can get a big response from the server, for example, when we download a photo, and we need to display the download progress. For this, request has a downloadProgress
method:
Instead of https://s-media-cache-ak0.pinimg.com/originals/ef/6f/8a/ef6f8ac3c1d9038cad7f072261ffc841.jpg
you can insert any link to the photo. It is desirable more so that the request is not executed instantly and you see the process itself.
request("https://s-media-cache-ak0.pinimg.com/originals/ef/6f/8a/ef6f8ac3c1d9038cad7f072261ffc841.jpg") .validate() .downloadProgress { progress in print("totalUnitCount:\n", progress.totalUnitCount) print("completedUnitCount:\n", progress.completedUnitCount) print("fractionCompleted:\n", progress.fractionCompleted) print("localizedDescription:\n", progress.localizedDescription) print("---------------------------------------------") } .response { response in guard let data = response.data, let image = UIImage(data: data) else { return } print(image) }
The Progress
class is a standard library class.
The logs will display the progress in the form of blocks:
totalUnitCount: 2113789 completedUnitCount: 2096902 fractionCompleted: 0.992011028536907 localizedDescription: 99% completed
We can divide the completedUnitCount into totalUnitCount and get a number from 0 to 1 that will be used in the UIProgressView , but we have already done it in the fractionCompleted property.
To see the picture itself, put the breakpoint on the line with print(image)
and click on Quick Look (the button with the eye) in the debug panel:
! [Debug console] (/ Users / zdaecqzezdaecq / Downloads / Working with queries using Alamofire / image_quick_look.png)
The simplest creation of an object on the server looks like this:
let params: [String: Any] = [ "title": "new post", "body": "some news", "userId": 10 ] request("http://jsonplaceholder.typicode.com/posts", method: .post, parameters: params).validate().responseJSON { responseJSON in switch responseJSON.result { case .success(let value): guard let jsonObject = value as? [String: Any], let post = Post(json: jsonObject) else { return } print(post) case .failure(let error): print(error) } }
id is not passed because the server must assign it itself. In general, the necessary parameters must be written in the documentation for creating each object.
When updating an object, its id is often written not in the parameter, but in the request path ( ~/posts/1
):
let params: [String: Any] = [ "title": "new post", "body": "some news", "userId": 10 ] request("http://jsonplaceholder.typicode.com/posts/1", method: .put, parameters: params).validate().responseJSON { responseJSON in switch responseJSON.result { case .success(let value): guard let jsonObject = value as? [String: Any], let post = Post(json: jsonObject) else { return } print(post) case .failure(let error): print(error) } }
Of course, they can also do this via a parameter, but this will not be by REST. More about REST in the article on the habr:
This is how the photo upload to the server looks like:
let image = UIImage(named: "some_photo")! let data = UIImagePNGRepresentation(image)! let httpHeaders = ["Authorization": "Basic YWNjXzE4MTM2ZmRhOW*****A=="] upload(multipartFormData: { multipartFormData in multipartFormData.append(data, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg") }, to: "https://api.imagga.com/v1/content", headers: httpHeaders, encodingCompletion: { encodingResult in switch encodingResult { case .success(let uploadRequest, let streamingFromDisk, let streamFileURL): print(uploadRequest) print(streamingFromDisk) print(streamFileURL ?? "streamFileURL is NIL") uploadRequest.validate().responseJSON() { responseJSON in switch responseJSON.result { case .success(let value): print(value) case .failure(let error): print(error) } } case .failure(let error): print(error) } })
Awful isn't it?
Let's see what is responsible for what.
I threw a photo named some_photo into Assets.xcassets
Create an image object and convert it to Data :
let image = UIImage(named: "some_photo")! let data = UIImagePNGRepresentation(image)!
Create a dictionary to transfer the authorization token:
let httpHeaders = ["Authorization": "Basic YWNjXzE4MTM2ZmRhOW*****A=="]
This is necessary because www.imagga.com service requires authorization to upload a picture.
To get your token, you just need to register on their website and copy it from your profile at the link: https://imagga.com/profile/dashboard
Before we used the request
method. Here is the upload
method. The first parameter is the cougher to attach our image:
upload(multipartFormData: { multipartFormData in multipartFormData.append(data, withName: "imagefile", fileName: "image.jpg", mimeType: "image/jpeg") }
The following parameters are URLs and headers :
to: "https://api.imagga.com/v1/content", headers: httpHeaders
Next comes the coder with the coded request:
encodingCompletion: { encodingResult in switch encodingResult { case .success(let uploadRequest, let streamingFromDisk, let streamFileURL): print(uploadRequest) print(streamingFromDisk) print(streamFileURL ?? "streamFileURL is NIL") ... case .failure(let error): print(error) } })
From it we can get a request ( uploadRequest ), and two variables necessary for streaming (stream) files.
I won't talk about streams, it's quite a rare thing. For now, you simply see that these two variables are false and nil, respectively.
Next, we must send a request in the form familiar to us:
uploadRequest.validate().responseJSON() { responseJSON in switch responseJSON.result { case .success(let value): print(value) case .failure(let error): print(error) } }
When you receive your token, insert your photo and complete the query, the result will be as follows:
{ status = success; uploaded = ( { filename = "image.jpg"; id = 83800f331a7f97e41e0f0b70bf7847bd; } ); }
The filename may not be different, but the id will be.
We got acquainted with the Alamofire framework, dealt with the request method, sending requests, processing the response, parsing a positive response, receiving information about the progress of the request. We made some simple requests and learned how to upload photos to the server with authorization.
Source: https://habr.com/ru/post/330760/
All Articles