Swift 4におけるCodableの使用(二)
16179 ワード
本編はSwift 4におけるCodableの使用シリーズ第2編であり,前回に続き,jsonとモデル間の符号化と復号化の基本的な使用について
カスタムモデル回転json符号化およびカスタムjson回転モデル復号化のプロセスでは、
まず、プレゼンテーションのためにStudioモデルを定義します.
カスタマイズする前に、この2つの方法をシステムのデフォルトの実装に書き換えてみましょう.この2つの方法について、
符号化および復号化のプロセスでは、属性とjsonのkeyの両方のマッピングのルールを指定するための
書き換えが正しいかどうかを確認します.
印刷の結果は正しいが,今では書き換える方法で原生と同じ効果を実現した.
次に、定義したモデルを逆に見てみましょう.モデルで定義された
構造体を使用してプロトコルを遵守するには、プロトコルの内容を実装する必要があります.ここで、jsonのkeyは
従って、
カスタムencodeでは、時間フォーマット処理、Optional値処理、配列処理に注意する必要があります.
前の記事では、時間フォーマットの処理についても言及しましたが、ここでは、時間フォーマットをカスタマイズする2つの方法があります.
方法1:encodeメソッドで処理する
方法2:汎用関数におけるJSONNcoderオブジェクトのdateEncodingStrategy属性の設定
ここで作成したコンテナは
モデルに属性がオプションでnilの場合、encodeを行うとnull形式でjsonに書き込まれません.
システムによるencodeの実装は、実は私たちが書いたようにcontainerで
ある配列タイプの属性を処理してencodeを行いたい場合があります.compute property処理を使えばいいと思うかもしれませんが、処理後の配列をencodeしたいだけです.元の配列は必要ありません.そこで、encodeをカスタマイズして実現します.そして!突然compute propertyを1つ多く書きたくなくなり、encodeメソッドで処理したいだけなので、containerの
カスタムdecode操作はカスタムencodeと同様で、説明する点も同様に時間フォーマット処理、配列処理ですが、Optional値は無視します.
カスタムdecodeコードを書き出してみると、エラーが表示されます.
ここで時間のフォーマットは浮動小数点数ではなく、一定のフォーマットされた文字列があるため、対応するフォーマットマッチングを行います.操作もカスタムencodeと同様に、
または、
このようなデータを取得すると、
gross_scoreはこの科目の総点数を表し、scoresには点数が総点数に占める割合が入っており、実際の点数に変換して初期化する必要があります.配列の処理については、encodingをカスタマイズするときに使用するコンテナとともにUnkeyedContainerであり、containerの
カスタムencodingとdecodingのプロセスに慣れました.配列処理はcontainerが作成した
私たちが定義したモデルの構造は扁平です.
このようなシナリオでは,containerの
次に検証します.
ネスト構造のjsonとフラットモデルとの間の変換を実現した.
これでencodingとdecodingをカスタマイズする方法を学びました.その鍵は
本文Demo
Codable
プロトコルを学習した.本稿では,Codableにおいて,カスタムモデル回転json符号化とカスタムjson回転モデル復号化のプロセスをどのように実現するかを理解する.カスタムモデル回転json符号化およびカスタムjson回転モデル復号化のプロセスでは、
Codable
プロトコルの符号化および復号方法をこのタイプに書き換えるだけでよい.public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
public init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
まず、プレゼンテーションのためにStudioモデルを定義します.
struct Student: Codable {
let name: String
let age: Int
let bornIn: String
// , json key
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
}
}
システムを書き換える方法で、システムと同じdecodeとencode効果を実現します。
カスタマイズする前に、この2つの方法をシステムのデフォルトの実装に書き換えてみましょう.この2つの方法について、
container
の使い方を把握します. init(name: String, age: Int, bornIn: String) {
self.name = name
self.age = age
self.bornIn = bornIn
}
// decoding
init(from decoder: Decoder) throws {
// , json ,
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let age = try container.decode(Int.self, forKey: .age)
let bornIn = try container.decode(String.self, forKey: .bornIn)
self.init(name: name, age: age, bornIn: bornIn)
}
// encoding
func encode(to encoder: Encoder) throws {
// , json,
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(bornIn, forKey: .bornIn)
}
符号化および復号化のプロセスでは、属性とjsonのkeyの両方のマッピングのルールを指定するための
keyedBy
のパラメータを持つコンテナを作成します.そこで、今回はCodingKeys
のタイプを伝えて、このルールを使用してマッピングすることを説明します.復号のプロセスでは、このコンテナを使用して復号を行い、値のタイプとどのkeyの値を取得するかを指定します.同様に、符号化のプロセスでは、符号化する値とjsonのkeyに対応する値を指定します.Dictionary
の使い方に似ています.前回の汎用関数を使用してencodeとdecodeを行います.func encode(of model: T) throws where T: Codable {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let encodedData = try encoder.encode(model)
print(String(data: encodedData, encoding: .utf8)!)
}
func decode(of jsonString: String, type: T.Type) throws -> T where T: Codable {
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let model = try decoder.decode(T.self, from: data)
return model
}
書き換えが正しいかどうかを確認します.
let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China"
}
"""
let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//▿ __lldb_expr_1.Student
// - name: "Jone"
// - age: 17
// - bornIn: "China"
//{
// "name" : "Jone",
// "age" : 17,
// "born_in" : "China"
//}
印刷の結果は正しいが,今では書き換える方法で原生と同じ効果を実現した.
structを使用してCodingKeyを遵守してマッピングルールを指定する
次に、定義したモデルを逆に見てみましょう.モデルで定義された
CodingKeys
マッピング規則は、enum
でCodingKey
プロトコルを遵守して実装されています.実際には、CodingKeys
のタイプをstruct
と定義して、CodingKey
プロトコルを実装することもできます. // , json key
// enum CodingKeys: String, CodingKey {
// case name
// case age
// case bornIn = "born_in"
// }
// , json key
struct CodingKeys: CodingKey {
var stringValue: String //key
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
// decode , stringValue json key, key
// encode , stringValue json key, key
init?(stringValue: String) {
self.stringValue = stringValue
}
// enum case
static let name = CodingKeys(stringValue: "name")!
static let age = CodingKeys(stringValue: "age")!
static let bornIn = CodingKeys(stringValue: "born_in")!
}
構造体を使用してプロトコルを遵守するには、プロトコルの内容を実装する必要があります.ここで、jsonのkeyは
String
タイプであるため、intValue
未満であるため、nilに戻ることができます.再実行しても、結果は正しいです.ただし、enum
を使用してCodingKey
プロトコルを遵守しない場合、例えばstruct
を使用して、Codable
プロトコルの符号化および復号方法を書き直さなければなりません.No者はエラーを報告します.cannot automatically synthesize 'Decodable' because 'CodingKeys' is not an enum
cannot automatically synthesize 'Encodable' because 'CodingKeys' is not an enum
従って、
struct
を使用してCodingKey
を遵守することは、enum
を使用するよりも工事量が大きい.では、なぜこのような使い方を提案するのですか.特定の状況では出場する機会があるため、struct
を使用してマッピングルールを指定するとより柔軟になり、第3編の例では使用シーンについて説明しますが、ここではまずその働き方がわかります.カスタムEncoding
カスタムencodeでは、時間フォーマット処理、Optional値処理、配列処理に注意する必要があります.
タイムフォーマット処理
前の記事では、時間フォーマットの処理についても言及しましたが、ここでは、時間フォーマットをカスタマイズする2つの方法があります.
方法1:encodeメソッドで処理する
struct Student: Codable {
let registerTime: Date
enum CodingKeys: String, CodingKey {
case registerTime = "register_time"
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
let stringDate = formatter.string(from: registerTime)
try container.encode(stringDate, forKey: .registerTime)
}
}
方法2:汎用関数におけるJSONNcoderオブジェクトのdateEncodingStrategy属性の設定
encoder.dateEncodingStrategy = .custom { (date, encoder) in
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy HH:mm:ssZ"
let stringDate = formatter.string(from: date)
var container = encoder.singleValueContainer()
try container.encode(stringDate)
}
ここで作成したコンテナは
singleValueContainer
です.ここではencodeメソッドのようにコンテナに値を常に追加する必要はありませんので、単一の値のコンテナを使用すればいいです.try! encode(of: Student(registerTime: Date()))
//{
// "register_time" : "Nov-13-2017 20:12:57+0800"
//}
Optional値処理
モデルに属性がオプションでnilの場合、encodeを行うとnull形式でjsonに書き込まれません.
struct Student: Codable {
var scores: [Int]?
}
try! encode(of: Student())
//{
//
//}
システムによるencodeの実装は、実は私たちが書いたようにcontainerで
encode
メソッドを呼び出すのではなく、encodeIfPresent
というメソッドを呼び出すので、nilに対してencodeを行わない.friendsをjsonに強制的に書き込むことができます.struct Student: Codable {
var scores: [Int]?
enum CodingKeys: String, CodingKey {
case scores
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(scores, forKey: .scores)
}
}
try! encode(of: Student())
//{
// "scores" : null
//}
はいれつしょり
ある配列タイプの属性を処理してencodeを行いたい場合があります.compute property処理を使えばいいと思うかもしれませんが、処理後の配列をencodeしたいだけです.元の配列は必要ありません.そこで、encodeをカスタマイズして実現します.そして!突然compute propertyを1つ多く書きたくなくなり、encodeメソッドで処理したいだけなので、containerの
nestedUnkeyedContainer(forKey:)
メソッドを使用してUnkeyedEncdingContainer
(名前の通り、配列にはkeyがない)を作成して配列を処理すればいいのです.struct Student: Codable {
let scores: [Int] = [66, 77, 88]
enum CodingKeys: String, CodingKey {
case scores
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
// (UnkeyedEncdingContainer)
var unkeyedContainer = container.nestedUnkeyedContainer(forKey: .scores)
try scores.forEach {
try unkeyedContainer.encode("\($0) ")
}
}
}
try! encode(of: Student())
//{
// "scores" : [
// "66 ",
// "77 ",
// "88 "
// ]
//}
カスタムDecoding
カスタムdecode操作はカスタムencodeと同様で、説明する点も同様に時間フォーマット処理、配列処理ですが、Optional値は無視します.
タイムフォーマット処理
カスタムdecodeコードを書き出してみると、エラーが表示されます.
struct Student: Codable {
let registerTime: Date
enum CodingKeys: String, CodingKey {
case registerTime = "register_time"
}
init(registerTime: Date) {
self.registerTime = registerTime
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let registerTime = try container.decode(Date.self, forKey: .registerTime)
self.init(registerTime: registerTime)
}
}
let res = """
{
"register_time": "2017-11-13 22:30:15 +0800"
}
"""
let stu = try! decode(of: res, type: Student.self) ❌
// error: Expected to decode Double but found a string/data instead.
ここで時間のフォーマットは浮動小数点数ではなく、一定のフォーマットされた文字列があるため、対応するフォーマットマッチングを行います.操作もカスタムencodeと同様に、
init(from decoder: Decoder
の方法を変更します. init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let dateString = try container.decode(Date.self, forKey: .registerTime)
let formaater = DateFormatter()
formaater.dateFormat = "yyyy-MM-dd HH:mm:ss z"
let registerTime = formaater.date(from: dateString)!
self.init(registerTime: registerTime)
}
または、
JSONDecoder
オブジェクトのdateDncodingStrategy
プロパティにcustomを使用して変更できます.decoder.dateDecodingStrategy = .custom{ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateString = try container.decode(String.self)
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
return formatter.date(from: dateString)!
}
はいれつしょり
このようなデータを取得すると、
let res = """
{
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
"""
gross_scoreはこの科目の総点数を表し、scoresには点数が総点数に占める割合が入っており、実際の点数に変換して初期化する必要があります.配列の処理については、encodingをカスタマイズするときに使用するコンテナとともにUnkeyedContainerであり、containerの
nestedUnkeyedContainer(forKey: )
メソッドでUnkeyedDecodingContainer
を作成し、このunkeyedContainerから値を取り出してdecodeし、そのタイプを指定します.struct Student: Codable {
let grossScore: Int
let scores: [Float]
enum CodingKeys: String, CodingKey {
case grossScore = "gross_score"
case scores
}
init(grossScore: Int, scores: [Float]) {
self.grossScore = grossScore
self.scores = scores
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let grossScore = try container.decode(Int.self, forKey: .grossScore)
var scores = [Float]()
// (UnkeyedDecodingContainer)
var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .scores)
// isAtEnd:A Boolean value indicating whether there are no more elements left to be decoded in the container.
while !unkeyedContainer.isAtEnd {
let proportion = try unkeyedContainer.decode(Float.self)
let score = proportion * Float(grossScore)
scores.append(score)
}
self.init(grossScore: grossScore, scores: scores)
}
}
フラット化JSONの符号化と復号化
カスタムencodingとdecodingのプロセスに慣れました.配列処理はcontainerが作成した
nestedUnkeyedContainer(forKey: )
が作成したunkeyedContainerが処理することも知っています.次に、ネストされた構造を含むデータのセットがあるとします.let res = """
{
"name": "Jone",
"age": 17,
"born_in": "China",
"meta": {
"gross_score": 120,
"scores": [
0.65,
0.75,
0.85
]
}
}
"""
私たちが定義したモデルの構造は扁平です.
struct Student {
let name: String
let age: Int
let bornIn: String
let grossScore: Int
let scores: [Float]
}
このようなシナリオでは,containerの
nestedContainer(keyedBy:, forKey: )
法を用いて作成したKeyedContainer処理と同様にインサートタイプを処理するコンテナであり,配列のようなunkeyのインサートタイプを処理するコンテナがある以上,辞書のようなkeyのあるインサートタイプを処理するコンテナも自然にあり,encodingではKeyedEncodingContainer
タイプであり,decodingではもちろんKeyedDecodingContainer
タイプであり,encodingとdecodingは似ているからです.struct Student: Codable {
let name: String
let age: Int
let bornIn: String
let grossScore: Int
let scores: [Float]
enum CodingKeys: String, CodingKey {
case name
case age
case bornIn = "born_in"
case meta
}
//
enum MetaCodingKeys: String, CodingKey {
case grossScore = "gross_score"
case scores
}
init(name: String, age: Int, bornIn: String, grossScore: Int, scores: [Float]) {
self.name = name
self.age = age
self.bornIn = bornIn
self.grossScore = grossScore
self.scores = scores
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let age = try container.decode(Int.self, forKey: .age)
let bornIn = try container.decode(String.self, forKey: .bornIn)
// (KeyedDecodingContainer), json key
let keyedContainer = try container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
let grossScore = try keyedContainer.decode(Int.self, forKey: .grossScore)
var unkeyedContainer = try keyedContainer.nestedUnkeyedContainer(forKey: .scores)
var scores = [Float]()
while !unkeyedContainer.isAtEnd {
let proportion = try unkeyedContainer.decode(Float.self)
let score = proportion * Float(grossScore)
scores.append(score)
}
self.init(name: name, age: age, bornIn: bornIn, grossScore: grossScore, scores: scores)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
try container.encode(bornIn, forKey: .bornIn)
// (KeyedEncodingContainer), json key
var keyedContainer = container.nestedContainer(keyedBy: MetaCodingKeys.self, forKey: .meta)
try keyedContainer.encode(grossScore, forKey: .grossScore)
var unkeyedContainer = keyedContainer.nestedUnkeyedContainer(forKey: .scores)
try scores.forEach {
try unkeyedContainer.encode("\($0) ")
}
}
}
次に検証します.
let stu = try! decode(of: res, type: Student.self)
dump(stu)
try! encode(of: stu)
//▿ __lldb_expr_82.Student
// - name: "Jone"
// - age: 17
// - bornIn: "China"
// - grossScore: 120
// ▿ scores: 3 elements
// - 78.0
// - 90.0
// - 102.0
//
//{
// "age" : 17,
// "meta" : {
// "gross_score" : 120,
// "scores" : [
// "78.0 ",
// "90.0 ",
// "102.0 "
// ]
// },
// "born_in" : "China",
// "name" : "Jone"
//}
ネスト構造のjsonとフラットモデルとの間の変換を実現した.
これでencodingとdecodingをカスタマイズする方法を学びました.その鍵は
container
の使用を把握することと、状況によって異なるcontainerを使用することです.実際の状況は千差万別ですが、やり方はいつも似ています.本文Demo