Three steps to write an iOS network request library yourself

Three steps to write an iOS network request library yourself

Code example: https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary

Open source project: Pitaya, an HTTP request library suitable for uploading large files: https://github.com/johnlui/Pitaya

In this series of articles, we will try to build our own network request library using NSURLSession technology.

Introduction to NSURLSession

NSURLSession is a new network request interface introduced in iOS7, which was introduced in detail in WWDC2013. Here is a slide describing its structure:

When the application is in the foreground, NSURLSession is no different from NSURLConnection, but Background Session is more flexible after the program switches to the background.

Try NSURLSession

Preparation

Create a new single-page application called BuildYourHTTPRequestLibrary and place a button called Request in the center of the page:

Drag and bind the Touch Up Inside event:

Using NSURLSession

Fill the mainButtonBeTapped function with the following code:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com" )!)
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  6. println(string)
  7. })
  8. task.resume()
  9. }

     

Successful use!

Feeling Asynchronous

asynchronous

Rewrite the code of the mainButtonBeTapped function:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSURLRequest(URL: NSURL(string: "http://baidu.com" )!)
  4. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  5. println( "just wait for 5 seconds!" )
  6. sleep( 5 )
  7. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  8. println(string)
  9. })
  10. task.resume()
  11. }

Try again. There is a five-second interval between the two prints. The main thread is not blocked, proving that NSURLSession is executed asynchronously.

block

Try clicking it multiple times and we can see it execute every five seconds until all are finished.

NSURLSession uses an "asynchronous blocking" model, that is, all requests enter thread 2# for execution after being issued, and are executed in blocking queue mode within thread 2#.

#p#

Open source project: Pitaya, an HTTP request library suitable for uploading large files: https://github.com/johnlui/Pitaya

In this chapter, we will try to use a class to encapsulate our previous code, and try to add the function of dynamically adding HTTP parameters (params), and then encapsulate a powerful interface.

[[135519]]

Basic package

Basic preparation

Create a new empty Swift file, name it Network.swift, write a Network class in it, and then write a static method request():

  1. class Network{
  2. static func request() {
  3. let session = NSURLSession .sharedSession()
  4. let request = NSURLRequest (URL: NSURL(string: "http://baidu.com")!)
  5. let task = session .dataTaskWithRequest(request, completionHandler: { (data, response, error) - > Void in
  6. println("just wait for 5 seconds!")
  7. sleep(5)
  8. let string = NSString (data: data, encoding: NSUTF8StringEncoding)
  9. println(string)
  10. })
  11. task.resume()
  12. }
  13. }

Modify the button function in ViewController:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request()
  3. }

Run the project and click the button. The effect is the same as before.

Custom HTTP method and URL

Modify the request() method and pass in the HTTP method and URL:

  1. static func request(method: String, url: String) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!)
  4. request.HTTPMethod = method
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  6. println( "just wait for 5 seconds!" )
  7. sleep( 5 )
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  9. println(string)
  10. })
  11. task.resume()
  12. }

Modify the previous function call:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request( "GET" , url: "http://baidu.com" )
  3. }

Run the project and click the button. The effect is the same as before.

Use closures to handle request results

Functions are first-class citizens in Swift. Closures can be used as function parameters and return values, which is very powerful. Next, we use closures to process the return value of network requests. Modify the request() method and pass in a closure:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let session = NSURLSession.sharedSession()
  3. let request = NSMutableURLRequest(URL: NSURL(string: url)!)
  4. request.HTTPMethod = method
  5. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  6. callback(data: data, response: response, error: error)
  7. })
  8. task.resume()
  9. }

Use closures to process results in the previous function call:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request( "GET" , url: "http://baidu.com" ) { (data, response, error) -> Void in
  3. println( "just wait for 5 seconds!" )
  4. sleep( 5 )
  5. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  6. println(string)
  7. }
  8. }

Run the project and click the button. The effect is the same as before.

Dynamically add Params

GET method

Under the GET method, params is directly attached to the end of the URL after being url-encoded and sent to the server. Modify the request() method and pass in a params dictionary:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. ... ...
  3. }

To process params, we steal the params processing function from Alamofire. If it is a GET method, then add the processed params to the end of the URL. The complete code of the Network class is as follows:

  1. class Network{
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  3. let session = NSURLSession.sharedSession()
  4.  
  5. var newURL = url
  6. if method == "GET" {
  7. newURL += "?" + Network().buildParams(params)
  8. }
  9.  
  10. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
  11. request.HTTPMethod = method
  12.  
  13. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  14. callback(data: data, response: response, error: error)
  15. })
  16. task.resume()
  17. }
  18.  
  19. // Three functions stolen from Alamofire  
  20. func buildParams(parameters: [String: AnyObject]) -> String {
  21. var components: [(String, String)] = []
  22. for key in sorted(Array(parameters.keys), [(String, String)] {
  23. var components: [(String, String)] = []
  24. if let dictionary = value as? [String: AnyObject] {
  25. for (nestedKey, value) in dictionary {
  26. components += queryComponents( "\(key)[\(nestedKey)]" , value)
  27. }
  28. } else   if let array = value as? [AnyObject] {
  29. for value in array {
  30. components += queryComponents( "\(key)" , value)
  31. }
  32. } else {
  33. components.extend([(escape(key), escape( "\(value)" ))])
  34. }
  35.  
  36. return components
  37. }
  38. func escape(string: String) -> String {
  39. let legalURLCharactersToBeEscaped: CFStringRef = ":&=;+!@#$()',*"  
  40. return CFURLCreateStringByAddingPercentEscapes(nil, string, nil, legalURLCharactersToBeEscaped, CFStringBuiltInEncodings.UTF8.rawValue) as String
  41. }
  42. }

Modify the previous function call:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request( "GET" , url: "http://pitayaswift.sinaapp.com/pitaya.php" , params: [ "get" : "Network" ]) { (data, response, error) -> Void in
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  4. println(string)
  5. }
  6. }

http://pitayaswift.sinaapp.com/pitaya.php is the server code I deployed for testing, which will directly return ooxx in ?get=ooxx. Run the project and click the button to see the effect:

POST method

There are several protocols to choose from under the POST method. There is no file upload here, so we use the simpler application/x-www-form-urlencoded method to send the request. Add some code to the request() method:

  1. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let session = NSURLSession.sharedSession()
  3.  
  4. var newURL = url
  5. if method == "GET" {
  6. newURL += "?" + Network().buildParams(params)
  7. }
  8.  
  9. let request = NSMutableURLRequest(URL: NSURL(string: newURL)!)
  10. request.HTTPMethod = method
  11.  
  12. if method == "POST" {
  13. request.addValue( "application/x-www-form-urlencoded" , forHTTPHeaderField: "Content-Type" )
  14. request.HTTPBody = Network().buildParams(params).dataUsingEncoding(NSUTF8StringEncoding)
  15. }
  16.  
  17. let task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  18. callback(data: data, response: response, error: error)
  19. })
  20. task.resume()
  21. }

Modify the previous function call:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. Network.request( "POST" , url: "http://pitayaswift.sinaapp.com/pitaya.php" , params: [ "post" : "Network" ]) { (data, response, error) -> Void in
  3. let string = NSString(data: data, encoding: NSUTF8StringEncoding)
  4. println(string)
  5. }
  6. }

Use POST to send a request, and the server will also return the value of the key post. Run the project and click the button. The result is the same as the previous GET method.

At this point, the interface encapsulation is completed!

#p#

Open source project: Pitaya, an HTTP request library suitable for uploading large files: https://github.com/johnlui/Pitaya

In this article, we will work together to reduce the coupling of the previous code and use the adapter pattern to implement a network API independent of the underlying structure to create a real network request "library".

Reduce coupling

How to reduce coupling

The current clear soup noodle style code is easy to understand, but the function is single and the code is messy. Let's analyze the use process of NSURLSession:

Constructing NSURLRequest

Determine the URL

Determine the HTTP method (GET, POST, etc.)

Adding specific HTTP headers

Filling HTTP Body

Drive the session.dataTaskWithRequest method to start the request

Specific implementation

Create another NetworkManager class under Network, set URL, params, files, etc. as member variables, and initialize them in the constructor:

  1. class NetworkManager {
  2.  
  3. let method: String!
  4. let params: Dictionary let callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void
  5.  
  6. let session = NSURLSession.sharedSession()
  7. let url: String!
  8. var request: NSMutableURLRequest!
  9. var task: NSURLSessionTask!
  10.  
  11. init(url: String, method: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  12. self.url = url
  13. self.request = NSMutableURLRequest(URL: NSURL(string: url)!)
  14. self.method = method
  15. self.params = params
  16. self.callback = callback
  17. }
  18. }

Afterwards, the above analysis

1. Determine the URL

2. Determine the HTTP method (GET, POST, etc.)

3. Add specific HTTP headers

4. Fill in the HTTP Body

Encapsulate the first three steps into one function, encapsulate the last step into one function, and then encapsulate the code that drives session.dataTaskWithRequest into one function:

  1. func buildRequest() {
  2. if self.method == "GET" && self.params.count > 0 {
  3. self.request = NSMutableURLRequest(URL: NSURL(string: url + "?" + buildParams(self.params))!)
  4. }
  5.  
  6. request.HTTPMethod = self.method
  7.  
  8. if self.params.count > 0 {
  9. request.addValue( "application/x-www-form-urlencoded" , forHTTPHeaderField: "Content-Type" )
  10. }
  11. }
  12. func buildBody() {
  13. if self.params.count > 0 && self.method != "GET" {
  14. request.HTTPBody = buildParams(self.params).nsdata
  15. }
  16. }
  17. func fireTask() {
  18. task = session.dataTaskWithRequest(request, completionHandler: { (data, response, error) -> Void in
  19. self.callback(data: data, response: response, error: error)
  20. })
  21. task.resume()
  22. }

Then use a unified method to drive the above three functions to complete the request:

  1. func fire() {
  2. buildRequest()
  3. buildBody()
  4. fireTask()
  5. }

At the same time, don't forget the three parse params functions stolen from Alamofire, and put them in this class. So far, the work of reducing coupling is basically completed, and then we start to encapsulate the "network API".

Encapsulating "network API" using the adapter pattern

Understanding the Adapter Pattern

The adapter pattern is a design pattern that is easy to understand: my app needs a function to get the string returned by a URL. I currently choose Alamofire, but the developing Pitaya looks good. I want to replace it with Pitaya in the future, so I encapsulate a layer of my own network interface to shield the underlying details. When the time comes, I only need to modify this class, and there is no need to go deeper into the project to change so many interface calls.

The adapter pattern sounds high-sounding, but it is actually a design pattern that we use very commonly in daily coding.

Do it!

Modify the code of the Network class to:

  1. class Network{
  2. static func request(method: String, url: String, params: Dictionary = Dictionary(), callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  3. let manager = NetworkManager(url: url, method: method, params: params, callback: callback)
  4. manager.fire()
  5. }
  6. }

Done!

Encapsulating multi-level interfaces

Interface without params:

  1. static func request(method: String, url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: method, callback: callback)
  3. manager.fire()
  4. }

Two get interfaces (with and without params):

  1. static func get(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: "GET" , callback: callback)
  3. manager.fire()
  4. }
  5. static func get(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  6. let manager = NetworkManager(url: url, method: "GET" , params: params, callback: callback)
  7. manager.fire()
  8. }

Two post interfaces (with and without params):

  1. static func post(url: String, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  2. let manager = NetworkManager(url: url, method: "POST" , callback: callback)
  3. manager.fire()
  4. }
  5. static func post(url: String, params: Dictionary, callback: (data: NSData!, response: NSURLResponse!, error: NSError!) -> Void) {
  6. let manager = NetworkManager(url: url, method: "POST" , params: params, callback: callback)
  7. manager.fire()
  8. }

Test interface

Modify the calling code in ViewController to test the multi-level API:

  1. @IBAction func mainButtonBeTapped(sender: AnyObject) {
  2. let url = "http://pitayaswift.sinaapp.com/pitaya.php"  
  3.  
  4. Network.post(url, callback: { (data, response, error) -> Void in
  5. println( "POST 1 request successful" )
  6. })
  7. Network.post(url, params: [ "post" : "POST Network" ], callback: { (data, response, error) -> Void in
  8. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  9. println( "POST 2 request successful " + string)
  10. })
  11.  
  12. Network.get(url, callback: { (data, response, error) -> Void in
  13. println( "GET 1 request successful" )
  14. })
  15. Network.get(url, params: [ "get" : "POST Network" ], callback: { (data, response, error) -> Void in
  16. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  17. println( "GET 2 request successful " + string)
  18. })
  19.  
  20. Network.request( "GET" , url: url, params: [ "get" : "Request Network" ]) { (data, response, error) -> Void in
  21. let string = NSString(data: data, encoding: NSUTF8StringEncoding) as! String
  22. println( "Request successful " + string)
  23. }
  24. }

Run the project and click the button to see the effect:

Multi-level API encapsulation is successful!

[Editor: chenqingxiang TEL: (010) 68476606]

<<:  Nintendo's next-generation console will use Android system

>>:  Write an iOS network request library by yourself - encapsulation interface

Recommend

Essential for developers: 2018 Android SDK tool recommendations

Pimple Popper is an app that pops virtual pimples...

2022 Brand Marketing Content Trends

Short videos have become an irreplaceable form of...

If the earthworm became pink and grew legs, would it be cute?

This pink creature in the picture looks very stra...

Introduction to Qinggua Media’s intelligent advertising delivery system!

Introduction to Qinggua Media Intelligent Adverti...

Why can’t Zhou Hongyi forget about search?

2015 was still a high-profile year for Zhou Hongy...

Peak has passed, consumer expectations for wearable devices plummet

[[146121]] Interest in the Apple Watch and other ...