Swift面向協(xié)議編程-Alamofire網(wǎng)絡(luò)請(qǐng)求

MQTT 協(xié)議核心角色

MQTT 協(xié)議主要有三大核心角色:發(fā)布者(Publisher)、Broker代理服務(wù)器(轉(zhuǎn)發(fā)者)、訂閱者(Subscriber)棕所。其中消息的發(fā)布者和訂閱者都是客戶(hù)端角色焕参,消息代理是服務(wù)器,消息發(fā)布者可以同時(shí)是訂閱者响蕴。

1、 MQTT客戶(hù)端

MQTT客戶(hù)端身兼二職:既可以是發(fā)布者角色,又可以是訂閱者角色铣墨。一個(gè)使用MQTT協(xié)議的應(yīng)用程序或者設(shè)備就是一個(gè)MQTT 客戶(hù)端,工作時(shí)它需要主動(dòng)去連接到代理服務(wù)器办绝,所以MQTT客戶(hù)端的功能有:

主動(dòng)與Broker 建立連接伊约,主動(dòng)斷開(kāi)Broker的連接。

作為發(fā)布者角色孕蝉,發(fā)布給其他客戶(hù)端訂閱的Topic

作為訂閱者屡律,主動(dòng)訂閱其它客戶(hù)端發(fā)布的Topic

退訂之前訂閱的Topic

清空服務(wù)端之前保留的Message

2、 MQTT服務(wù)器

MQTT通信必須依賴(lài)一個(gè)MQTT Broker降淮,Broker(服務(wù)器)可以看出是MQTT網(wǎng)絡(luò)的Hub超埋,負(fù)責(zé)處理客戶(hù)端的訂閱邏輯并轉(zhuǎn)發(fā)給其他訂閱的客戶(hù)端,如下圖所示佳鳖。

MQTT服務(wù)器又稱(chēng)為"消息代理"服務(wù)器(Broker)霍殴,可以是一個(gè)應(yīng)用程序或一臺(tái)設(shè)備,它是位于消息發(fā)布者和訂閱者之間系吩,具有以下功能:

接受來(lái)自客戶(hù)端的網(wǎng)絡(luò)連接并建立通信鏈路
接收發(fā)布者的Topic并轉(zhuǎn)發(fā)給訂閱者
處理來(lái)自客戶(hù)端的訂閱和退訂請(qǐng)求
向訂閱的客戶(hù)轉(zhuǎn)發(fā)相應(yīng)地Topic

-----------------------------------------------------------------------------------------分割線(xiàn)-----------------------------------------------------------------------------------------------

面向協(xié)議

先看代碼:

protocol Run {
    var name: String { get }
    func run()
}

代碼中定義名為Run協(xié)議,包含一個(gè)name屬性,以及一個(gè)run方法的定義 所謂協(xié)議来庭,就是屬性的定義和方法的聲明(注意這里只需要聲明即可),而如果某個(gè)具體類(lèi)型想要遵守一個(gè)協(xié)議穿挨,那它需要實(shí)現(xiàn)這個(gè)協(xié)議所定義的所有內(nèi)容

POP就是通過(guò)協(xié)議擴(kuò)展月弛,協(xié)議繼承和協(xié)議組合的方式來(lái)設(shè)計(jì)需要編寫(xiě)的代碼。

首先在Swift中絮蒿,值類(lèi)型優(yōu)先于類(lèi)尊搬。然而,面向?qū)ο蟮母拍畈荒芎芎玫嘏c結(jié)構(gòu)體和枚舉一起工作: 因?yàn)榻Y(jié)構(gòu)體和枚舉不能夠被繼承土涝。

再者,實(shí)際開(kāi)發(fā)工程中,我們經(jīng)常會(huì)遇到如下場(chǎng)景: 假設(shè)我們有一個(gè)ViewController佛寿,它繼承自UIViewController,我們向其新添加一個(gè)方法 customMethod:

class ViewController: UIViewController {
    //新添加
    func customMethod() {
    }
}

這個(gè)時(shí)候我們有另外一個(gè)繼承自UITableViewController的OtherViewController,同樣也需要向其添加方法customMethod

class OtherViewController: UITableViewController {
    //新添加
    func customMethod() {
    }
}

這里就存在一個(gè)問(wèn)題:很難在不同繼承關(guān)系的類(lèi)里共用代碼但壮。
我們的關(guān)注點(diǎn)customMethod位于兩條繼承鏈 UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController 的橫切面上冀泻。面向?qū)ο笫且环N不錯(cuò)的抽象方式,但是肯定不是最好的方式蜡饵。它無(wú)法描述兩個(gè)不同事物具有某個(gè)相同特性這一點(diǎn)弹渔。在這里,特性的組合要比繼承更貼切事物的本質(zhì)溯祸。

總的來(lái)說(shuō),面向協(xié)議編程(POP) 帶來(lái)的好處如下:

結(jié)構(gòu)體肢专、枚舉等值類(lèi)型也可以使用
以繼承多個(gè)協(xié)議舞肆,彌補(bǔ) swift 中類(lèi)單繼承的不足
增強(qiáng)代碼的可擴(kuò)展性,減少代碼的冗余
讓項(xiàng)目更加組件化博杖,代碼可讀性更高
讓無(wú)需的功能代碼組成一個(gè)功能塊椿胯,更便于單元測(cè)試。

使用pop解決上面的問(wèn)題

protocol ex {
    func customMethod();
}

在實(shí)際類(lèi)型遵守這個(gè)協(xié)議:

extension ViewController :ex {
    func customMethod() {
        //
    }
}
extension OtherViewController : ex {
    func customMethod() {
        //
    }
}

上述方式就是復(fù)制粘貼

而協(xié)議的擴(kuò)展是可以在 extension ex 中為 customMethod 添加一個(gè)實(shí)現(xiàn):

extension ex {
    func customMethod() {
        //
    }
}

協(xié)議的特性及使用

協(xié)議擴(kuò)展:

1.提供協(xié)議方法的默認(rèn)實(shí)現(xiàn)和協(xié)議屬性的默認(rèn)值剃根,從而使它們成為可選哩盲;符合協(xié)議的類(lèi)型可以提供自己的實(shí)現(xiàn),也可以使用默認(rèn)的實(shí)現(xiàn)狈醉。
2.添加協(xié)議中未聲明的附加方法實(shí)現(xiàn)廉油,并且實(shí)現(xiàn)協(xié)議的任何類(lèi)型都可以使用到這些附加方法。這樣就可以給遵循協(xié)議的類(lèi)型添加特定的方法

protocol Entity {
    var name: String {get set}
    static func uid() -> String
}

extension Entity {
    static func uid() -> String {
        return UUID().uuidString
    }
}

struct Order: Entity {
    var name: String
    let uid: String = Order.uid()
}
let order = Order(name: "My Order")
print(order.uid)

協(xié)議繼承

協(xié)議可以從其他協(xié)議繼承苗傅,然后在它繼承的需求之上添加功能抒线,因此可以提供更細(xì)粒度和更靈活的設(shè)計(jì)。

protocol Persistable: Entity {
    func write(instance: Entity, to filePath: String)
    init?(by uid: String)
}

struct InMemoryEntity: Entity {
    var name: String
}

struct PersistableEntity: Persistable {
    var name: String
    func write(instance: Entity, to filePath: String) { // ...
    }  
    init?(by uid: String) {
        // try to load from the filesystem based on id
    }
}

協(xié)議的組合

類(lèi)金吗、結(jié)構(gòu)體和枚舉可以符合多個(gè)協(xié)議十兢,它們可以采用多個(gè)協(xié)議的默認(rèn)實(shí)現(xiàn)。這在概念上類(lèi)似于多繼承摇庙。這種組合的方式不僅比將所有需要的功能壓縮到一個(gè)基類(lèi)中更靈活旱物,而且也適用于值類(lèi)型。

struct MyEntity: Entity, Equatable, CustomStringConvertible {
    var name: String
    // Equatable
    public static func ==(lhs: MyEntity, rhs: MyEntity) -> Bool {
        return lhs.name == rhs.name
    }
    // CustomStringConvertible
    public var description: String {
        return "MyEntity: \(name)"
    }
}
let entity1 = MyEntity(name: "42")
print(entity1)
let entity2 = MyEntity(name: "42")
assert(entity1 == entity2, "Entities shall be equal")

網(wǎng)絡(luò)請(qǐng)求

iOS開(kāi)發(fā)中卫袒,一般App端網(wǎng)絡(luò)請(qǐng)求都是通過(guò)一個(gè)API請(qǐng)求到JSON數(shù)據(jù)宵呛,自己轉(zhuǎn)化為model然后刷新UI。

這里用一個(gè)騰訊地圖的API來(lái)獲取數(shù)據(jù)(主要他們數(shù)據(jù)規(guī)范夕凝,還一直可以使用)API 和參數(shù)拼接到一起是

https://apis.map.qq.com/ws/place/v1/suggestion/?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77

這個(gè)API返回的數(shù)據(jù)是


{
    "status":0,
    "message":"query ok",
    "count":100,
    "data":[{
        "id":"4062879599476274838",
        "title":"海底撈火鍋(西單店)",
        "address":"北京市西城區(qū)西單北大街109號(hào)西單婚慶大樓7層",
        "category":"美食:火鍋",
        "type":0,
        "location":{
            "lat":39.9139,
            "lng":116.3732
        },
        "adcode":110102,
        "province":"北京市",
        "city":"北京市",
        "district":"西城區(qū)"
    },
      ...
    ],
    "request_id":"1053741451029820730"
}

根據(jù)API返回的數(shù)據(jù)類(lèi)型創(chuàng)建數(shù)據(jù)模型RestanurantTotal


import Foundation

struct RestanurantTotal {
    let status:Int
    let message:String
    var data:Array<Any>
    let count:Int
    let request_id:String
    
    init?(data: Any) {

        guard let total = data as? Dictionary<String, Any> else {
        return nil
    }
        
    guard let status = total["status"] as? Int else {
             return nil
         }
        self.status = status
        
        guard let message = total["message"] as? String else {
                 return nil
             }
            self.message = message
        
        guard let count = total["count"] as? Int else {
                 return nil
             }
            self.count = count
        
        guard let request_id = total["request_id"] as? String else {
                 return nil
             }
        self.request_id = request_id
        
        
        guard let restaurant = total["data"] as? Array<Dictionary<String, Any>> else {
                      return nil
        }
        
        let array:NSMutableArray = NSMutableArray.init()
        
        for index in 0..<restaurant.count  {
            let dic = restaurant[index]
            let res : Restaurant = Restaurant.init(info: dic)!
            array.add(res)
        }
        self.data = array as! Array<Any>
}
    
}
    
    
struct Restaurant {

    let id : String
    let title: String
    let address: String
    let category: String
    let type: Int
    let location: LLocation
    let adcode: Int
    let province: String
    let district: String


    init?(info: Any) {

        guard let restaurant = info as? Dictionary<String, Any> else {
            return nil
        }
        guard let id = restaurant["id"] as? String else {
            return nil
        }
        guard let title = restaurant["title"] as? String else {
             return nil
         }
        guard let address = restaurant["address"] as? String else {
            return nil
        }
        guard let category = restaurant["category"] as? String else {
            return nil
        }
        guard let type = restaurant["type"] as? Int else {
            return nil
        }
        guard let adcode = restaurant["adcode"] as? Int else {
            return nil
        }
        guard let province = restaurant["province"] as? String else {
                  return nil
        }
        guard let district = restaurant["district"] as? String else {
                  return nil
        }
        guard let  location = restaurant["location"] as? Dictionary<String, Any> else {
                  return nil
        }

        self.id = id
        self.title = title
        self.address = address
        self.category = category
        self.type = type
        self.adcode = adcode
        self.province = province
        self.district = district
        self.location = LLocation.init(data: (location ))!
        
    }
}

struct  LLocation {
    let lat: Double
    let lng: Double
    
    init?(data: Any) {

        guard let location = data as? Dictionary<String, Any> else {
            return nil
        }
        guard let lat = location["lat"] as? Double else {
            return nil
        }

        guard let lng = location["lng"] as? Double else {
            return nil
        }
        self.lat = lat
        self.lng = lng
}
    
}

init中傳入一個(gè)NSDictionary,創(chuàng)建一個(gè)RestanurantTotal實(shí)例宝穗。
如何使用POP的方式從URL請(qǐng)求到數(shù)據(jù)并生成對(duì)應(yīng)的RestanurantTotal,是這里的重點(diǎn)。
我們知道Request是網(wǎng)絡(luò)請(qǐng)求的入口,所以可以直接創(chuàng)建一個(gè)網(wǎng)絡(luò)請(qǐng)求協(xié)議,網(wǎng)絡(luò)請(qǐng)求需要知道路徑,方法,參數(shù)等等码秉。

enum HPHTTPMethod {
    case GET
    case POST
}

protocol HPRequest {
    var host : String {get}
    var path : String {get}
    var method : HPHTTPMethod {get}
    var parameter: [String: Any] { get }
}

請(qǐng)求地址由host和path拼接而成
method支持GET和POST,本例使用GET
parameter是請(qǐng)求的參數(shù)

struct TencentRequest: HPRequest {
    typealias Response = RestanurantTotal
    var host: String  {
        return "https://apis.map.qq.com/ws/place/v1/"
//        ?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77
    }
    var path: String {
        return "suggestion/"
    }
    let method: HPHTTPMethod = .GET
    var parameter: [String : Any]
}

設(shè)置host路徑和path路徑
指定method為GET

這個(gè)時(shí)候,我們已經(jīng)有了發(fā)請(qǐng)求的條件(路由,方法,參數(shù))逮矛。下一步就需要發(fā)送請(qǐng)求了。我們可以為HPRequest擴(kuò)展發(fā)送請(qǐng)求的能力,這樣可以讓每一個(gè)請(qǐng)求都是用一樣的方法發(fā)送的转砖。

extension HPRequest {
    
    func sendRequest(hander:@escaping(RestanurantTotal)->Void) {
        //...
    }}

為HPRequest擴(kuò)展sendRequest
逃逸閉包hander可將請(qǐng)求結(jié)果返回到外界

這里返回的是RestanurantTotal模型,這樣的話(huà)sendRequest方法就只能支持這個(gè)餐館請(qǐng)求须鼎。我們可以使用關(guān)聯(lián)類(lèi)型解決這個(gè)問(wèn)題,使請(qǐng)求一般化.

protocol HPRequest {
   //.....
    associatedtype Response
}

struct RencentRequest: JNRequest {
    typealias Response = RestanurantTotal
    //....
}
extension HPRequest {
    func sendRequest(hander:@escaping(Response)->Void) {
    //...
    }
}

HPRequest協(xié)議中添加associatedtype Response
TencentRequest添加typealias Response = RestanurantTotal,執(zhí)行返回類(lèi)型為RestanurantTotal
HPRequest的擴(kuò)展方法sendRequest的逃逸閉包將返回類(lèi)型改為Response

sendRequest發(fā)送方法中,使用Alamofire發(fā)送網(wǎng)絡(luò)請(qǐng)求

        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod.get, parameters: self.parameter).responseJSON { (response) in
                    switch response.result {
                    print(result)
                }
          }

拼接url
調(diào)用Alamofire.request請(qǐng)求數(shù)據(jù)
使用JSON序列化器
獲取到網(wǎng)絡(luò)返回的JSON數(shù)據(jù)

現(xiàn)在還差最后一步,將返回的JSON數(shù)據(jù)轉(zhuǎn)化為RestanurantTotal模型數(shù)據(jù) 我們?yōu)镠PRequest協(xié)議添加方法

    func parse(data: NSDictionary) -> Response?

TencentRequest的擴(kuò)展中實(shí)現(xiàn)parse

extension TencentRequest {
    
    func parse(data: NSDictionary) -> RestanurantTotal? {
        return RestanurantTotal(data:data)
    }
}

sendRequest中調(diào)用序列化解析

   func sendRequest(hander:@escaping(Response?)->Void) {
        //...
        let url = self.host + self.path
        Alamofire.request(url, method: HTTPMethod.get, parameters: self.parameter).responseJSON { (response) in
                    switch response.result {
                    case .success(let data):
                           let dic = data as? NSDictionary
                           if let res = self.parse(data: dic!) {
                            hander(res)
                         }else {
                            hander(nil)
                         }
                        case .failure:
                            hander(nil)
                    }
                }
          }

調(diào)用

let request = TencentRequest(parameter: ["region":"北京","keyword":"美食","key":"YSJBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
                    request.sendRequest { (Total) in
                        let total:RestanurantTotal = (Total as RestanurantTotal?)!
                        let resta:Restaurant = total.data.first! as! Restaurant
                        print("第一個(gè)地區(qū)的id是\(resta.id)")
                }

使用起來(lái)非常便捷,也能實(shí)現(xiàn)需求。但是這樣的實(shí)現(xiàn)非常差勁府蔗〗兀回頭看看HPRequest的定義和擴(kuò)展:

protocol HPRequest {
    var host : String {get}
    var path : String {get}
    var method : HPHTTPMethod {get}
    var parameter: [String: Any] { get }
    
    associatedtype Response
    func parse(data: NSDictionary) -> Response?
}

extension HPRequest {
    func sendRequest(hander:@escaping(Response?)->Void) {
     ...      
}

上面的實(shí)現(xiàn)主要問(wèn)題在于Request管理的東西太多.Request該做的事情應(yīng)該是定義請(qǐng)求入口,保存請(qǐng)求的信息和響應(yīng)類(lèi)型。而這里的Request保存host,還進(jìn)行數(shù)據(jù)的解析成,這樣做就無(wú)法在不修改請(qǐng)求的情況下更改解析的方式,增加耦合度,不利于測(cè)試姓赤。發(fā)送請(qǐng)求也是它的一部分,這樣請(qǐng)求的具體實(shí)現(xiàn)就和請(qǐng)求產(chǎn)生耦合,這也是不合理的...

重構(gòu)優(yōu)化

鑒于上述問(wèn)題赡译,開(kāi)始優(yōu)化代碼,先將send從Request中剝離出來(lái)不铆。因此需要一個(gè)單獨(dú)的類(lèi)型負(fù)責(zé)發(fā)送請(qǐng)求蝌焚。根據(jù)POP協(xié)議裹唆,定義以下協(xié)議

protocol LHDataClient {
    var host: String { get }
    func send<T :LHRequest>(_ r : T, handler: @escaping(T.Response?)->Void)
}

host不應(yīng)該在Request中設(shè)置,我們將其移動(dòng)到LHDataClient只洒。清除請(qǐng)求中的host以及send品腹。并定義LHAlamofireClient實(shí)現(xiàn)LHDataClient協(xié)議:

struct LHAlamofireClient {
   
    static let `default` = LHAlamofireClient()

   var host: String  {
       return "https://apis.map.qq.com/ws/place/v1/"
   }
   func send<T :LHRequest>(_ r : T, handler: @escaping(T.Response?)->Void) {

           let url = self.host + r.path
    Alamofire.request(url, method:HTTPMethod.get, parameters: r.parameter).responseJSON { (response) in
               switch response.result {
               case .success(let data):
                if let dic = data as? NSDictionary {
                    if let res = T.Response.parse(data: dic) {
                        handler(res)
                    }else {
                        handler(nil)
                    }
                }else {
                    handler(nil)
                }

               case .failure:
                   handler(nil)
               }
           }
   }
}


目前已經(jīng)將發(fā)送請(qǐng)求和請(qǐng)求本身分離開(kāi),我們定義了LHDataClient協(xié)議红碑,這里實(shí)現(xiàn)了LHAlamofireClient,使用Alamofire發(fā)送請(qǐng)求泡垃。對(duì)象的解析不應(yīng)該由Request來(lái)完成析珊,交給應(yīng)該Response我們新增一個(gè)協(xié)議,滿(mǎn)足這個(gè)協(xié)議的需要實(shí)現(xiàn)蔑穴。parse方法:

protocol Decodable {
   static func parse(data : NSDictionary)->Self?
}

為了保證所有的Response都能解析數(shù)據(jù)忠寻,我們需要對(duì)Response實(shí)現(xiàn)Decodable協(xié)議,并刪除Request的解析方法

protocol JNRequest {
   
    var path : String {get}
    var httpMethod : JNHTTPMethod {get}
    var parameter: [String: Any] { get }
    
    associatedtype Response : Decodable
}

為Model類(lèi)擴(kuò)展協(xié)議方法

extension RestanurantTotal : Decodable {
   static func parse(data: NSDictionary) -> RestanurantTotal? {
       return RestanurantTotal(data: data)!
   }
}

send中直接提交T.response:

if let dic = data as? NSDictionary {
                        if let res = T.Response.parse(data: dic) {
                            handler(res)
                        }else {
                            handler(nil)
                        }
                    }else {
                        handler(nil)
                    }

創(chuàng)建單例

    static let `default` = LHAlamofireClient()

外部調(diào)用

 let request = LHlhRequest(parameter: ["region":"北京","keyword":"美食","key":"JBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
                LHAlamofireClient.default.send(request) {(Total) in
                           print(Total!)
                    
                    let total:RestanurantTotal = (Total as RestanurantTotal?)!
                                    let resta:Restaurant = total.data.first! as! Restaurant
                                    print("第一個(gè)地區(qū)的id是\(resta.id)")
                }
         
                        
                
            })

如果需要?jiǎng)?chuàng)建其他的請(qǐng)求存和,可以使用和LHlhRequest相似的方式奕剃,為網(wǎng)絡(luò)層添加其他的API請(qǐng)求,只需要定義請(qǐng)求所必要的內(nèi)容捐腿,而不用擔(dān)心會(huì)觸及網(wǎng)絡(luò)方面的具體實(shí)現(xiàn)纵朋。

易于測(cè)試

準(zhǔn)備一個(gè)response.json的文件,內(nèi)容是

{
    "status":0,
    "message":"query ok",
    "count":100,
    "data":[{
        "id":"4412846406955100612",
        "title":"鮮魚(yú)口老字號(hào)美食街",
        "address":"北京市東城區(qū)前門(mén)東路附近",
        "category":"美食:中餐廳:其它中餐廳",
        "type":0,
        "location":{
            "lat":39.89605,
            "lng":116.39991
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"東城區(qū)"
    },
    {
        "id":"12500720047859529446",
        "title":"初色海鮮自助火鍋",
        "address":"北京市豐臺(tái)區(qū)萬(wàn)豐路302號(hào)",
        "category":"美食:海鮮",
        "type":0,
        "location":{
            "lat":39.870803,
            "lng":116.293982
        },
        "adcode":110106,
        "province":"北京市",
        "city":"北京市",
        "district":"豐臺(tái)區(qū)"
    },
    {
        "id":"8301295427127788926",
        "title":"王府井小吃街",
        "address":"北京市東城區(qū)王府井大街與大紗帽胡同交叉口西北角",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.910895,
            "lng":116.410904
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"東城區(qū)"
    },
    {
        "id":"11385228330756553756",
        "title":"新干線(xiàn)美食一條街",
        "address":"北京市朝陽(yáng)區(qū)霄云路35號(hào)三元橋",
        "category":"美食:中餐廳:其它中餐廳",
        "type":0,
        "location":{
            "lat":39.957371,
            "lng":116.460884
        },
        "adcode":110105,
        "province":"北京市",
        "city":"北京市",
        "district":"朝陽(yáng)區(qū)"
    },
    {
        "id":"2151925127924188045",
        "title":"鼎好美食廣場(chǎng)",
        "address":"北京市海淀區(qū)中關(guān)村日月光·鼎好大廈B座",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.983855,
            "lng":116.314364
        },
        "adcode":110108,
        "province":"北京市",
        "city":"北京市",
        "district":"海淀區(qū)"
    },
    {
        "id":"6120575816560518252",
        "title":"全聚德烤鴨店(天安門(mén)店)",
        "address":"北京市東城區(qū)東交民巷44號(hào)",
        "category":"美食:其它美食",
        "type":0,
        "location":{
            "lat":39.90147,
            "lng":116.40007
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"東城區(qū)"
    },
    {
        "id":"16827131455368440528",
        "title":"上可味美食廣場(chǎng)",
        "address":"北京市豐臺(tái)區(qū)南四環(huán)西路188號(hào)",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.824105,
            "lng":116.283744
        },
        "adcode":110106,
        "province":"北京市",
        "city":"北京市",
        "district":"豐臺(tái)區(qū)"
    },
    {
        "id":"12511480423602913915",
        "title":"可味美食城(三里屯店)",
        "address":"北京市朝陽(yáng)區(qū)工人體育場(chǎng)北路8號(hào)院三里屯SOHO6號(hào)商場(chǎng)B1層",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.93167,
            "lng":116.45363
        },
        "adcode":110105,
        "province":"北京市",
        "city":"北京市",
        "district":"朝陽(yáng)區(qū)"
    },
    {
        "id":"8119722848339667861",
        "title":"美食廣場(chǎng)食字街區(qū)(北京市百貨大樓)",
        "address":"北京市東城區(qū)王府井大街255號(hào)北京市百貨大樓F7",
        "category":"美食:小吃快餐",
        "type":0,
        "location":{
            "lat":39.914084001,
            "lng":116.410407448
        },
        "adcode":110101,
        "province":"北京市",
        "city":"北京市",
        "district":"東城區(qū)"
    },
    {
        "id":"7632255329310100195",
        "title":"西單明珠市場(chǎng)美食廣場(chǎng)",
        "address":"北京市西城區(qū)橫二條59號(hào)西單明珠市場(chǎng)8層",
        "category":"美食:中餐廳:其它中餐廳",
        "type":0,
        "location":{
            "lat":39.9103,
            "lng":116.37618
        },
        "adcode":110102,
        "province":"北京市",
        "city":"北京市",
        "district":"西城區(qū)"
    }],
    "request_id":"658616875259013526"
}

步驟,創(chuàng)建一個(gè)類(lèi)型LHLocalClient,實(shí)現(xiàn)LHDataClient協(xié)議:

struct LHLocalClient {
    
    var host: String  {
        return ""
    }
    func send<T :LHRequest>(_ r : T, handler: @escaping(T.Response?)->Void) {
        
        switch r.path {
        case "suggestion/":
            let fileURL = Bundle.main.path(forResource: "response", ofType: "json")
           
//            if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
//                let jsonData:Data = data.data(using: .utf8)!
//                if let dic = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) {
//                    if let res = T.Response.parse(data: dic as! NSDictionary) {
//                         handler(res)
//                    }else {
//                         handler(nil)
//                    }
//                }else {
//                    handler(nil)
//                }
//            }
            
            if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
                            let jsonData:Data = data.data(using: .utf8)!
                            if let res = T.Response.parse(data: jsonData) {
                                    handler(res)
                            }else {
                                    handler(nil)
                            }
                        }else
            {
                
                handler(nil)
            }
        default:
            handler(nil)
        }
    }
}

檢查輸入請(qǐng)求的path屬性茄袖,根據(jù)path不同,從bundle中讀取預(yù)先設(shè)定的文件數(shù)據(jù)操软。
對(duì)返回的結(jié)果做JSON解析,然后調(diào)用Response的parse解析
調(diào)用handler返回?cái)?shù)據(jù)到外界。
如果我們需要增加其他請(qǐng)求的測(cè)試宪祥,可以添加新的case項(xiàng)

protocol Decodable {
   static func parse(data : NSDictionary)->Self?
}

所以在LHLocalClient中我們需要自己解析成JSON,使用起來(lái)不太好用聂薪。使用POP的方式我們可以不限定單獨(dú)的類(lèi)型,而是限定一個(gè)協(xié)議DecodeType:

protocol DecodeType {
    func asDictionary() -> NSDictionary?;
}

可能傳入解析的類(lèi)型比如NSDictionary,Data等添加擴(kuò)展:

extension NSDictionary : DecodeType {
    func asDictionary() -> NSDictionary? {
        return self
    }
}

extension Data : DecodeType {
   func asDictionary() -> NSDictionary? {
      if let dic = try? JSONSerialization.jsonObject(with: self, options: .mutableContainers) {
        return dic as? NSDictionary
      }
    return nil
   }
}


修改協(xié)議Decodable協(xié)議,限定參數(shù)類(lèi)型DecodeType協(xié)議:

protocol Decodable {
   static func parse(data : DecodeType)->Self?
}

修改RestanurantTotal的解析方式:

extension RestanurantTotal : Decodable {
   static func parse(data: DecodeType) -> RestanurantTotal? {
       return RestanurantTotal(data: data.asDictionary()!)
   }
}

LHLocalClient的send方法不需要在解析JSON,直接調(diào)用Parse解析:

if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
                let jsonData:Data = data.data(using: .utf8)!
                if let res = T.Response.parse(data: jsonData) {
                        handler(res)
                }else {
                        handler(nil)
                }
            }else {
                handler(nil)
            }

這樣用起來(lái)就更方便了蝗羊。 回到剛才的話(huà)題,有了LHLocalClient,我們就可以不受網(wǎng)絡(luò)的限制,單獨(dú)測(cè)試LoginRequest藏澳、 parse是否正常,以及之后的各個(gè)流程是否正常。

解耦&可擴(kuò)展

基于POP實(shí)現(xiàn)的代碼高度解耦,為代碼的擴(kuò)展提供相對(duì)寬松的可能性耀找。在上面的例子中,我們可以?xún)H僅實(shí)現(xiàn)發(fā)送請(qǐng)求的方法,在不影響請(qǐng)求定義和使用的情況下更換了請(qǐng)求方式翔悠。這里我們使用手動(dòng)解析賦值模型,我們我們完全可以使用第三方解析庫(kù),HandyJSON涯呻,來(lái)幫助我們迅速構(gòu)建模型類(lèi)型凉驻。

pod里面添加代碼

pod 'HandyJSON'

步驟參考:https://juejin.im/post/5d6a1bad518825391623e64b
代碼拉取地址:https://gitee.com/xgkp/SwiftNetRequest.git

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市复罐,隨后出現(xiàn)的幾起案子涝登,更是在濱河造成了極大的恐慌,老刑警劉巖效诅,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胀滚,死亡現(xiàn)場(chǎng)離奇詭異趟济,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)咽笼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)顷编,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人剑刑,你說(shuō)我怎么就攤上這事媳纬。” “怎么了施掏?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵钮惠,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我七芭,道長(zhǎng)素挽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任狸驳,我火速辦了婚禮预明,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘耙箍。我一直安慰自己撰糠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布究西。 她就那樣靜靜地躺著窗慎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卤材。 梳的紋絲不亂的頭發(fā)上遮斥,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音扇丛,去河邊找鬼术吗。 笑死,一個(gè)胖子當(dāng)著我的面吹牛帆精,可吹牛的內(nèi)容都是我干的较屿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼卓练,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼隘蝎!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起襟企,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嘱么,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后顽悼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體曼振,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡几迄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冰评。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片映胁。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甲雅,靈堂內(nèi)的尸體忽然破棺而出解孙,到底是詐尸還是另有隱情,我是刑警寧澤抛人,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布妆距,位于F島的核電站,受9級(jí)特大地震影響函匕,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚪黑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一盅惜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧忌穿,春花似錦抒寂、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至朴译,卻和暖如春井佑,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背眠寿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工躬翁, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盯拱。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓盒发,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親狡逢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宁舰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容