Photo by Markus Spiske on Unsplash
說到 Codable 已經推出很久了,也用了好幾年了,但是對它還是不算是很了解,該怎麼說呢,因為Codable 最常運用在解析 API Response,基本上寫完之後,除非API 回傳值要改變,大概到離職的那一刻都不會再去修改它了,所以即使它推出了很多年了,實際上接觸的時間意外地少。
最近正好有機會再次寫不少 Codable 的部分,發現了一些過去不知道的東西,把它們記錄在這裡。
CodingKey
最常使用在API 回傳值的Key 風格 不是 Swift 習慣的風格,做為對應解析之用
enum CodingKeys: String, CodingKey {
case id = "product_id"
case name
case description
case expiryDate = "expiry_date"
}
在API 回傳值裡的 product_id
就會被放到 id
裡,這是非常好用的地方。
另一個好用的地方在於也可以讓decoder只解析寫在 CodingKeys 裡的那些參數,舉個例
struct SubItem: Decodable,Identifiable {
var id = UUID().string
var name: String
var description: String
var isActive: Bool
}
SubItem
conform Identifiable protocol ,因此有一個參數是id
且還是直接使用UUID,並不希望它被拿來當解析用的參數時,就可以使用 CodingKey來避開 id
,就不需要再寫 init(from decoder: any Decoder)
struct SubItem: Decodable,Identifiable {
var id = UUID().string
var name: String
var description: String
var isActive: Bool
enum CodingKeys: String, CodingKey {
case name
case description
case isActive
}
}
Try? & decodeIfPresent()
基於因為不知道API 會回傳什麼,是否真的會依照文件回傳,為了讓App 不會因為回傳值問題需導至 Crash,一般而言,除非100%保証一定會有值的情況下,大多數參數都會被設定為 Optional,在解析的時候就會用上 decodeIfPresent
,當回傳值完全沒有這個參數,或是參數是nil(null)的時候,它會回傳 nil。
一般情況下,這樣就幾乎不會有什麼問題,但是,就是這個但是,當回傳值是有值的時候,但偏偏回傳的資料型態是錯誤的,這樣的寫法就還是會解析失敗,導致整個物件都會是nil。
如果你是覺得「為什麼會回傳資料型態不同的資料來啊? 這不合理啊」,沒錯,理解上不應該出現回傳值的資料有誤的情況,多數情況下都會要求修正。因為App 端對於資料型態相對來說是嚴格的,解析失敗最後就可能造成 Crash ,所以最好是大家都遵守好訂好的資料型態,不應該回傳不正確的型態。
在有點歷史的產品裡或是一開始就沒有 App 端產品就可能會發生,回傳值會變成不預期的資料型態問題,且可能也不是馬上可以修正的情況,這種時候就需要先確認有問題的資料是不是需要用的,若是需要用的就必須要再另外處理,若是不需要用的資料,就是try?
出馬的時候了!
try?
本來就是在解析失敗的時候會回傳nil ,本來比較常會和 decode
() 一起使用,如果和 decodeIfPresent
一起使用,所有可以會造成問題的情況都會包含到,這樣就比較不用怕因為資料型態解析失敗問題,當然,前提是有問題的資料並不是會使用到的資料
以下就是合體技的寫法
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try? container.decodeIfPresent(String.self, forKey: .id)
name = try? container.decodeIfPresent(String.self, forKey: .name)
}
假如運氣特好遇到…
萬一真的運氣特好遇到同一支API 回傳值在不同情況下某一個參數會回傳完全不同的型態,偏偏你又必須要使用這些參數,那只好自定義解析方式了,方法上網Google 一下就會找得到了,不然也可以詢問 Claude AI.
其方式是 extenstion KeyedDecodingContainer
這個Struct ,寫一個通用的 decode
,之後就可以利用這個 decode
為它加上一些自定義的解析方式
func decodeAny<T>(_: T.Type, forKey key: K, transform: ((Any) -> T?)? = nil) throws -> T? {
guard contains(key) else { return nil }
//預設轉換邏輯
let defaultTransform: (Any) -> T? = { value in
if let typedValue = value as? T {
return typedValue
}
if let stringConvertible = T.self as? LosslessStringConvertible.Type {
let stringValue = String(describing: value)
return stringConvertible.init(stringValue) as? T
}
return nil
}
let transformer = transform ?? defaultTransform
//不同型別的解法
let attempts: [(Any.Type, (K) throws -> Any)] = [
(String.self, {try decode(String.self, forKey: $0) as Any}),
(Int.self, {try decode(Int.self, forKey: $0) as Any}),
(Double.self, {try decode(Double.self, forKey: $0) as Any}),
(Bool.self, {try decode(Bool.self, forKey: $0) as Any})
]
// value: 取得API 資料的型態,transformed: 利用transformer 解析成需要的型態
for(_, decoder) in attempts {
if let value = try? decoder(key),
let transformed = transformer(value) {
return transformed
}
}
return nil
}
例如 Bool
是最常遇到擁有各種不同型態的表示法的,就可以為它來寫一個方便的解析方式
func decodeBool(forKey key: K) throws -> Bool? {
try decodeAny(Bool.self, forKey: key) { value in
switch value {
case let boolValue as Bool:
return boolValue
case let intValue as Int:
return intValue != 0
case let doubleValue as Double:
return doubleValue != 0
case let stringValue as String:
let lowercased = stringValue.lowercased()
if ["true", "t", "yes", "1"].contains(lowercased) { return true }
if ["false", "f", "no", "0"].contains(lowercased) { return false }
return nil
default nil
}
}
}
說真的,最好是不要遇到需要這樣寫的情況,為什麼呢? 主要的原因是萬一真的是 API 的錯誤,但因為App 端有自定義解析的方式,所以就解析成功了,那麼就可能完全不會有人知道可能真的是資料錯誤的問題,當這樣資料數量多了之後,也許就會造成後續資料分析有誤,或有些神奇的情況。
最後,祝大家 Coding 愉快!!