最近の Swift では Decodable
に準拠したオブジェクトを簡単に JSON から変換できる。
上のようにあらかじめかっちりと定義した通りのデータが与えられるときは自動でやってくれて便利だけど、実際にはそうもいかなくて難しい。
例えば、次のようにバリエーションが複数ある場合:
BoolParameter
と NumberParameter
は同列な存在でどちらも Parameter
に準拠させている。JSON では独自の型情報を定義できないので Parameter.type
を用意して区別することにする。
面倒
これをプロパティに持つ場合の Root
はこんな感じだろうか:
だがこれはエラーが出てコンパイルできない。
Parameter
プロトコルでなく直接 BoolParameter
や NumberParameter
を指定すれば動いてくれるものの、どちらか一方にしか使えなくなってしまう。
こういう場合、Root
の Decodable
を自力で実装するのがよくある解決策のようだ。具体的には:
Root
の持つプロパティのうち Decodable
を適用させたい対象の一覧を CodingKey
に準拠した enum
で定義
Root.init(from decoder:)
の中で 1 の enum
を Decoder.container(keyedBy:)
に与えて、全キーの値を持つ KeyedDecodingContainer
を取得
- 各キーに対して
KeyedDecodingContainer.decode(_,forKey:)
で値を取得して Root
を初期化していく
...面倒! 今回の例では単純化しているけど実際には Root
に大量のプロパティがあってもおかしくない。ほんの一部のプロパティで凝ったことをするだけでも、途端にすべてを手動で触らなければいけなくなってしまうなんて。
解決策:Property Wrapper を使おう
そこでたどり着いた解決策は Property Wrapper だ。それを Decodable
に準拠させて派生型への分岐処理をここに詰め込み、各々の init(from coder:)
で生成したものを wrappedValue
としている。
すると Root
はこんなにシンプルになる:
SwiftUI の印象が強かった Property Wrapper だけど、思ったよりも幅広く役立ちそうだ。
おまけ:複数の場合
プロパティの型が単体の Parameter
ではなく [Parameter]
だったら...?
先ほどと同じような Property Wrapper を作るのだけど、unkeyedContainer
を 2 つ用意する点に注意。どうやら内部に現在のパース位置というかカーソルみたいな情報を持っているらしく、type
を調べるときとデコードするときの両方でそれが増加してしまうためだ。
2020.7.17 追記
takasek さんがより汎用化を目指したコードを書いてくれた!
元コードから変わった点
- 型情報を失いたくないのでデータの派生はenumで表現
- CustomDecodedParameter をデータごとにいちいち作らなくていいようドメイン固有の処理を切り出し、共通部分をprotocol extensionに
- typeが想定外だったときにfatalErrorだと失敗時にアプリごと死ぬので、適切にCodingErrorをthrow
- 単体でもArrayでも同じように扱えるように工夫していたらPropertyWrapperが消滅してしまいました…
なるほど、同じ目的の違うアプローチを見るのはとても勉強になる。
Share
リンクも共有もお気軽に。記事を書くモチベーションの向上に役立てます。