はじめに(承前)
本ブログの 以前のポスト で以下の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 []
}
}
}
プロパティラッパと欲しい型、どちらを返却すると筋がいいかまでは理解が及んでいません🙏
説明は以上となります!
最後に
デコード周りは一区切りとします。
また何か知見が出てきたら書くかもしれません。
以上です。
お疲れ様でした!
