一个可以快速开发的代码封装框架--PJQuicklyDev2

PJQuicklyDev2 快速开发框架的2.0版本,Swift4.0 Xcode9.1

1.0版本如果有兴趣请移步,相关信息可以参考1.0版本。

PJQuicklyDev2在GitHub上的地址

在看了🐱神的文章面向协议编程与 Cocoa 的邂逅 (上)面向协议编程与 Cocoa 的邂逅 (下)感觉1.0版本的网络与数据解析耦合度太高了,不方便扩展。想要把🐱神的面向协议的思想应用起来,恰巧最近开始看Swift4.0于是就有了快速开发框架2.0版本。主要优化的底层点为网络请求与数据解析。还有tableView网络请求加载数据。

网络请求

本次的网络请求重构的目的在于解耦,网络请求的方式get,post等等,网络请求的具体实现可以随意替换(即低耦合了).

如果定义一个网络发送协议,而让其他具体的类去遵循并且实现具体的网络请求功能:

///网络请求协议
protocol PJClient {
    func send<T: PJRequest>(_ r: T, success: @escaping PJSuccess, fatalError: @escaping PJFatalError)
    func sendRequestForStruct<T: PJRequest>(_ r: T, success: @escaping PJSuccessForStruct<T>, fatalError: @escaping PJFatalError)
}

这样以后哦要替换底层网络具体实现时就很容易了,并且不会影响到现有的网络请求相关业务逻辑.

///默认的网络请求实现结构体
struct PJHttpRequestClient: PJClient {
    
    ///用于网络请求的数据要转成struct类型的模型
    func sendRequestForStruct<T: PJRequest>(_ r: T, success: @escaping PJSuccessForStruct<T>, fatalError: @escaping PJFatalError) {
        self.send(r, success: { (model, response) -> Void in
            if let response = response as? DataResponse<Any>, let data = response.data, let jsonString = String(data:data, encoding: String.Encoding.utf8) {
                let object = T.Response.parseStruct(jsonString: jsonString)
                success(object, response)
            } else {
                success(T.Response.parseStruct(jsonString: ""), response)
            }
        }) { (error) -> Void in
            fatalError(error)
        }
    }
    
    ///用于网络请求的数据要转成class类型的模型
    func send<T: PJRequest>(_ r: T, success: @escaping PJSuccess, fatalError: @escaping PJFatalError) {
        let url = r.host.appending(r.path)
        let request: DataRequest = Alamofire.request(url, method: r.httpMethod, parameters: r.parameter, encoding: URLEncoding.default, headers: r.headers)
        
        switch r.responseDataType {
        case .json:
            request.responseJSON(completionHandler: { (response : DataResponse<Any>) in
                self.responseHandle(r, response: response, success: success, fatalError: fatalError)
            })
            break
        case .string:
            request.responseString(completionHandler: { (response : DataResponse<String>)  in
                self.responseHandle(r, response: response, success: success, fatalError: fatalError)
            })
            break
        case .data:
            request.responseData(completionHandler: { (response : DataResponse<Data>) in
                self.responseHandle(r, response: response, success: success, fatalError: fatalError)
            })
            break
        }
    }
    
    /*****解析服务器返回的数据*****/
    func responseHandle<T: PJRequest, P>(_ r: T, response : DataResponse<P>, success: @escaping PJSuccess, fatalError: @escaping PJFatalError) {
        if response.result.isSuccess {
            
            if let data = response.data, let jsonString = String(data:data, encoding: String.Encoding.utf8) {
                PJPrintLog("请求成功结果JSON: \(jsonString)")
                let className:String = NSStringFromClass(r.responseClass)
                if let classType = NSClassFromString(className) as? PJBaseModel.Type {
                    let model = classType.init()
                    let object = model.parse(jsonString: jsonString)
                    success(object, response)
                } else {
                    success(response.result.value, response)
                }
            } else {
                PJPrintLog("请求成功结果\(String(describing: response.result.value))")
                success(response.result.value, response)
            }
        }else{
            PJPrintLog("请求失败结果error = \(String(describing: response.result.error))")
            fatalError(response.result.error)
        }
    }
}

PJHttpRequestClient是网络具体实现struct,其中

func send<T: PJRequest>(_ r: T, success: @escaping PJSuccess, fatalError: @escaping PJFatalError)

是实现PJClient的协议的方法,网络调用时类似:

PJHttpRequestClient().send

send函数里面的具体网络实现可以修改,用原生也好,第三方也好,只要能达到网络请求的目的,随意替换,而不用去大改代码,这就是面向协议的好处。

把网络请求相关的配置也抽象出来也是比较灵活的

///网络请求配置协议
protocol PJRequest {
    var path: String { get }
    var parameter: [String: Any] { get }
    var headers: HTTPHeaders { get }
    var httpMethod: HTTPMethod { get }
    var host: String { get }
    var responseDataType: PJResponseDataType { get }
    associatedtype Response: PJDecodable
    ///要转换的目标数据模型
    var responseClass: AnyClass { set get }
}

///默认网络请求配置,用于网络请求返回数据转成class的模型
struct PJBaseRequest<T: PJBaseModel>: PJRequest {
    var host: String = PJConst.PJBaseUrl
    var responseDataType: PJResponseDataType = .json
    var headers: HTTPHeaders = [:]
    var httpMethod: HTTPMethod = .get
    var path: String = ""
    var parameter: [String: Any] = [:]
    /// 如果需要改变类型,可以用子类重写改类型
    typealias Response = T
    var responseClass: AnyClass = T.classForCoder()
    
    ///responseClass:用于指定请求结果要转换的目标数据模型
    init(path: String, responseClass: AnyClass) {
        self.path = path
        self.responseClass = responseClass
    }
    
    init(path: String) {
        self.path = path
    }
}

///默认网络请求配置,用于网络请求返回数据转成struct的模型
struct PJBaseStrcutRequest<T: PJDecodable>: PJRequest {
    var host: String = PJConst.PJBaseUrl
    var responseDataType: PJResponseDataType = .json
    var headers: HTTPHeaders = [:]
    var httpMethod: HTTPMethod = .get
    var path: String = ""
    var parameter: [String: Any] = [:]
    /// 如果需要改变类型,可以用子类重写改类型
    typealias Response = T
    var responseClass: AnyClass = PJBaseModel.classForCoder()
    init(path: String) {
        self.path = path
    }
}

这里针对不同的返回处理结果(model用class或struct分别实现了协议,后面数据解析会用到),网络的请求部分大概是这样。

数据解析

显然数据解析也要达到解耦的目的,不管具体用第三方库还是自己一行一行写去解析数据,都是为了达到解析的目的,这样也采用协议,具体解析怎么实现可以随时替换,而不影响现有的解析好的。

///模型解析协议
protocol PJDecodable {
    /// PJDecodable 用于解析class类型的模型,由于class不能继承static 静态方法,故使用普通成员方法
    func parse(jsonString: String) -> Self?
    /// PJDecodable 用于解析struct类型的模型
    static func parseStruct(jsonString: String) -> Self?
}

前面的protocol PJRequest有定义associatedtype Response: PJDecodable即是协议的泛型,表示网络请求返回后要解析转换后的目的模型。实现该协议时需要指定具体的目的类型struct PJBaseRequest<T: PJBaseModel>: PJRequest,这里我们希望代码可以复用故又加了一层泛型,这样/// 如果需要改变类型,可以用子类重写改类型typealias Response = T`,T即是目的解析类型,这样调用网络配置类时大概是这样:

PJBaseRequest<Model>(path: requestUrl)

每个model类只要去实现协议,并且实现具体的数据解析操作

func parse(jsonString: String) -> Self? {
        let classType = type(of: self)
        if let baseModel = classType.deserialize(from: jsonString) {
            return baseModel
        }
        return nil
    }
    
    static func parseStruct(jsonString: String) -> Self? {
        let type = self
        if let baseModel = type.deserialize(from: jsonString) {
            return baseModel
        }
        return nil
    }

这里数据解析使用HandyJSON,当然你大可以换其他的,因为很容易换。

这样一个完整的网络的请求,返回数据解析是这样:

///请求的数据转成class(ExpressModel)
var baseRequest = PJBaseRequest<ExpressModel>(path: self.requestUrl)
        baseRequest.headers = self.headers
        baseRequest.httpMethod = .get
        baseRequest.parameter = self.params
        PJHttpRequestClient().send(baseRequest, success: { (model, response) -> Void in
            if let model = model as? ExpressModel {               print("\(model)")
            }
        }) { (error) -> Void in
            return
        }
        
        ///请求的数据转成struct(ExpressModel2)
        var r = PJBaseStrcutRequest<ExpressModel2>(path: "query")
        r.parameter = self.getParams()
        PJHttpRequestClient().sendRequestForStruct(r, success: { (structModel, response) in
            if let model = structModel {
        print("\(model)")
            }
        }) { (error) in

        }

其中var baseRequest = PJBaseRequest<ExpressModel>是网络请求配置,ExpressModel(可以替换成任意实现PJDecodable解析协议的类)是要解析转换的目的类型,这样网络请求完拿到的数据即是解析转换好的数据。var r = PJBaseStrcutRequest<ExpressModel2>(path: "query")是正对解析目的类型是struct的,ExpressModel2及时和目的struct,可以替换成任意实现PJDecodable解析协议的struct.网络请求与数据解析到此结束。

tableView网络请求的封装

发起网络请求,请求到数据,更新dataSource,reload,cell创建,设置好model,这大概是tableView显示的一贯流程,哪个地方要用到,就把代码复制一份过去。故我这边把这些可以复用的代码都封装到一个父类,需要用到tableView时,只要做一些必要配置一个网络请求你,数据解析,设置,显示的tableView便呈现在我们面前。

具体的用法大概这样:

前期必要设置

dataSource,实现PJBaseTableViewDataSourceAndDelegate协议,PJBaseTableViewDataSourceAndDelegate协议是对tableView的dataSource的抽出提取,以减小controller大小

class PJTableViewDemoDataSource: PJBaseTableViewDataSourceAndDelegate{
    // MARK: /***********必须重写以告诉表格什么数据模型对应什么cell*************/
    override func tableView(tableView: UITableView, cellClassForObject object: AnyObject?) -> AnyClass {
        if let _ = object?.isKind(of: ExpressItemModel.classForCoder()){
            return ExpressTableViewCell.classForCoder()
        }
        return super.tableView(tableView: tableView, cellClassForObject: object)
    }
}

只要实现这么一个方法,在控制器中:

lazy var pjTableViewDemoDataSource : PJTableViewDemoDataSource = {
        let tempDataSource = PJTableViewDemoDataSource(dataSourceWithItems: nil)
        // TODO: /*******cell点击事件*******/
        tempDataSource.cellClickClosure = {
            (tableView:UITableView,indexPath : IndexPath,cell : UITableViewCell,object : Any?) in
            PJSVProgressHUD.showSuccess(withStatus: "点击了cell")
        }
        
        // TODO: /************cell的子控件的点击事件************/
        tempDataSource.subVieClickClosure = {
            (sender:AnyObject?, object:AnyObject?) in
            
        }
        return tempDataSource
    }()
    
    /**
     *  网络请求配置,子类可以重写,如果有需要
     */
//    override func getBaseRequest() -> PJBaseRequest<PJBaseModelViewController.ModelType> {
//        var baseRequest = PJBaseRequest<PJBaseModelViewController.ModelType>(path: self.requestUrl, responseClass: self.getModelClassType())
//        baseRequest.headers = self.headers
//        baseRequest.httpMethod = .get
//        baseRequest.parameter = self.params
//        return baseRequest
//    }
    
    /**
     *   第二步:子类重写,网络请求完成
     */
    override func requestDidFinishLoad(success: Any?, failure: Any?) {
        if let expressModel = success as? ExpressModel {
            self.updateView(expressModel: expressModel)
        }
    }
    
    /**
     *   子类重写,网络请求失败
     */
    override func requestDidFailLoadWithError(failure: Any?) {
        
    }
    
    /**
     *   子类重写,以设置tableView数据源
     */
    override func createDataSource(){
        self.dataSourceAndDelegate = self.pjTableViewDemoDataSource
    }
    
    // MARK: 网络请求地址
    override func getRequestUrl() -> String{
        return "query"
    }
    
    // MARK: 网络请求参数
    override func getParams() -> [String:Any] {
        return ["type":"shentong","postid":"3342625464825"]
    }
    
    /// 获取返回的数据的模型类型
    ///
    /// - Returns: 获取返回的数据的模型类型
    override func getModelClassType() -> AnyClass {
        return ExpressModel.classForCoder()
    }

一个带有分页,数据为空,数据显示,网络请求,数据解析,显示的tableView变完成了。

self.doRequest()

发起网络请求,一个完整的tableview网络请求变搞定。当然可以定制,修改这边不一一列举。今天就到这里,大概是这样,代码和思路大量借鉴🐱神(福建文档写的最烂的男人😜)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容