はじめに(承前)
本ブログの 以前のポスト で以下のJSONをデコードしようとしました。
{
"食品名":"肉類",
"食材": "鶏肉, 豚肉, 牛肉",
"栄養": ["タンパク質", "ビタミンA", "ビタミンB"]
}
食材
(String) と 栄養
(String の配列) を、どちらも文字列の配列でデコードしたいという話でした。
そのためのコードとして以下を書きました。
struct Food: Decodable {
let 食品名: String
let 食材: [Foodstuff] // 独自の型に変更
let 栄養: [String]
}
struct Foodstuff: Decodable {
let value: String
}
extension KeyedDecodingContainer {
func decode(_: [Foodstuff].Type, forKey key: Key) throws -> [Foodstuff] {
let valueString = try decodeIfPresent(String.self, forKey: key)
let valueArray = valueString?.components(separatedBy: ",")
return valueArray?.map { Foodstuff(value: $0) } ?? [] // 独自の型に変更
}
}
[Foodstuff]
という型に変えてしまえば、KeyedDecodingContainer
のエクステンションで食材
要素のみに独自のデコード処理を適応できます。
で、 Foodstuff.first?.value
とかで文字列を取得できます。
いやー[String]
で扱いたいですよね!
KeyedDecodingContainer
のエクステンションではなくinit(from decoder: Decoder) throws {
で
自前でデコード処理を書けば[String]
でデコードできます。
が、それはそれで手間です。
ということで、「一部の要素にのみ独自のデコード処理を適用しつつ、狙った型に変換する手法」
を書いていきます。
タイトルが長い!
前提条件
Xcode: 12.4
Swift: 5.3
まずは最終的なコード
struct Food: Decodable {
let 食品名: String
@StringComponents var 食材: [String]
let 栄養: [String]
}
extension KeyedDecodingContainer {
func decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
if let value = try decodeIfPresent(StringComponents.self, forKey: key) {
return value
} else {
return StringComponents(wrappedValue: [])
}
}
}
@propertyWrapper
struct StringComponents: Decodable {
var wrappedValue: [String] = []
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let valueString = try container.decode(String.self)
wrappedValue = valueString.components(separatedBy: ",")
}
init(wrappedValue: [String]) {
self.wrappedValue = wrappedValue
}
}
これで狙い通り、食材
を [String]
として扱えます。
説明
食材
に、定義した StringComponents
のPropertyWrapper を適応します。
(PropertyWrapper の基本的な挙動については、
以前のポスト[iOS] Property Wrapper の概要について にてまとめています)
KeyedDecodingContainer
のエクステンションにfunc decode(_: StringComponents.Type, forKey key: Key) throws -> StringComponents {
を定義します。
そうすると食材
をデコードする時のみ、その処理を通ります。
その処理の中で if let value = try decodeIfPresent(StringComponents.self, forKey: key) {
でデコードを試みます。
実際のデコード処理は StringComponents
の init(from decoder: Decoder) throws {
にて行います。
ここで String
を [String]
に変換しています。
あとは StringComponents
を返却して、食材
のデコードは完了です。
結果をデバッガを見ると以下のようになります。

左のペインでは _食材
というプロパティ名で表示されますが、.食材
でアクセスすると [String]
で取得できています。
また、KeyedDecodingContainer
のエクステンションを変更して、
以下のように返り値などを [String]
にしても同様のデコード結果になります。
extension KeyedDecodingContainer {
func decode(_: StringComponents.Type, forKey key: Key) throws -> [String] {
if let valueString = try decodeIfPresent(String.self, forKey: key) {
return valueString.components(separatedBy: ",")
} else {
return []
}
}
}
プロパティラッパと欲しい型、どちらを返却すると筋がいいかまでは理解が及んでいません🙏
説明は以上となります!
最後に
デコード周りは一区切りとします。
また何か知見が出てきたら書くかもしれません。
以上です。
お疲れ様でした!