こんにちは
AppBrewのはるふです。 この記事は Swift Advent Calendar 2020 の25日目の記事になります!
メリークリスマス!
毎年アドカレ参加しつつも「SwiftじゃなくてiOSやん」という内容を書いていることが多いのですが、今年は弊社のメンバー(@r_plus)から教えて頂いて実際に使っているSwiftネタがあったので紹介します。
Decodable
Swiftではstruct/classがDecodableなメンバだけを持っている場合、Decodableに準拠させるだけで特別な設定も何もなくJSONなどからデコードできるようになります。
struct Spot: Decodable { var id: Int var name: String }
Optionalにしておけば、存在しないプロパティも表現することができます。
struct Spot: Decodable { var id: Int var name: String var isGoodPlace: Bool? }
{ "id": 300, "name": "AppBrew" }
{ "id": 300, "name": "AppBrew", "isGoodPlace": true }
どちらのJSONもデコードすることができ、プロパティが存在しない場合はnilになります。
※ Codableについてより詳しくは以前Qiitaに書いたことがあるのでご参照ください。
Codableについて色々まとめた[Swift4.x] - Qiita
デフォルト値に落としたい
ただし、Booleanの場合などは特にnilではなくデフォルト値にしたいこともあります。
ひとつの方法として、DTO(Data Transfer Object)と内部データとで別々にオブジェクトを定義して、DTOはOptionalに、内部データではnon-Optionalにして変換するのも考えられます。
しかしもし共通のオブジェクトを使う場合は、以下のような手動実装をする必要があります。
struct Spot: Decodable { var id: Int var name: String var isGoodPlace: Bool enum CodingKeys: String, CodingKey { case id case name case isGoodPlace } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(Int.self, forKey: .id) name = try container.decode(String.self, forKey: .name) isGoodPlace = (try? container.decode(Bool.self, forKey: .isGoodPlace)) ?? false } }
これだと、高々デフォルト値のためにコード量が数倍になり、メンテコストも上がってしまいます。
@Zero - KeyedCodable
KeyedCodableというOSSがあり、Codableの簡略化のための実装をいくつも提供してくれています。
この中の @Zero
というものがあり、以下のようにアノテーションしておくとDecode時の初期値として0が入ります。
{ "name": "Jan" }
struct ZeroTestCodable: Codable { var name: String // Jan @Zero var secondName: String // "" - empty string @Zero var numberOfChildren: Int // 0 @Zero var point: Point // Point(x: 0.0, y: 0.0) } struct Point: Codable { let x: Float let y: Float }
0以外入れられないのは難点なんですが、かなりかっこよく実装を省略できています。
※ Swift 5.1で導入されたProperty Wrappersという機能を使っています
EmptyDecoded
LIPSでは必要な機能のみ、EmptyDecoded
として独自に拡張させて入れています。
まずデフォルト値を0の代わりに、以下のような空のイニシャライザの結果として定義しました。
public protocol EmptyInitializable { init() } extension Int: EmptyInitializable {} extension String: EmptyInitializable {} extension Array: EmptyInitializable {} extension Bool: EmptyInitializable {}
これはIntでは0、Arrayでは空配列、Boolではfalseになっています。 心配ではあるので自動テストで確認しています。
これで独自の型について独自にデフォルト値を定義しつつ、もとからある型に対しても大して負担なく初期値を定義できます。 (場合により切り替えられないのは相変わらず難点ではあります)
PropertyWrapperは以下のように定義します。
@propertyWrapper public struct EmptyDecoded<T: Decodable & EmptyInitializable>: Decodable { public var wrappedValue: T public init(wrappedValue: T) { self.wrappedValue = wrappedValue } public init(from decoder: Decoder) throws { guard let container = try? decoder.singleValueContainer(), let value = try? container.decode(T.self) else { wrappedValue = T() return } wrappedValue = value } } extension EmptyDecoded: Equatable where T: Equatable {} extension EmptyDecoded: Hashable where T: Hashable {} extension KeyedDecodingContainer { public func decode<T>(_ type: EmptyDecoded<T>.Type, forKey key: K) throws -> EmptyDecoded<T> where T: Decodable & EmptyInitializable { try EmptyDecoded<T>(from: superDecoder(forKey: key)) } }
すると以下の実装だけで、先程のメンバ抜けJSONをデコードすることができるようになります!
struct Spot: Decodable { var id: Int var name: String @EmptyDecoded var isGoodPlace: Bool }
propertyWrapperなのでletなかったり、Equatable自動実装のために別で準拠させていたり、そういう制約もあります。
まとめたGistはこちら→EmptyDecoded.swift · GitHub
Javaやってると
Gsonみたいな @SerializedName("is_good_place")
とか、もっと色々見通しよく名前やデフォルト値をかける日が来ると楽で良いですね。
We’re Hiring
(最後にちょっとだけ会社の宣伝を失礼します) LIPSではエンジニアを募集しています! 言語に関わらず、プロダクトグロースに興味がある方、経験のある方是非お話しましょう!