Swift 項(xiàng)目中涉及到 JSONDecoder,網(wǎng)絡(luò)請求,泛型協(xié)議式編程的一些記錄和想法

前言

最近項(xiàng)目開發(fā)一直在使用 swift,因?yàn)?HTN 項(xiàng)目最近會有另外一位同事加入,所以打算對最近涉及到的一些技術(shù)和自己的一些想法做個記錄,同時也能夠方便同事熟悉代碼。

JSON 數(shù)據(jù)的處理

做項(xiàng)目只要是涉及到服務(wù)器端接口都沒法避免和 JSON 數(shù)據(jù)打交道。對于來自網(wǎng)絡(luò)的 JSON 結(jié)構(gòu)化數(shù)據(jù)的處理,可以使用 JSONDecoder 這個蘋果自己提供的字符串轉(zhuǎn)模型類,這個類是在 Swift 4 的 Fundation 模塊里提供的,可以在Swift 源碼目錄 swift/stdlib/public/SDK/Fundation/JSONEncoder.swift 看到蘋果對這個類實(shí)現(xiàn)。

其它對 JSON 處理的庫還有 SwiftyJSON GitHub - SwiftyJSON/SwiftyJSON: The better way to deal with JSON data in Swift

使用 JSONDecoder

下面蘋果使用 JSONDecoder 的一個例子來看看如何使用 JSONDecoder

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var description: String?
}

let json = """
{
    "name": "Durian",
    "points": 600,
    "description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!

let decoder = JSONDecoder()
let product = try decoder.decode(GroceryProduct.self, from: json)

print(product.name) // Prints "Durian"

這里要注意 GroceryProduct 結(jié)構(gòu)體需要遵循 Codable,因?yàn)?JSONDecoder 的實(shí)例對象的 decode 方法需要遵循 Decodable 協(xié)議的結(jié)構(gòu)體。Codable 是 Encodable 和 Decodable 兩個協(xié)議的組合,寫法如下:

public typealias Codable = Decodable & Encodable

當(dāng)然 JSON 數(shù)據(jù)的結(jié)構(gòu)不會都是這么簡單,如果遇到嵌套情況如下:

let json = """
{
    "name": "Durian",
    "points": 600,
    "ability": {
        "mathematics": "excellent",
        "physics": "bad",
        "chemistry": "fine"
    },
    "description": "A fruit with a distinctive scent."
}
""".data(using: .utf8)!

這時可以通過在 struct 里再套一個 struct 來做,修改過的 struct 如下:

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var ability: Ability
    var description: String?
    
    struct Ability: Codable {
        var mathematics: String
        var physics: String
        var chemistry: String
    }
}

這里可以觀察到 ability 里數(shù)學(xué)物理化學(xué)的評價都是那幾個,無非是優(yōu)良差,所以很適合用枚舉表示,swift 的枚舉對于字符串關(guān)聯(lián)類型枚舉也有很好的支持,只要聲明關(guān)聯(lián)值類型是 String 就行了,改后的代碼如下:

struct GroceryProduct: Codable {
    var name: String
    var points: Int
    var ability: Ability
    var description: String?
    
    struct Ability: Codable {
        var mathematics: Appraise
        var physics: Appraise
        var chemistry: Appraise
    }
    
    enum Appraise: String, Codable {
        case excellent, fine, bad
    }
}

API 返回的結(jié)果會有一個不可控的因素,是什么呢?那就是有的鍵值有時會返回有時不會返回,那么這個 struct 怎么兼容呢?

好在swift 原生就支持了 optional,只需要在屬性后加個問號就行了。比如 points 有時會返回有時不會,那么就可以這么寫:

struct GroceryProduct: Codable {
    var name: String
    var points: Int? //可能會用不到
}

CodingKey 協(xié)議

接口還會有一些其它不可控因素,比如會產(chǎn)生出 snake case 的命名風(fēng)格,要求風(fēng)格統(tǒng)一固然是很好,但是現(xiàn)實(shí)環(huán)境總會有些不可抗拒的因素,比如不同團(tuán)隊(duì),不同公司或者不同風(fēng)格潔癖的 coder 之間。還好 JSONDecoder 已經(jīng)做好了。下面我們看看如何用:

let json = """
{
    "nick_name": "Tom",
    "points": 600,
}
""".data(using: .utf8)!

這里 nick_name 我們希望處理成 swift 的風(fēng)格,那么我們可以使用一個遵循 CodingKey 協(xié)議的枚舉來做映射。

struct GroceryProduct: Codable {
    var nickName: String
    var points: Int
    
    enum CodingKeys : String, CodingKey{
        case nickName = "nick_name"
        case points
    }
}

當(dāng)然這個方法是個通用方法,不光能夠處理 snake case 還能夠起自己喜歡的命名,比如你喜歡簡寫,nick name 寫成 nName,那么也可以用這個方法。Codable 協(xié)議默認(rèn)的實(shí)現(xiàn)實(shí)際上已經(jīng)能夠 cover 掉現(xiàn)實(shí)環(huán)境的大部分問題了,如果有些自定義的東西要處理的話可以通過覆蓋默認(rèn) Codable 的方式來做。關(guān)鍵點(diǎn)就是 encoder 的 container,通過獲取 container 對象進(jìn)行自定義操作。

JSONDecoder 的 keyDecodingStrategy 屬性

JSONDecoder 里還有專門的一個屬性 keyDecodingStrategy,這個值是個布爾值,有個 case 是 convertFromSnakeCase,這樣就會按照這個 strategy 來轉(zhuǎn)換 snake case,這個是核心功能內(nèi)置的,就不需要我們額外寫代碼處理了。上面加上的枚舉 CodingKeys 也可以去掉了,只需要在 JSONDecoder 這個實(shí)例設(shè)置這個屬性就行。

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

keyDecodingStrategy 這個屬性是在 swift 4.1 加上的,所以這個版本之前的還是用 CodingKey 這個協(xié)議來處理吧。

那么蘋果是如何通過這個 keyDecodingStrategy 屬性的設(shè)置來做到的呢?

感謝蘋果使用 Swift 寫了 Swift 的核心功能,以后想要了解更多功能背后原理可以不用啃 C++ 了,一邊學(xué)習(xí)原理還能一邊學(xué)習(xí)蘋果內(nèi)部是如何使用 Swift 的,所謂一舉兩得。

實(shí)現(xiàn)這個功能代碼就在上文提到的 Swift 源碼目錄 swift/stdlib/public/SDK/Fundation/ 下的 JSONEncoder.swift 文件,如果不想把源碼下下來也可以在 GitHub 上在線看,地址:https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift

先看看這個屬性的定義:

/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

這個屬性是一個 keyDecodingStrategy 枚舉,默認(rèn)是 .userDefaultKeys。這個枚舉是這樣定義的:

public enum KeyDecodingStrategy {
    /// Use the keys specified by each type. This is the default strategy.
    case useDefaultKeys
    
    /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
    ///
    /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
    ///
    /// Converting from snake case to camel case:
    /// 1. Capitalizes the word starting after each `_`
    /// 2. Removes all `_`
    /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
    /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
    ///
    /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
    case convertFromSnakeCase
    
    /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
    /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding.
    /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from.
    case custom((_ codingPath: [CodingKey]) -> CodingKey)
    
    fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
        ...
    }
}

case convertFromSnakeCase 就是我們使用的,注釋部分描述了整個過程,首先會把 ‘’ 符號后面的字母轉(zhuǎn)成大寫的,然后移除掉所有的 ‘’ 符號,保留最前面和最后的 ‘_’ 符號。比如 _nick_name 就會轉(zhuǎn)換成 _nickName 而這些都是在枚舉里定義的靜態(tài)方法 _convertFromSnakeCase 里完成的。

fileprivate static func _convertFromSnakeCase(_ stringKey: String) -> String {
    guard !stringKey.isEmpty else { return stringKey }
    
    // Find the first non-underscore character
    guard let firstNonUnderscore = stringKey.index(where: { $0 != "_" }) else {
        // Reached the end without finding an _
        return stringKey
    }
    
    // Find the last non-underscore character
    var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
    while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
        stringKey.formIndex(before: &lastNonUnderscore);
    }
    
    let keyRange = firstNonUnderscore...lastNonUnderscore
    let leadingUnderscoreRange = stringKey.startIndex..<firstNonUnderscore
    let trailingUnderscoreRange = stringKey.index(after: lastNonUnderscore)..<stringKey.endIndex
    
    var components = stringKey[keyRange].split(separator: "_")
    let joinedString : String
    if components.count == 1 {
        // No underscores in key, leave the word as is - maybe already camel cased
        joinedString = String(stringKey[keyRange])
    } else {
        joinedString = ([components[0].lowercased()] + components[1...].map { $0.capitalized }).joined()
    }
    
    // Do a cheap isEmpty check before creating and appending potentially empty strings
    let result : String
    if (leadingUnderscoreRange.isEmpty && trailingUnderscoreRange.isEmpty) {
        result = joinedString
    } else if (!leadingUnderscoreRange.isEmpty && !trailingUnderscoreRange.isEmpty) {
        // Both leading and trailing underscores
        result = String(stringKey[leadingUnderscoreRange]) + joinedString + String(stringKey[trailingUnderscoreRange])
    } else if (!leadingUnderscoreRange.isEmpty) {
        // Just leading
        result = String(stringKey[leadingUnderscoreRange]) + joinedString
    } else {
        // Just trailing
        result = joinedString + String(stringKey[trailingUnderscoreRange])
    }
    return result
}

這段代碼處理的邏輯不算復(fù)雜,功能也不多,但是還是有很多值得學(xué)習(xí)的地方,首先可以看看是如何處理邊界條件的??梢钥吹絻蓚€邊界條件都是用 guard 語法來處理的。

guard !stringKey.isEmpty else { return stringKey }

// Find the first non-underscore character
guard let firstNonUnderscore = stringKey.index(where: { $0 != "_" }) else {
    // Reached the end without finding an _
    return stringKey
}

第一個是判斷空,第二個是通過 String 的 public func index(where predicate: (Character) throws -> Bool) rethrows -> String.Index? 這個函數(shù)來看字符串里是否包含了 ‘_’ 符號,如果沒有包含就直接返回原 String 值。這個函數(shù)的參數(shù)就是一個自定義返回布爾值的 block,返回 true 即刻返回不再繼續(xù)遍歷了,可見蘋果對于性能一點(diǎn)也不浪費(fèi)。

然后這個返回的 index 值還有個作用就是可以得到 ‘’ 符號在最前面后第一個非 ‘’ 符號的字符。因?yàn)樾枨笕绱?,不需要把最前面和最后面?‘’ 轉(zhuǎn)駝峰,但是前面和后面的 ‘’ 符號個數(shù)又不一定,所以需要得到前面 ‘_’ 符號和后面的范圍。

那么得到前面的范圍后,后面的蘋果是怎么做的呢?

// Find the last non-underscore character
var lastNonUnderscore = stringKey.index(before: stringKey.endIndex)
while lastNonUnderscore > firstNonUnderscore && stringKey[lastNonUnderscore] == "_" {
    stringKey.formIndex(before: &lastNonUnderscore);
}

這里正好可以看到對 String 的 public func formIndex(before i: inout String.Index) 函數(shù)的應(yīng)用,這里的參數(shù)定義為 inout 的作用是能夠在函數(shù)里對這個參數(shù)不用通過返回的方式直接修改生效。這個函數(shù)的作用就是移動字符的 index,before 是往前移動,after 是往后移動。

上面的代碼就是先找到整個字符串的最后的 index 然后開始從后往前找,找到不是 ‘_’ 符號時跳出這個 while,同時還要滿足不超過 lastNonUnderscore 的范圍。

在接下內(nèi)容之前可以考慮這樣一個問題,為什么在做前面的判斷時為什么不用 public func formIndex(after i: inout String.Index) 這個方法,after 不是代表從開始往后移動遍歷么,也可以達(dá)到找到第一個不是 ‘_’ 的字符就停止的效果。

蘋果真是雙槍老太婆,一擊兩發(fā),既解決了邊界問題又能解決一個需求,代碼有了優(yōu)化,代碼量還減少了。其實(shí)面試過程中通常都會有些算法題的環(huán)節(jié),很多人都以為只要有了解決思路或者寫出簡單的處理代碼就可以了,我碰到了一些的面試人甚至用中文一條條寫出思路以為就完事了。其實(shí)算法題的考察是分為兩種的,一種是考智商的,就是解決辦法很多或者解決辦法很難,能夠想到解法或者最優(yōu)解是比較困難的,這樣的題適合那些在面談過程中能覺得實(shí)力和深度不錯的人,通過這些題同時還能更多為判斷面試人是否更具創(chuàng)造力,屬于拔尖的考法。還有種是考嚴(yán)謹(jǐn)和實(shí)際項(xiàng)目能力的,這種更多是考察邊界條件的處理,邏輯的嚴(yán)謹(jǐn)還有對代碼優(yōu)化的處理,這種題的解法和邏輯會比較簡單。

_convertFromSnakeCase 這個枚舉的靜態(tài)函數(shù)會在創(chuàng)建 container 的時候調(diào)用,具體使用的函數(shù)是 _JSONKeyedDecodingContainer,在它的初始化方法里會判斷 decoder.options.keyDecodingStrategy 這個枚舉值,滿足 convertFromSnakeCase 就會調(diào)用那個靜態(tài)函數(shù)了。調(diào)用的時候還要注意一個處理就是轉(zhuǎn)換成駝峰后的 key 可能會和已有命名重名,那么就需要選擇進(jìn)行一個選擇,蘋果的選擇是第一個。實(shí)現(xiàn)方式如下:

self.container = Dictionary(container.map {
    key, value in (JSONDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value)
}, uniquingKeysWith: { (first, _) in first })

這里遇到一個 Dictionary 的初始化函數(shù)

public init<S>(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary.Value, Dictionary.Value) throws -> Dictionary.Value) rethrows where S : Sequence, S.Element == (Key, Value)

這個函數(shù)就是專門用來處理上面的重復(fù) key 的問題。如果要選擇最后一個 key 的值用這個函數(shù)也會很容易。

let pairsWithDuplicateKeys = [("a", 1), ("b", 2), ("a", 3), ("b", 4)]

let firstValues = Dictionary(pairsWithDuplicateKeys,
                             uniquingKeysWith: { (first, _) in first })
// ["b": 2, "a": 1]
let lastValues = Dictionary(pairsWithDuplicateKeys,
                            uniquingKeysWith: { (_, last) in last })
// ["b": 4, "a": 3]

枚舉定義 block

KeyEncodingStrategy 還可以自定義 codingKey

case custom((_ codingPath: [CodingKey]) -> CodingKey)

在 container 初始化時會調(diào)用這個 block 來進(jìn)行 key 的轉(zhuǎn)換,同樣如果轉(zhuǎn)換后出現(xiàn)重復(fù) key 也會和 convertFromSnakeCase 一樣選擇第一個。這里可以看到 Swift 里的枚舉還能夠定義一個 block 方便自定義處理自己特定規(guī)則,這樣就可以完全拋棄以前的那種覆蓋 Codable 協(xié)議默認(rèn)實(shí)現(xiàn)的方式了。

inout

上面提到了 public func formIndex(before i: inout Index) 這個函數(shù),那么跟著這個函數(shù)在源碼里看看它的實(shí)現(xiàn),這個函數(shù)是在這個文件里實(shí)現(xiàn)的 swift/IndexSet.swift at master · apple/swift · GitHub

找到這個方法時發(fā)現(xiàn)沒有 inout 定義的同名函數(shù)也還在那里

public func index(before i: Index) -> Index {
    if i.value == i.extent.lowerBound {
        // Move to the next range
        if i.rangeIndex == 0 {
            // We have no more to go
            return Index(value: i.value, extent: i.extent, rangeIndex: i.rangeIndex, rangeCount: i.rangeCount)
        } else {
            let rangeIndex = i.rangeIndex - 1
            let rangeCount = i.rangeCount
            let extent = _range(at: rangeIndex)
            let value = extent.upperBound - 1
            return Index(value: value, extent: extent, rangeIndex: rangeIndex, rangeCount: rangeCount)
        }
    } else {
        // Move to the previous value in this range
        return Index(value: i.value - 1, extent: i.extent, rangeIndex: i.rangeIndex, rangeCount: i.rangeCount)
    }
}

public func formIndex(before i: inout Index) {
    if i.value == i.extent.lowerBound {
        // Move to the next range
        if i.rangeIndex == 0 {
            // We have no more to go
        } else {
            i.rangeIndex -= 1
            i.extent = _range(at: i.rangeIndex)
            i.value = i.extent.upperBound - 1
        }
    } else {
        // Move to the previous value in this range
        i.value -= 1
    }
}

這兩個函數(shù)的實(shí)現(xiàn)最直觀的感受就是 inout 的少了三個 return。還有一個好處就是值類型參數(shù) i 可以以引用方式傳遞,不需要 var 和 let 來修飾

當(dāng)然 inout 還有一個好處在上面的函數(shù)里沒有體現(xiàn)出來,那就是可以方便對多個值類型數(shù)據(jù)進(jìn)行修改而不需要一一指明返回。

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

說到網(wǎng)絡(luò)請求,在 Objective-C 世界里基本都是用的 AFNetworking GitHub - AFNetworking/AFNetworking: A delightful networking framework for iOS, macOS, watchOS, and tvOS. 在 Swift 里就是 Alamofire GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift 。我在 Swift 1.0 之前 beta 版本時就注意到 Alamofire 庫里,那時還是 Mattt Thompson 一個人在寫,文件也只有一個。如今功能已經(jīng)多了很多,但代碼量依然不算太大。我在做 HTN 項(xiàng)目時對于網(wǎng)絡(luò)請求的需求不是那么大,但是也有,于是開始的時候就是簡單的使用 URLSession 來實(shí)現(xiàn)了一下網(wǎng)路請求,就是想直接拉下接口下發(fā)的 JSON 數(shù)據(jù)。

開始結(jié)合著前面解析 JSON 的方法,我這么寫了個網(wǎng)絡(luò)請求:

struct WebJSON:Codable {
    var name:String
    var node:String
    var version: Int?
}
let session = URLSession.shared
let request:URLRequest = NSURLRequest.init(url: URL(string: "http://www.starming.com/api.php?get=testjson")!) as URLRequest
let task = session.dataTask(with: request) { (data, res, error) in
    if (error == nil) {
        let decoder = JSONDecoder()
        do {
            print("解析 JSON 成功")
            let jsonModel = try decoder.decode(WebJSON.self, from: data!)
            print(jsonModel)
        } catch {
            print("解析 JSON 失敗")
        }
    }
}

這么寫是 ok 的,能夠成功請求得到 JSON 數(shù)據(jù)然后轉(zhuǎn)換成對應(yīng)的結(jié)構(gòu)數(shù)據(jù)。不過如果還有另外幾處也要進(jìn)行網(wǎng)絡(luò)請求,拿這一坨代碼不是要到處寫了。那么先看看 Alamofire 干這個活是什么樣子的?

Alamofire.request("https://httpbin.org/get").responseData { response in
    if let data = response.data {
        let decoder = JSONDecoder()
        do {
            print("解析 JSON 成功")
            let jsonModel = try decoder.decode(H5Editor.self, from: data)
        } catch {
            print("解析 JSON 失敗")
        }
    }
}

Alamofire 有 responseJSON 的方法,不過解完是個字典,用的時候需要做很多容錯判斷很不方便,所以還是要使用 JSONDecoder 或者其它第三方庫。不過 Alamofire 的寫法已經(jīng)做了一些簡化,當(dāng)然里面還實(shí)現(xiàn)了更多的功能,我待會再說,現(xiàn)在我的主要任務(wù)是簡化調(diào)用。于是動手改改先前的實(shí)現(xiàn),學(xué)習(xí) Alamofire 的做法,首先創(chuàng)建一個類,然后簡化掉 request 寫法,再建個 block 方便請求完成后的數(shù)據(jù)返回處理,最后使用泛型支持不同 struct 的數(shù)據(jù)統(tǒng)一返回。寫完后,我給這個網(wǎng)絡(luò)類起個名字叫 SMNetWorking 這個類實(shí)現(xiàn)如下:

open class SMNetWorking<T:Codable> {
    open let session:URLSession
    
    typealias CompletionJSONClosure = (_ data:T) -> Void
    var completionJSONClosure:CompletionJSONClosure =  {_ in }
    
    public init() {
        self.session = URLSession.shared
    }
    
    //JSON的請求
    func requestJSON(_ url: SMURLNetWorking,
                     doneClosure:@escaping CompletionJSONClosure
                    ) {
        self.completionJSONClosure = doneClosure
        let request:URLRequest = NSURLRequest.init(url: url.asURL()) as URLRequest
        let task = self.session.dataTask(with: request) { (data, res, error) in
            if (error == nil) {
                let decoder = JSONDecoder()
                do {
                    print("解析 JSON 成功")
                    let jsonModel = try decoder.decode(T.self, from: data!)
                    self.completionJSONClosure(jsonModel)
                } catch {
                    print("解析 JSON 失敗")
                }
                
            }
        }
        task.resume()
    }
    
}

/*----------Protocol----------*/
protocol SMURLNetWorking {
    func asURL() -> URL
}

/*----------Extension---------*/
extension String: SMURLNetWorking {
    public func asURL() -> URL {
        guard let url = URL(string:self) else {
            return URL(string:"http:www.starming.com")!
        }
        return url
    }
}

這樣調(diào)用起來就簡單得多了,看起來如下:

SMNetWorking<WModel>().requestJSON("https://httpbin.org/get") { (jsonModel) in
    print(jsonModel)
}

當(dāng)然這樣寫起來是簡單多了,特別是請求不同的接口返回不同結(jié)構(gòu)時,本地定義了很多的 model 結(jié)構(gòu)體,那么請求時只需要指明不同的 model 類型,block 里就能夠直接返回對應(yīng)的值。

默認(rèn)都按照 GET 方法請求,在實(shí)際項(xiàng)目中會用到其它比如 POST 等方法,Alamofire 的做法是這樣的:

/// HTTP method definitions.
///
/// See https://tools.ietf.org/html/rfc7231#section-4.3
public enum HTTPMethod: String {
    case options = "OPTIONS"
    case get     = "GET"
    case head    = "HEAD"
    case post    = "POST"
    case put     = "PUT"
    case patch   = "PATCH"
    case delete  = "DELETE"
    case trace   = "TRACE"
    case connect = "CONNECT"
}

會先定義一個枚舉,依據(jù)的標(biāo)準(zhǔn)也列在了注釋里。使用起來是這樣的:

Alamofire.request("https://httpbin.org/get") // method defaults to `.get`

Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

可以看出在 request 方法里有個可選參數(shù),設(shè)置完會給 NSURLRequest 的 httpMethod 的這個可選屬性附上設(shè)置的值。

public init(url: URLConvertible, method: HTTPMethod, headers: HTTPHeaders? = nil) throws {
    let url = try url.asURL()
    
    self.init(url: url)
    
    httpMethod = method.rawValue
    
    if let headers = headers {
        for (headerField, headerValue) in headers {
            setValue(headerValue, forHTTPHeaderField: headerField)
        }
    }
}

那么接下來我在 SMNetWorking 類里也加上這個功能,先定義一個枚舉:

enum HTTPMethod: String {
    case GET,OPTIONS,HEAD,POST,PUT,PATCH,DELETE,TRACE,CONNECT
}

利用枚舉的字符串協(xié)議特性,可以將枚舉名直接轉(zhuǎn)值的字符串,可以通過這種方式簡化枚舉定義。

翻下 NSURLRequest 提供的那些可選設(shè)置項(xiàng)還不少,如果把這些設(shè)置都做成一個個可配參數(shù)那么后期維護(hù)會非常麻煩。所以我打算使用鏈?zhǔn)絹砼?。?fix HTTPMethod 這個。

//鏈?zhǔn)椒椒?//HTTPMethod 的設(shè)置
func httpMethod(_ md:HTTPMethod) -> SMNetWorking {
    self.op.httpMethod = md
    return self
}

這里的 op 是個結(jié)構(gòu)體,專門用來存放這些可選項(xiàng)的值的。完整的代碼可以在這里看到 HTN/SMNetWorking.swift at master · ming1016/HTN · GitHub

使用起來也很方便:

SMNetWorking<WModel>().method(.POST).requestJSON("https://httpbin.org/get")

有了這樣一個結(jié)構(gòu)的設(shè)計(jì)后面擴(kuò)展起來會非常方便,不過目前的功能是能夠滿足基本需求的,所以需要完善的比如對于 POST 請求需要的 HTTTP Body,還有 HTTP Headers 的自定義設(shè)置,Authentication 里的 HTTP Basic Authentication,Authentication with URLCredential 等,這些也可以先提供一個接口到外部去設(shè)置。所以可以先建個 block 把 URLRequest 提供出去由外圍設(shè)置。

弄完后的使用效果如下:

SMNetWorking<WModel>().method(.POST).configRequest { (request) in
    //設(shè)置 request
}.requestJSON("https://httpbin.org/get")

就剛才提到的請求參數(shù)來說,Alamofire 是定義了一個 ParameterEncoding 協(xié)議,協(xié)議里規(guī)定一個統(tǒng)一處理的方法 func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest 這樣就可以對多種情況做一樣的返回處理了。從遵循這個協(xié)議的結(jié)構(gòu)體可以看到 URL,JSON 和 PropertyList 都遵循了,那么從實(shí)現(xiàn)這個協(xié)議的 encode 函數(shù)的實(shí)現(xiàn)里可以看到他們都是殊途同歸到 request 的 httpBody 里??梢阅?URLEncoding 看看具體實(shí)現(xiàn):

public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
    var urlRequest = try urlRequest.asURLRequest()
    
    guard let parameters = parameters else { return urlRequest }
    
    if let method = HTTPMethod(rawValue: urlRequest.httpMethod ?? "GET"), encodesParametersInURL(with: method) {
        guard let url = urlRequest.url else {
            throw AFError.parameterEncodingFailed(reason: .missingURL)
        }
        
        if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
            let percentEncodedQuery = (urlComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters)
            urlComponents.percentEncodedQuery = percentEncodedQuery
            urlRequest.url = urlComponents.url
        }
    } else {
        if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
            urlRequest.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
        }
        
        urlRequest.httpBody = query(parameters).data(using: .utf8, allowLossyConversion: false)
    }
    
    return urlRequest
}

泛型協(xié)議式編程

對于目前 HTN 項(xiàng)目來說,請求到了數(shù)據(jù),將 JSON 解析生成了對應(yīng)的 Struct,那么下一步就是要把這個結(jié)構(gòu)化的數(shù)據(jù)生成不同平臺的代碼,比如首先是 Objective-C 代碼,然后是 Swift 代碼,再然后會有 Java 代碼。為了能夠更好的合并多語言里重復(fù)的東西,我打算將處理生成不同語言的實(shí)現(xiàn)遵循相同的協(xié)議,這樣就可以更規(guī)范更減少重復(fù)的實(shí)現(xiàn)這樣的功能了。最終的效果如下:

SMNetWorking<H5Editor>().requestJSON("https://httpbin.org/get") { (jsonModel) in
        let reStr = H5EditorToFrame<H5EditorObjc>(H5EditorObjc()).convert(jsonModel)
        print(reStr)
}

如果是轉(zhuǎn)成 Swift 的話就把 H5EditorObjc 改成 H5EditorSwift 就好了,他們遵循的都是 HTNMultilingualismSpecification 協(xié)議,其它語言依此類推。如果遇到統(tǒng)一的實(shí)現(xiàn),可以建個協(xié)議的擴(kuò)展,然后用統(tǒng)一函數(shù)去實(shí)現(xiàn)就好了。

extension HTNMultilingualismSpecification {
    //統(tǒng)一處理函數(shù)放這里
}

這種設(shè)計(jì)很類似類簇,比如我們熟悉的 NSString 就是這么設(shè)計(jì)的,根據(jù)初始化的不同,比如 initWith 什么的實(shí)例出來的對象是不同的,不過他們都遵循了相同的協(xié)議,所以我們在使用的時候沒有感覺到差別。

HTNMultilingualismSpecification 這個協(xié)議里具體的定義在這里:https://github.com/ming1016/HTN/blob/master/HTNSwift/HTNSwift/Core/HTNFundation/HTNMultilingualism.swift

回頭看看 JSONDecoder 也是使用協(xié)議泛型式編程的一個典范。先看看 decode 函數(shù)的定義

open func decode<T : Decodable>(_ type: T.Type, from data: Data) throws -> T

入?yún)?type 是遵循了統(tǒng)一的 Decodable 協(xié)議的,那么就可以按照統(tǒng)一的方法去做處理,在內(nèi)部實(shí)現(xiàn)時實(shí)際上 JSONDecoder 會代理給 _JSONDecoder 來實(shí)現(xiàn)具體邏輯的。所以在 decode 里的具體實(shí)現(xiàn)值類型轉(zhuǎn)換的 unbox 函數(shù)都是在 _JSONDecoder 的擴(kuò)展里實(shí)現(xiàn)的。unbox 會處理數(shù)字,字符串,布爾值這些基礎(chǔ)數(shù)據(jù)類型,如果有其它層級的結(jié)構(gòu)體也會一層層解下去, _JSONDecoder 的 _JSONDecodingStorage 通過保存最終得到完整的結(jié)構(gòu)體??梢酝ㄟ^下面的代碼看出支持這個過程的結(jié)構(gòu)是怎么設(shè)計(jì)的。首先是 _JSONDecoder 的屬性

/// The decoder's storage.
fileprivate var storage: _JSONDecodingStorage

/// Options set on the top-level decoder.
fileprivate let options: JSONDecoder._Options

/// The path to the current point in encoding.
fileprivate(set) public var codingPath: [CodingKey]

/// Contextual user-provided information for use during encoding.
public var userInfo: [CodingUserInfoKey : Any] {
    return self.options.userInfo
}

下面是初始化

/// Initializes `self` with the given top-level container and options.
fileprivate init(referencing container: Any, at codingPath: [CodingKey] = [], options: JSONDecoder._Options) {
    self.storage = _JSONDecodingStorage()
    self.storage.push(container: container)
    self.codingPath = codingPath
    self.options = options
}

這里可以看到 storage 在初始化時只 push 了頂層,push 的實(shí)現(xiàn)是:

fileprivate mutating func push(container: Any) {
    self.containers.append(container)
}

containers 在定義的時候是個 [Any] 數(shù)組,這樣就允許 container 包含 container 也就是 struct 包含 struct 這樣的結(jié)構(gòu)。

函數(shù)式思想編程

在處理映射成表達(dá)式是設(shè)置布局屬性最復(fù)雜的地方,需要考慮兼顧到各種表達(dá)式情況的處理,這樣救需要設(shè)計(jì)一個類似 SnapKit 那樣可鏈?zhǔn)秸{(diào)用設(shè)置值的結(jié)構(gòu),我先設(shè)計(jì)了一個結(jié)構(gòu)體用來存一些可變的信息

struct PtEqual {
    var leftId = ""
    var left = WgPt.none
    var leftIdPrefix = "" //左前綴
    var rightType = PtEqualRightType.pt
    var rightId = ""
    var rightIdPrefix = ""
    var right = WgPt.none
    var rightFloat:Float = 0
    var rightInt:Int = 0
    var rightColor = ""
    var rightText = ""
    var rightString = ""
    var rightSuffix = ""
    
    var equalType = EqualType.normal
}

對于這些結(jié)構(gòu)的設(shè)置會在 PtEqualC 這個類里去處理,把每個結(jié)構(gòu)體屬性的設(shè)置做成各個函數(shù)返回類本身即可實(shí)現(xiàn)。效果如下:

p.left(.width).leftId(id).leftIdPrefix("self.").rightType(.float).rightFloat(fl.viewPt.padding.left * 2).equalType(.decrease)

不過每次設(shè)置完后需要累加到最后返回的字符串里,這樣一個過程其實(shí)也可以封裝一個簡單函數(shù),比如 add()。這個怎么做能夠更通用呢?比如希望支持不同的累加方法等。

那么可以先設(shè)計(jì)一個累加的 block 屬性

typealias MutiClosure = (_ pe: PtEqual) -> String
var accumulatorLineClosure:MutiClosure = {_ in return ""}

添加累加字符串和換行標(biāo)示

var mutiEqualStr = ""         //累加的字符串
var mutiEqualLineMark = "\n"  //換行標(biāo)識

寫個函數(shù)去設(shè)置這個 block 返回是類自己用于鏈?zhǔn)?/p>

//累計(jì)設(shè)置的 PtEqual 字符串
func accumulatorLine(_ closure:@escaping MutiClosure) -> PtEqualC {
    self.accumulatorLineClosure = closure
    return self
}

最后添加一個函數(shù)專門用來使用的

//執(zhí)行累加動作
func add() {
    if filterBl {
        self.mutiEqualStr += accumulatorLineClosure(self.pe) + self.mutiEqualLineMark
    }
    _ = resetFilter()
}

我們看看用起來是什么效果:

 HTNMt.PtEqualC().accumulatorLine({ (pe) -> String in
    return self.ptEqualToStr(pe: pe)
}).filter({ () -> Bool in
    return vpt.isNormal
}).once({ (p) in
    p.left(.height).rightFloat(fl.viewPt.padding.top * 2).add()
})

細(xì)心的同學(xué)會注意到這里多了兩個東西,一個是 filter, 一個是 once,這兩個函數(shù)里的 block 會把一些通用邏輯進(jìn)行封裝。filter 的設(shè)置會根據(jù)返回決定是否處理后面的 block 或者結(jié)構(gòu)體屬性的設(shè)置,實(shí)現(xiàn)方式如下

//過濾條件
func filter(_ closure: FilterClosure) -> PtEqualC {
    filterBl = closure()
    return self
}

這里的 filterBl 是類的一個屬性,后面會根據(jù)這個屬性來決定動作是否繼續(xù)執(zhí)行。比如屬性的設(shè)置會去判斷

func left(_ wp:WgPt) -> PtEqualC {
    filterBl ? self.pe.left = wp : ()
    return self
}

once 這個函數(shù)也會判斷

func once(_ closure:(_ pc: PtEqualC) -> Void) -> PtEqualC{
    if filterBl {
        closure(self)
    }
    _ = resetPe()
    _ = resetFilter()
    return self
}

同時 once 這個函數(shù)還會重置 filterBl 和重置設(shè)置的結(jié)構(gòu)體,一箭三雕,相當(dāng)于一個完整的設(shè)置周期。

有了這樣一套函數(shù),再復(fù)雜的設(shè)置過程以及邏輯處理都可以很清晰統(tǒng)一的表達(dá)出來,下面可以看一個復(fù)雜布局比如映射成原生表達(dá)式的代碼效果:

//UIView *myViewContainer = [UIView new];
lyStr += newEqualStr(vType: .view, id: cId) + "\n"

//屬性拼裝
lyStr += HTNMt.PtEqualC().accumulatorLine({ (pe) -> String in
    return self.ptEqualToStr(pe: pe)
}).once({ (p) in
    p.left(.top).leftId(cId).end()
    if fl.isFirst {
        //myViewContainer.top = 0.0;
        p.rightType(.float).rightFloat(0).add()
    } else {
        //myViewContainer.top = lastView.bottom;
        p.rightId(fl.lastId + "Container").rightType(.pt).right(.bottom).add()
    }
}).once({ (p) in
    //myViewContainer.left = 0.0;
    p.leftId(cId).left(.left).rightType(.float).rightFloat(0).add()
}).once({ (p) in
    //myViewContainer.width = self.myView.width;
    p.leftId(cId).left(.width).rightType(.pt).rightIdPrefix("self.").rightId(id).right(.width).add()
    
    //myViewContainer.height = self.myView.height;
    p.left(.height).right(.height).add()
}).once({ (p) in
    //self.myView.width -= 16 * 2;
    p.left(.width).leftId(id).leftIdPrefix("self.").rightType(.float).rightFloat(fl.viewPt.padding.left * 2).equalType(.decrease).add()
    
    //self.myView.height -= 8 * 2;
    p.left(.height).rightFloat(fl.viewPt.padding.top * 2).add()
    
    //self.myView.top = 8;
    p.equalType(.normal).left(.top).rightType(.float).rightFloat(fl.viewPt.padding.top).add()
    
    //屬性 verticalAlign 或 horizontalAlign 是 padding 和其它排列時的區(qū)別處理
    if fl.viewPt.horizontalAlign == .padding {
        //self.myView.left = 16;
        p.left(.left).rightFloat(fl.viewPt.padding.left).add()
    } else {
        //[self.myView sizeToFit];
        p.add(sizeToFit(elm: "self.\(id)"))
        p.left(.height).rightType(.pt).rightId(cId).right(.height).add()
        switch fl.viewPt.horizontalAlign {
        case .center:
            p.left(HTNMt.WgPt.center).right(.center).add()
        case .left:
            p.left(.left).right(.left).add()
        case .right:
            p.left(.right).right(.right).add()
        default:
            ()
        }
    }
}).mutiEqualStr

完整代碼在這里:https://github.com/ming1016/HTN/blob/master/HTNSwift/HTNSwift/H5Editor/H5EditorObjc.swift

PS:最近在一個公司分享時有人希望推薦下 iOS 相關(guān)的博客,當(dāng)時我推薦了孫源的博客,其實(shí)孫源也推薦過一個博客,當(dāng)時由于地址沒記住沒有說出來,現(xiàn)在推薦給大家:https://www.mikeash.com/ 他的twitter:https://twitter.com/mikeash

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)載自:https://github.com/Tim9Liu9/TimLiu-iOS[https://github...
    香橙柚子閱讀 9,140評論 0 36
  • 暑假雖然像小溪一樣悄悄地流去了,但暑假的生活卻是豐富多彩的,最令人難忘的,還是西湖的風(fēng)景。 西湖的...
    水果冰糖4321閱讀 215評論 1 0
  • 文化的建立是點(diǎn)點(diǎn)滴滴,文化的走偏也是點(diǎn)點(diǎn)滴滴。窗戶窗簾沒有關(guān)上,麥克風(fēng)的聲音……一繫列細(xì)節(jié)處,無不是企業(yè)文化的體現(xiàn)...
    粟莎閱讀 177評論 0 0
  • 看了杭杭老師的輕手繪的課程,突然之間想起上次嘗試禪繞畫時平靜的心情。據(jù)說這是心靈的瑜伽,是高度注意狀態(tài),可以放松身...
    Sparkle_cc1b閱讀 340評論 2 4
  • 離開家的第12小時,想家噴涌成水雙目流出?!凹摇弊肿忠饧词怯胸i在的房子,所以我們都是或散養(yǎng)或圈養(yǎng)的一條條可愛的豬...
    微冥皇閱讀 466評論 2 5

友情鏈接更多精彩內(nèi)容