An overview of the

Almost all projects require web requests because they can present richer content to users and allow us to manage and synchronize data across devices. Web requests will appear everywhere in your project: the launch page, the list page, the login registration… So how to manage organizational network requests is a very important part of the App architecture. There are similar frameworks on Github such as Moya, let’s call it a framework for web frameworks. Moya also has this framework which has been developed for a long time. It is very powerful and the community is always active. There are also derivatives RxMoya and ReactiveMoya. However, AFTER using it, I found that it was too heavy, and I always felt that his separate writing method of path + method + parameter was too complicated. So, in this paper, let’s build a network framework suitable for ourselves step by step.

Tip: For convenience and generality, our web request API is written directly based on Alamofire.

Analysis and Thinking

First let’s look at what the simplest request looks like.

AF.request("https://httpbin.org/get").response { response in
    debugPrint(response)
}
Copy the code

Pretty simple, right? In reality, however, we have to deal with all sorts of complications in a single request, and the code in a single request can be very long (too long to fit on a screen!). So we want to abstract and reuse the logic here as much as possible.

How do you start? The most common way to solve a problem is to see it clearly, then break it down into smaller pieces and tackle it one by one. Let’s first consider 🤔, what a complete request would do:

  1. Encapsulate the URL, Method, and body into oneHTTP RequestObject,
  2. Set requestedHTTP Header
  3. acceptHTTP RequestData back
  4. To deal witherrorresponse code
  5. throughcodableAnd so onraw dataconversionmodelobject
  6. Request retry

In a normal business, 2, 3, 4, and 5 can be abstracted uniformly, and the most common approach is to unify the handle logic with an HTTPClient or APIManager. For 1, the parameters, addresses, and methods of each request are different, so we will still expose them. The result will look something like this:

Warning: The following methods are just for ideas, some code is omitted

class HTTPClient {
  
  var host: String
  
  init(host: String) {
    self.host = host
  }
  
  / / set the timeout
  private let sessionManager: SessionManager = {
    let config = URLSessionConfiguration.default
    config.timeoutIntervalForRequest = 15
    let sessionManager = SessionManager(configuration: config)
    return sessionManager
  }()
  
  / / set the HTTPHeaders
  private var defaultHeader: HTTPHeaders = {
    var defaultHTTPHeaders = SessionManager.defaultHTTPHeaders
    defaultHTTPHeaders["User-Agent"] = "You device user agent"
    defaultHTTPHeaders["Accept-Encoding"] = acceptEncoding
    // Add the token to the header
    return defaultHTTPHeaders
  }()
}

extension HTTPClient {
  @discardableResult
  func requestObject<T: Codable>(path: String,
                                 method: Alamofire.HTTPMethod = .get, parameters:[String:Any?] ? , handler: @escaping(T? , Error?) -> Void) - >Request {
    // json -> model
    return buidRequest(path: path, method: method, parameters: parameters) { [weak self](dataSource, error) in
      if let error = error {
        handler(nil, error)
        return
      }
      // Convert raw data into Model objects through the 'codable' framework
      do {
        let model = trydataSource.data? .mapObject(Response<T>.self).data
        handler(model, nil)}catch let error {
        let parseError = HTTPClientError(code:.decodeError,localDescrition:"parse_error".localized)
        self? .showDecodingError(path: path, error: error) handler(nil, parseError)
      }
    }
  }
}

// MARK: - Private Methods
private extension HTTPClient {
  /// note: data used by codable
  typealias CompletionHandler = ((data: Data? , result:Any?). .Error?). ->Void
  
  @discardableResult
  private func buidRequest(path:String, method: Alamofire.HTTPMethod, parameters:[String:Any?] ? , handler: @escaping CompletionHandler) -> Request {
    
    // filter nil value
    letvalidParas = parameters? .compactMapValues { $0 }
    let request = sessionManager.request(host + path, method: method, parameters: validParas, headers: defaultHeader)
    return request.responseJSON { response in
      // 4. Handle error and Response code
      self.handelResponse(response: response, handler: handler)
    }
  }
}
Copy the code

Our final method for initiating the request looks something like this:

static func auth(from: String, token: String) -> AuthResult? {
  let path = "wp-json/wc/v3/third/party/access/token"
  let parameters = ["from": from, "third_access_token": token]
  return HTTPClient.shared.requestObject(path: path, parameters: parameters)
}
    
Copy the code

RxSwift True Fragrance series 😋

No, RxSwift recommends you all learn it. Reactive programming is really good

How do I support RxSwift

extension HTTPClient: ReactiveCompatible {}

extension Reactive where Base: HTTPClient {
  /// Designated request-making method.
  ///
  /// - Parameters:
  /// - path: url path
  /// - parameters: A dictionary of parameters to apply to a `URLRequest`
  /// - Returns: Response of singleobject.
  func requestObject<T: Codable>(path:String, method: HTTPMethod = .get, parameters:[String:Any?] ?). -> Single<T? > {return Single.create { single in
      let request = self.base.requestObject(path: path, method: method, parameters: parameters, handler: { (model: T? , error)in
        if let error = error {
          single(.error(error))
        } else {
          single(.success(model))
        }
      })
      
      return Disposables.create {
        request.cancel()
      }
    }
  }
}
Copy the code

Retry and request merge

Benefit retries and request merges with RxSwift are very simple.

// request merge
Observable.zip(request1, request2, request3)
  .subscribe(onNext: { (resp1, resp2, resp3) in
  })
  .disposed(by: disposeBag)

// Request a retry
HTTPClient.rx.user()
  .asObservable()
  .catchErrorJustReturn(nil)
  .retry(3, delay: .constant(time: 3))
  .disposed(by: disposeBag)

// RxSwift+Retry
enum DelayOptions {
  case immediate
  case constant(time: Double)
  case exponential(initial: Double, multiplier: Double, maxDelay: Double)
  case custom(closure: (Int) - >Double)}extension DelayOptions {
  func make(_ attempt: Int) -> Double {
    switch self {
    case .immediate: return 0.0
    case .constant(let time): return time
    case .exponential(let initial, let multiplier, let maxDelay):
      // if it's first attempt, simply use initial delay, otherwise calculate delay
      let delay = attempt == 1 ? initial : initial * pow(multiplier, Double(attempt - 1))
      return min(maxDelay, delay)
    case .custom(let closure): return closure(attempt)
    }
  }
}
/// Is mainly used for network request retries. You can set the number of retries, the interval between retries, and the logic of network start retries
/// reference:http://kean.github.io/post/smart-retry
extension ObservableType {
  /// Retries the source observable sequence on error using a provided retry
  /// strategy.
  /// - parameter maxAttemptCount: Maximum number of times to repeat the
  /// sequence. `Int.max` by default.
  /// - parameter didBecomeReachable: Trigger which is fired when network
  /// connection becomes reachable.
  /// - parameter shouldRetry: Always returns `true` by default.
  func retry(_ maxAttemptCount: Int = Int.max,
             delay: DelayOptions,
             didBecomeReachable: Observable<Void> = Reachability.shared.didBecomeReachable,
             shouldRetry: @escaping (Error) -> Bool = { _ in true}) - >Observable<Element> {
    return retryWhen { (errors: Observable<Error>) in
      
      return errors.enumerated().flatMap { attempt,error -> Observable<Void> in
        guard shouldRetry(error),
          maxAttemptCount > attempt + 1 else {
            return .error(error)
        }
        let timer = Observable<Int>
          .timer(RxTimeInterval.seconds(Int(delay.make(attempt + 1))),
                 scheduler: MainScheduler.instance)
          .map { _ in()}return Observable.merge(timer, didBecomeReachable)
      }
    }
  }
}

Copy the code

conclusion

It is not easy for me to do a good job in a highly applicable network architecture. In fact, the complexity of network requests is far more than that. What we have done here is to unify some common logic, and there are many things that are not covered in this paper. Such as

  1. How do you optimize the logic here with the Single Responsibility principle
  2. What do we do when we encounter non-standard data structures returned by the server?
  3. useCodableWhether an Array or Object is returned is treated differently
  4. When we have multiple API addresses such as test environment and formal environment, how do we manage them?

Those are the things we need to solve. I stepped in a lot of holes. It was hard. These questions leave you to think about 😆

reference

Finally, I strongly recommend meow God’s speech on iPlayground in Taiwan

www.youtube.com/watch?v=Xk4…