Swift 4におけるCodableの使用(二)

16179 ワード

本編はSwift 4におけるCodableの使用シリーズ第2編であり,前回に続き,jsonとモデル間の符号化と復号化の基本的な使用について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マッピング規則は、enumCodingKeyプロトコルを遵守して実装されています.実際には、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