Swfit学习ios技术交流Android技术交流

iOS架构篇-3 网络接口封装

2021-03-10  本文已影响0人  浪人残风

iOS架构篇-3 网络接口封装

关键字:iOS,网络接口封装,Alamofire,swift

网络接口API通常都需要自己封装一套管理,这里以swift版的Alamofire为例.

实现功能:

1.暴露参数请求地址url、请求方法method、请求参数params、请求头header、请求响应response(响应数据、响应头responseHeader)、https请求证书
2.支持get、post、put、delete、head、patch、文件上传下载、https证书
3.本地开发环境、测试环境、生产环境切换

1.添加Alamofire库

source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
platform :ios, '9.0'
workspace 'ProjectApp'
inhibit_all_warnings!
use_frameworks!
install! 'cocoapods', :deterministic_uuids => false
abstract_target 'common' do
  pod 'Alamofire', '~> 4.9.1'
  target :'ProjectApp' do
    project 'ProjectApp'
    pod 'RxSwift', '6.0.0'
    pod 'RxCocoa', '6.0.0'
  end
  target :'ComponentNetwork' do
    project '../ComponentNetwork/ComponentNetwork'
  end
end

Alamofire需要同时添加到ComponentNetwork、ProjectApp两个project,不能只添加到ComponentNetwork project,这里用cocopods的abstract_target为多个project添加同样的库, CocoPods的具体使用参考iOS技术篇-CocoaPods

2.先定义需要暴露的API接口

// 定义别名

public typealias CPMethod = Alamofire.HTTPMethod
public typealias CPRespHandle = (_ suc: Bool, _ data: String?, _ error: Error?, _ responseHeader: [AnyHashable : Any]?) -> Void
public typealias CPProgressHandle = (_ progress: Progress) -> Void
public typealias CPDownloadRespHandle = (_ suc: Bool, _ resp: Data?, _ destinationURL: URL?, _ error: Error?, _ responseHeader: [AnyHashable : Any]?) -> Void

// 定义方法
public func CPRequest(url: String, requestMethod: CPMethod, params: [String: Any]? = nil, header: [String: String]? = nil, respHandle: CPRespHandle? = nil, cer: Data? = nil) {}
public func CPUploadFile(url: String, header: [String: String]? = nil, params: [String: Any]? = nil, file: AnyObject, fileName: String, key: String, mimeType: String, uploadingHandle:@escaping CPProgressHandle, respHandle: CPRespHandle? = nil) -> Void {}
public func CPDownloadFile(url: String, header: [String: String]? = nil, filePath:URL, downloaDingHandle: CPProgressHandle? = nil, respHandle: CPDownloadRespHandle? = nil) -> Void {}

3.封装Alamofire

针对暴露的API定义方法

//
//  AFService.swift
//  
//
//  Created by wrs on 2018/7/13.
//  Copyright © 2018年 wrs. All rights reserved.
//

import UIKit
import Alamofire
public let shareSessionManager: SessionManager = {
    let configuration = URLSessionConfiguration.default
    configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
    configuration.timeoutIntervalForRequest = 20
    return SessionManager(configuration: configuration)
}()
func AFRequest(url: String, requestMethod: CPMethod, params: [String: Any]? = nil, header: [String: String]? = nil, respHandle: CPRespHandle? = nil, cer localCer: Data?)
{
   let manager = shareSessionManager
    manager.delegate.sessionDidReceiveChallenge = { session, challenge in
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { // 认证服务器!
            if let localCertificateData = localCer { // 有证书就认证
                let serverTrust:SecTrust = challenge.protectionSpace.serverTrust!
                let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)!
                let remoteCertificateData
                    = CFBridgingRetain(SecCertificateCopyData(certificate))!
                if (remoteCertificateData.isEqual(localCertificateData) == true) { // 证书一致
                    let credential = URLCredential(trust: serverTrust)
                    challenge.sender?.use(credential, for: challenge)
                    return (.useCredential,
                            URLCredential(trust: serverTrust))
                } else { // 证书不对
                    return (.cancelAuthenticationChallenge, nil)
                }
            } else { // 没有证书就不校验
                return (.performDefaultHandling, nil)
            }
        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { //认证客户端证书
//            let identityAndTrust:IdentityAndTrust = extractIdentity();
//            let urlCredential:URLCredential = URLCredential(
//                identity: identityAndTrust.identityRef,
//                certificates: identityAndTrust.certArray as? [AnyObject],
//                persistence: URLCredential.Persistence.forSession);
//
//            return (.useCredential, urlCredential);
            return (.performDefaultHandling, nil)
        } else { // 其它情况(不接受认证)
            return (.cancelAuthenticationChallenge, nil)
        }
    }
    var encoding: ParameterEncoding = URLEncoding.methodDependent
    switch requestMethod {
    case .post:
        encoding = JSONEncoding.default
    default:
        break

    }
    let request: DataRequest = manager.request(url, method: requestMethod, parameters: params, encoding: encoding, headers: header)
    request.responseString(queue: nil, encoding: String.Encoding.utf8) { (response: DataResponse<String>) in
        if let handle = respHandle {
            switch response.result {
            case .success:
                handle(true, response.result.value, nil, response.response?.allHeaderFields)
            case .failure(let error):
                handle(false, response.result.value, error, nil)
            }
        }
    }
//    request.responseString { (response: DataResponse<String>) in
//        if let handle = respHandle {
//            switch response.result {
//            case .success:
//                handle(true, response.result.value, nil)
//            case .failure(let error):
//                 handle(true, response.result.value, error)
//            }
//        }
//    }
//    request.responseJSON { (response: DataResponse<Any>) in
//        if let handle = respHandle {
//            switch response.result {
//            case .success:
//                handle(true, response.result.value, nil)
//            case .failure(let error):
//                if let _ = response.data {
//                    let respStr =  String(data: response.data!, encoding: .utf8)
//                    handle(false, respStr, error)
//                } else {
//                    handle(false, nil, error)
//                }
//            }
//        }
//    }

}
func AFUploadFile(url: String, header: [String: String]? = nil, params: [String: Any]? = nil, file: AnyObject?, fileName: String, key: String, mimeType: String, uploadingHandle:@escaping CPProgressHandle, respHandle: CPRespHandle? = nil) -> Void {
     if let aUrl = URL(string: url) {
        Alamofire.upload(multipartFormData: { (formData) in
            if let f = file {
                if f is Data {
                    formData.append(file as! Data, withName: key, fileName: fileName, mimeType: mimeType)
                } else if f is String {
                    formData.append(URL(fileURLWithPath: file as! String), withName: key, fileName: fileName, mimeType: mimeType)
                } else if f is UIImage {
                    if let data = (file as! UIImage).pngData() {
                        formData.append(data, withName: key, fileName: fileName, mimeType: mimeType)
                    }
                }
            }
            if let para = params {
                for (key, value) in para {
                    if let content = value as? String {
                        if let valueData = content.data(using: String.Encoding.utf8) {
                            formData.append(valueData, withName: key)
                        }
                    } else if let file = value as? Data {
                        formData.append(file , withName: key, fileName: fileName, mimeType: mimeType)
                    } else if let file = value as? UIImage {
                        if let data = (file).pngData() {
                            formData.append(data, withName: key, fileName: fileName, mimeType: mimeType)
                        }
                    }
                }
            }
        }, usingThreshold: SessionManager.multipartFormDataEncodingMemoryThreshold, to:aUrl, method: .post, headers: header) { (encodingResult: SessionManager.MultipartFormDataEncodingResult) in
            switch encodingResult {
            case .success(let uploadRequest, _, _):

                uploadRequest.uploadProgress(closure: { (progress: Progress) in
                    uploadingHandle(progress)

                })
                uploadRequest.responseString(completionHandler: { (dataResponse) in
                    if let handle = respHandle {
                        handle(true, dataResponse.result.value, nil, dataResponse.response?.allHeaderFields)
                    }
                })
            case .failure(let error):
                if let handle = respHandle {
                    handle(false, nil, error, nil)
                }
            }
        }
     } else {
        if let handle = respHandle {
            handle(false, nil, nil, nil)
        }
    }
}
func AFDownloadFile(url: String, header: [String: String]? = nil, filePath:URL, downloaDingHandle: CPProgressHandle? = nil, respHandle: CPDownloadRespHandle? = nil) -> Void {
    if let aUrl = URL(string: url) {
    let destination: DownloadRequest.DownloadFileDestination = { (resultUrl:URL, resp:HTTPURLResponse) in
        let fileUrl = filePath.appendingPathComponent(resp.suggestedFilename!)
        return (fileUrl, DownloadRequest.DownloadOptions.removePreviousFile)
    }
    Alamofire.download(aUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: header, to: destination).downloadProgress { (progress) in
            if let handle = downloaDingHandle {
                handle(progress)
            }
        }.responseData { (response:DownloadResponse<Data>) in
              if let handle = respHandle {
                          switch response.result {
                          case .success:
                              handle(true, response.result.value, response.destinationURL, nil, response.response?.allHeaderFields)
                          case .failure(let error):
                              handle(false, response.result.value, response.destinationURL, error, nil)
                          }
                      }
        }
    } else {
        if let handle = respHandle {
            handle(false, nil, nil, nil, nil)
        }
    }
}
//获取客户端证书相关信息
func extractIdentity() -> IdentityAndTrust {
    var identityAndTrust:IdentityAndTrust!
    var securityError:OSStatus = errSecSuccess
    let path: String = Bundle.main.path(forResource: "client", ofType: "p12")!
    let PKCS12Data = NSData(contentsOfFile:path)!
    let key : NSString = kSecImportExportPassphrase as NSString
    let options : NSDictionary = [key : "xxxxxxxxxxxx"] //客户端证书密码
    var items : CFArray?
    securityError = SecPKCS12Import(PKCS12Data, options, &items)
    if securityError == errSecSuccess {
        let certItems:CFArray = items!;
        let certItemsArray:Array = certItems as Array
        let dict:AnyObject? = certItemsArray.first;
        if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
            // grab the identity
            let identityPointer:AnyObject? = certEntry["identity"];
            let secIdentityRef:SecIdentity = identityPointer as! SecIdentity
            // grab the trust
            let trustPointer:AnyObject? = certEntry["trust"]
            let trustRef:SecTrust = trustPointer as! SecTrust
            // grab the cert
            let chainPointer:AnyObject? = certEntry["chain"]
            identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef,
                                                trust: trustRef, certArray:  chainPointer!)
        }
    }
    return identityAndTrust;
}

//定义一个结构体,存储认证相关信息
struct IdentityAndTrust {
    var identityRef:SecIdentity
    var trust:SecTrust
    var certArray:AnyObject
}

4.抽取接口参数,增加参数API

public protocol CPRequestParams {
    var url: String { get }
    var method: CPMethod { get }
    var params: [String : Any]? { get }
    var headers: [String: String]? { get }
    var cer: Data? { get }
}
public func CPRequest(params: CPRequestParams, respHandle: CPRespHandle? = nil) {
    AFRequest(url: params.url, requestMethod: params.method, params: params.params, header: params.headers, respHandle: respHandle, cer: params.cer)
}

5.定义业务层API

实现功能:

1)API环境切换,如本地静态配置文件数据、swagger mock数据、开发环境数据、测试环境数据、灰度环境数据、生产环境数据
2)接口请求是否需要HUD Loading
3)上层屏蔽网络出错、后台系统出错、后台业务错误等数据,上层业务只关注正确的接口数据
4)接口返回数据统一模型,不同业务通过范型统一返回
5)支持入参建模
6)debug模式打印接口数据,方便调试和数据监听
7)所有接口统一加上固定请求头,登录后再加上token
业务层接口API定义:

/// 业务接口请求
/// - Parameters:
///   - params: 业务参数
///   - errorToast: 错误时是否进行错误toast提示
///   - hud: 是否显示请求loading
///   - handle: 业务回调
func request<T: Codable>(params: RequestParams, errorToast: Bool, hud: Bool, handle: @escaping RespHandle<T>)
// API环境切换枚举定义
enum DataEnv {
    case QA(host: String)
    case Staging(host: String)
    case Production(host: String)
    case Mock(host: String)
    case Custom(host: String)
    var host: String {
        var path = ServerProdPath
        switch self {
        case let .QA(host):
            path = host
        case let .Staging(host):
            path = host
        case let .Production(host):
            path = host
        case let .Mock(host):
            path = host
        case let .Custom(host):
            path = host
        }
        return path
    }
}

定义一个API环境变量,通过更改此变量来切换不同环境:

var dataEnv: DataEnv = .QA(host: ServerDevPath)
var ServerProdPath = "http://www.xxxx.cn"
let ServerDevPath = "http://192.168.0.169:8089"

定义业务接口类型,继承CPRequestParams:

enum RequestParams: CPRequestParams{
    case Login(account: String, password: String)
    case Register(user: RegisterEntity)
    var target: RequestParamsTarget {
        get {
            switch self {
            case let .Login(account, password):
                return RequestParamsTarget(suffix: "/login", method: .post, params: [
                    "account": account,
                    "password": password
                ])
            case let .Register(user):
                var params: [String: Any]? = nil
                if let data: Data = try? JSONEncoder().encode(user), let dic = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
                    params = dic
                }
                return RequestParamsTarget(suffix: "/register", method: .post, params: params)
            }
        }
    }
    var url: String {
        self.target.url
    }
    var method: CPMethod {
        self.target.method
    }
    var params: [String : Any]? {
        self.target.params
    }
    var headers: [String : String]? {
        getAppHeader()
    }
    var cer: Data? {
        nil
    }
}

定义接口返回数据模型

class RespEntity<T: Codable> {
    var code: Int?
    var msg: String?
    var data: T?
    init(dic: [String : Any]) {
        if let value = dic["code"] as? Int {
            self.code = value
        }
        if let value = dic["msg"] as? String {
            self.msg = value
        }
        if let tempJson: Any = dic["data"] {
            if !(tempJson is NSNull) { // 防止data为null
                if tempJson is String  { // T -> String
                    self.data = tempJson as? T
                } else if tempJson is Double { // T -> Double, data为数字时,T不能是Float,使用Double通用
                    self.data = tempJson as? T
                } else {
                    if let tempData: Data = try? JSONSerialization.data(withJSONObject: tempJson, options: JSONSerialization.WritingOptions.prettyPrinted){
                        if let model: T = try? JSONDecoder().decode(T.self, from: tempData) {
                            self.data = model
                        }
                    }
                }
            }
        }
    }
    var suc: Bool {
        var flag = false
        if let c = self.code, c == 10000 {
            flag = true
        }
        return flag
    }
}

后台一般所有数据都返回统一模型,如:

{
    "code": 10000,
    "msg":"aaaa",
    "data":[{
        "date":"2020-12-01",
        "title": "手机充值"
    }]
}

注意这里的模型转换需要兼容后台返回错误的模型,防止后台出错导致app出现闪退或未知异常

调用例子

       request(params: .Login(account: "", password: ""), errorToast: true, hud: true) { (suc: Bool, model: RespEntity<[VideoEntity]>?, error: Error?) in
            if let flag = model?.suc, flag == true {
                if let list = model?.data {
                    DPrint(message:list)
                } else {
                    DPrint(message:"no list")
                }
            }
        }
        

//        let registerEntity: RegisterEntity = RegisterEntity()
//        registerEntity.username = "wrs"
//        registerEntity.password = "123456"
//        request(params: .Register(user: registerEntity), errorToast: true, hud: true) { (suc, model:RespEntity<String>?, error) in
//            
//        }

源码下载

上一篇下一篇

猜你喜欢

热点阅读