This paper mainly deals with two issues:

  • Requested Loading to expand processing
  • Encapsulating URLSession returns the Observable sequence

1. Request Loading to expand processing

As for the Loading component, I have packaged it and published it on Github, RPToastView. Please refer to readme.md for the usage method. Here you only need to make an extension to UIViewController and use an attribute to control the display and hiding of the Loading component. The core code is as follows:

extension Reactive where Base: UIViewController { public var isAnimating: Binder<Bool> { return Binder(self.base, binding: {(vc, active) in if active == true {// display Loading View} else {// hide Loading View}})}}Copy the code

Here we pass true to isAnimating to display a LoadingView, false to hide a LoadingView,

2. Why not Moya

Github Moya

Moya is another layer encapsulated on the basis of commonly used Alamofire, but I did not use Moya in the project, mainly based on the following three considerations:

  • (1) Reasons for Moya itself: Moya is perfectly packaged, which brings great convenience to developers, but too much packaging will inevitably lead to decreased scalability
  • (2) Internal reasons: Since there is no unified standard for the background interface of our company, the data structure returned by the background of different modules is different, so I have to deal with it separately
  • (3) Considering the size of App package: Importing too many third-party open source libraries will inevitably make App package synchronously larger, which is not what I expected

So my final choice is RxSwift+URLSession+SwiftyJSON.

3. Use of RxSwift

For network requests, the common open source library in OC is AFNetworking, and in Swift we use Alamofire. As of December 2020, the number of STARS of AFNetworking is 33.1K, and that of Alamofire is 35K. From this data point of view, Swift, while a new language, is more popular with developers.

The simplest way to network requests Personally feel successful or unsuccessful in returning through Closures with Alamofire:

func post(with body: [String : AnyObject], _ path: String, with closures: @escaping ((_ json: [String : AnyObject],_ failure : String?) -> Void))
Copy the code

If we need to call the interface again for the user Socket server data after a successful login, the request code will be Closures nested within Closures.

 RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI) { (siginInfo, errorMsg) in
   if let errorMsg = errorMsg {
                
   } else {
       RPAuthRemoteAPI().socketInfo(with: ["username":""], userInfoAPI) { (userInfo, userInfoErrorMsg) in
          if let userInfoErrorMsg = userInfoErrorMsg {
                        
          } else {
                        
         }
      }
   }
}
Copy the code

RxSwift allows multiple requests to be processed together. See RxSwift: Wait for multiple concurrent tasks to complete

  • 1, more intuitive and concise RxSwift

Using RxSwift, which returns an Observable, also avoids the problem of nested callbacks.

The above code is written in RxSwift, which is more logical:

let _ = RPAuthRemoteAPI().signIn(with: ["username":"","password":""], signInAPI) .flatMap({ (returnJson) in return RPAuthRemoteAPI().userInfo(with: [" username ":" "], userInfoAPI)}). The subscribe {(json) in print (" user information -- -- -- -- -- -- -- -- -- -- - : \ (json) ")} onError: { (error) in } onCompleted: { } onDisposed: { }Copy the code
  • 2. Process the data returned by the server

Generally, a request can be made in three ways:

  • The data structure returned by the server when the request succeeds

  • The request to the server succeeds, but the returned data is abnormal, such as parameter error, encryption processing exception, login timeout, etc

  • The request was not successful. Handle the request according to the returned error code

Create a protocol to manage requests, here need to know the request API, HTTP mode, required parameters, etc., code as follows:

Public protocol Request {var path: String {get} var method: HTTPMethod {get} var parameter: [String: AnyObject]? {get} var host: String {get} }Copy the code

You may not need any parameters when making a request, so here we do an extension processing with parameter as an optional parameter:

extension Request {
    var parameter: [String: AnyObject] {
        return [:]
    }
}
Copy the code

If the request succeeds, the server will return the following data structure:

{
  "access_token" : "b6298027-a985-441c-a36c-d0a362520896",
  "user_id" : "1268805326995996673",
  "dept_id" : 1,
  "license" : "made by tsn",
  "scope" : "server",
  "token_type" : "bearer",
  "username" : "198031",
  "expires_in" : 19432,
  "refresh_token" : "692a1b6e-051f-424d-bd2e-3a9ccec8d4f2"
}
Copy the code

The data structure returned when the request succeeds but an exception occurs:

{"returnCode" : "601", "returnMsg" : "Login invalid ",}Copy the code

Create a new signinModel.swift as the model

public struct SignInModel {
    public let username,dept_id,access_token,token_type,user_id,scope,refresh_token,expires_in,license: String   
}

Copy the code

Convert the returned SwiftyJSON object to a Model object

extension SignInModel { public init? (json: JSON) { username = json["username"].stringValue dept_id = json["dept_id"].stringValue access_token = json["access_token"].stringValue token_type = json["token_type"].stringValue user_id = json["user_id"].stringValue scope  = json["scope"].stringValue refresh_token = json["refresh_token"].stringValue expires_in = json["expires_in"].stringValue license = json["license"].stringValue } }Copy the code

When the request is successful, the Data obtained by the server is converted to SwiftyJSON instance and then to SignInModel in ViewModel.

If the request succeeds but the returned data is abnormal, you can send a friendly prompt to the user based on the code and Message message returned in the background.

An enum can be defined to handle a request server failure:

Public enum RequestError: Error { case unknownError case connectionError case timeoutError case authorizationError(JSON) case notFound case serverError }Copy the code

Make the request and return an Observable

RxSwift also extends the URLSession provided by the system to allow developers to use it directly:

URLSession.shared.rx.response(request: urlRequest).subscribe(onNext: { (response, data) in
            
}).disposed(by: disposeBag)
Copy the code

An Observable queue is returned regardless of whether the Request succeeds or fails. A **

generic is used here. Any type that follows AuthRemoteProtocol** can implement network requests.

public protocol AuthRemoteProtocol {
  func post<T: Request>(_ r: T) -> Observable<JSON>
}
Copy the code

When launching a request, we need to do some request configuration for URLSession, such as setting header, body, URL, timeout and request mode, so as to successfully complete a request. Header and timeout are usually fixed. The body and URL parameters must be a Request compliant object. The core code is as follows:

Public func post<T: Request>(_ r: T) -> Observable<JSON> public func post<T: Request>(_ r: T) -> Observable<JSON> r.host.appending(r.path)) else { return .error(RequestError.unknownError) } var headers: [String : String]? Var urlRequest = urlRequest (url: path, cachePolicy:. UseProtocolCachePolicy, timeoutInterval: 30) / / set the header urlRequest. AllHTTPHeaderFields = headers / / request of setting up the urlRequest. HttpMethod = r.m ethod. RawValue return Observable.create { (observer) -> Disposable in URLSession.shared.dataTask(with: UrlRequest) {(data, response, error) in // Process according to the code returned by the server and pass it to ViewModel}. Resume () return Disposables. Create {}}}Copy the code

Generally, it is agreed with the server that when the code returned by the server is 200, the server considers the request successful and normally returns data. When other codes are returned, the server will process the data according to the returned code. The final code looks like this:

Request struct SigninRequest: Request {typeAlias Response = SigninRequest var parameter: [String: AnyObject]? var path: String var method: HTTPMethod = .post var host: String { return __serverTestURL } } public enum RequestError: Error { case unknownError case connectionError case timeoutError case authorizationError(JSON) case notFound case ServerError} public protocol AuthRemoteProtocol {JSON -----> RxSwift func requestData<T: Request>(_ r: T) -> Observable<JSON> } public struct RPAuthRemoteAPI: JSON -----> RxSwift public func post<T: Request>(_ r: T) -> Observable<JSON> { let path = URL(string: r.host.appending(r.path))! var urlRequest = URLRequest(url: path, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30) urlRequest.allHTTPHeaderFields = ["Content-Type" : "application/x-www-form-urlencoded; application/json; charset=utf-8;"]  urlRequest.httpMethod = r.method.rawValue if let parameter = r.parameter { // --> Data let parameterData = parameter.reduce("") { (result, param) -> String in return result + "&\(param.key)=\(param.value as! String)" }.data(using: .utf8) urlRequest.httpBody = parameterData } return Observable.create { (observer) -> Disposable in URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in if let error = error { print(error) observer.onError(RequestError.connectionError) } else if let data = data ,let responseCode = response as? HTTPURLResponse { do { let json = try JSON(data: data) switch responseCode.statusCode { case 200: print("json-------------\(json)") observer.onNext(json) observer.onCompleted() break case 201... 299: observer.onError(RequestError.authorizationError(json)) break case 400... 499: observer.onError(RequestError.authorizationError(json)) break case 500... 599: observer.onError(RequestError.serverError) break case 600... 699: observer.onError(RequestError.authorizationError(json)) break default: observer.onError(RequestError.unknownError) break } } catch let parseJSONError { observer.onError(parseJSONError) print("error on parsing request to JSON : \(parseJSONError)") } } }.resume() return Disposables.create { } } }Copy the code

Call in ViewModel and do what the server returns with the code:

// Display LoadingView self.loading.onNext(true) RPAuthRemoteAPI().post(SigninRequest(parameter: [:], path: Subscribe (onNext: {returnJson in // JSON object convert to Model, and local cache Token self.loading.onnext (true)}, onError: Prompt (by: disposeBag) {errorJson in // fail self.load.onnext (true)}, onCompleted: {// Call completed}). Disposed (by: disposeBag)Copy the code

5. There are problems

Although the above method is based on the implementation of POP, it is easy to expand and maintain the code. But I think there are problems:

  • Over-reliance on RxSwift and SwiftyJSON third-party libraries, if the system version is upgraded, or the authors of these third-party libraries no longer maintain problems, will bring great trouble to our later development and maintenance;

In this paper, the demo:

Github RPChat

Links:

Protocol-oriented programming’s encounter with Cocoa

Sample Music list app

Github RxSwift

RxSwift Chinese website

Mooring who