如何将 (Swift4) init 添加到 Decodable 协议

How to add a (Swift4) init to the Decodable protocol

我正在尝试创建一个 Codable extension,它能够仅使用 json 字符串初始化 Decodable (Swift 4) 对象。那么应该工作的是:

struct MyObject: Decodable {
   var title: String?
}

let myObject = MyObject(json: "{\"title\":\"The title\"}")

我认为这意味着我应该创建一个使用解码器调用 self.init 的 init。这是我想出的代码:

public init?(json: String) throws {
    guard let decoder: Decoder = GetDecoder.decode(json: json)?.decoder else { return }
    try self.init(from: decoder) // Who what should we do to get it so that we can call this?
}

该代码能够获取解码器,但我在调用 init 时遇到编译器错误。我得到的错误是:

'self' used before self.init call

这是否意味着没有办法在 Decodable 协议中添加一个 init?

有关完整的源代码,请参阅 codable extension on github

更新: 从@appzYourLive 调试下面的解决方案后,我发现我与 Decodable 和 Array 上的 init(json: 初始化程序发生冲突。我刚刚发布了 GitHub 扩展的新版本。我还添加了解决方案作为这个问题的新答案。

A possible workaround

DecodableFromString

一个可能的解决方案是定义另一个协议

protocol DecodableFromString: Decodable { }

有自己的初始化器

extension DecodableFromString {
    init?(from json: String) throws {
        guard let data = try json.data(using: .utf8) else { return nil }
        guard let value = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
        self = value
    }
}

符合 DecodableFromString

现在您需要使您的类型符合 DecodableFromString

struct Person:Codable, DecodableFromString {
    let firstName: String
    let lastName: String
}

结果

最后给了一个JSON

let json =  """
            {
            "firstName": "Luke",
            "lastName": "Skywalker"
            }
            """

你可以建立你的价值

if let luke = try? Person(from: json) {
    print(luke)
}

Person(firstName: "Luke", lastName: "Skywalker")

这是可以做到的:

extension Decodable {

    init?(jsonString: String) throws {
        guard let jsonData = jsonString.data(using: .utf8) else { return nil }
        self = try JSONDecoder().decode(Self.self, from: jsonData)
    }

}

参见:https://bugs.swift.org/browse/SR-5356

[UPD] XCode 9 beta 3 中已修复的问题(有关详细信息,请参阅上述 link)。

我最初的问题是由这两个原因引起的:

  • 我添加的具有相同签名的 init 之间存在冲突 Array 的扩展,当其内部对象是 Codable 时 可编码。
  • 一个 swift 编译器错误,当您使用可失败的时会导致问题 初始化程序。参见 https://bugs.swift.org/browse/SR-5356

因此,由于使用可失败初始化程序存在问题,我最终得到:

public extension Decodable {
    init(json: String) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data) throws {
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

enum CodingError : Error {
    case RuntimeError(String)
}

我还做了一个变体,您可以使用 keyPath 跳转到某个部分:

public extension Decodable {
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            self = try JSONDecoder().decode(Self.self, from: nestedData)
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }
}

您可以在下面找到我的 Decodable 和 Encodable 扩展的完整代码。它也在我的 GitHub 子规格中。使用此扩展程序,您可以使用如下代码:

struct YourCodableObject : Codable {
    var naam: String?
    var id: Int?
}

let json = yourEncodableObjectInstance.toJsonString()
let data = yourEncodableObjectInstance.toJsonData()
let newObject = try? YourCodableObject(json: json)
let newObject2 = try? YourCodableObject(data: data)
let objectArray = try? [YourCodableObject](json: json)
let objectArray2 = try? [YourCodableObject](data: data)
let newJson = objectArray.toJsonString()
let innerObject = try? TestCodable(json: "{\"user\":{\"id\":1,\"naam\":\"Edwin\"}}", keyPath: "user")
try initialObject.saveToDocuments("myFile.dat")
let readObject = try? TestCodable(fileNameInDocuments: "myFile.dat")
try objectArray.saveToDocuments("myFile2.dat")
let objectArray3 = try? [TestCodable](fileNameInDocuments: "myFile2.dat")

这是 2 个扩展:

//
//  Codable.swift
//  Stuff
//
//  Created by Edwin Vermeer on 28/06/2017.
//  Copyright © 2017 EVICT BV. All rights reserved.
//

enum CodingError : Error {
    case RuntimeError(String)
}

public extension Encodable {
    /**
     Convert this object to json data

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json data
     */
    public func toJsonData(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> Data? {
        let encoder = JSONEncoder()
        encoder.outputFormatting = outputFormatting
        encoder.dateEncodingStrategy = dateEncodingStrategy
        encoder.dataEncodingStrategy = dataEncodingStrategy
        return try? encoder.encode(self)
    }

    /**
     Convert this object to a json string

     - parameter outputFormatting: The formatting of the output JSON data (compact or pritty printed)
     - parameter dateEncodinStrategy: how do you want to format the date
     - parameter dataEncodingStrategy: what kind of encoding. base64 is the default

     - returns: The json string
     */
    public func toJsonString(outputFormatting: JSONEncoder.OutputFormatting = .compact, dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate, dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64Encode) -> String? {
        let data = self.toJsonData(outputFormatting: outputFormatting, dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy)
        return data == nil ? nil : String(data: data!, encoding: .utf8)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveTo(_ fileURL: URL) throws {
        guard let data = self.toJsonData() else { throw CodingError.RuntimeError("cannot create data from object")}
        try data.write(to: fileURL, options: .atomic)
    }


    /**
     Save this object to a file in the temp directory

     - parameter fileName: The filename

     - returns: Nothing
     */
    public func saveToTemp(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }



    #if os(tvOS)
    // Save to documents folder is not supported on tvOS
    #else
    /**
     Save this object to a file in the documents directory

     - parameter fileName: The filename

     - returns: true if successfull
     */
    public func saveToDocuments(_ fileName: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileName)
        try self.saveTo(fileURL)
    }
    #endif
}

public extension Decodable {
    /**
     Create an instance of this type from a json string

     - parameter json: The json string
     - parameter keyPath: for if you want something else than the root object
     */
    init(json: String, keyPath: String? = nil) throws {
        guard let data = json.data(using: .utf8) else { throw CodingError.RuntimeError("cannot create data from string") }
        try self.init(data: data, keyPath: keyPath)
    }

    /**
     Create an instance of this type from a json string

     - parameter data: The json data
     - parameter keyPath: for if you want something else than the root object
     */
    init(data: Data, keyPath: String? = nil) throws {
        if let keyPath = keyPath {
            let topLevel = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
            guard let nestedJson = (topLevel as AnyObject).value(forKeyPath: keyPath) else { throw CodingError.RuntimeError("Cannot decode data to object")  }
            let nestedData = try JSONSerialization.data(withJSONObject: nestedJson)
            let value = try JSONDecoder().decode(Self.self, from: nestedData)
            self = value
            return
        }
        self = try JSONDecoder().decode(Self.self, from: data)
    }

    /**
     Initialize this object from an archived file from an URL

     - parameter fileNameInTemp: The filename
     */
    public init(fileURL: URL) throws {
        let data = try Data(contentsOf: fileURL)
        try self.init(data: data)
    }

    /**
     Initialize this object from an archived file from the temp directory

     - parameter fileNameInTemp: The filename
     */
    public init(fileNameInTemp: String) throws {
        let fileURL = try FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInTemp)
        try self.init(fileURL: fileURL)
    }

    /**
     Initialize this object from an archived file from the documents directory

     - parameter fileNameInDocuments: The filename
     */
    public init(fileNameInDocuments: String) throws {
        let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent(fileNameInDocuments)
        try self.init(fileURL: fileURL)
    }
}