如何扩展Date和其他内置类型的Codable功能?

时间:2018-02-16 00:44:56

标签: swift extension-methods decodable

我正在尝试扩展Date.init(from:Decoder)的功能,以处理从我的服务器传递的不同格式。有时,日期将被编码为字符串,有时该字符串嵌套在字典中。根据Swift源代码,Date被解码/编码,如:

extension Date : Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let timestamp = try container.decode(Double.self)
        self.init(timeIntervalSinceReferenceDate: timestamp)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.timeIntervalSinceReferenceDate)
    }
}

所以我尝试按如下方式扩展该功能:

public extension Date {

    private enum CodingKeys: String, CodingKey {
        case datetime
    }

    public init(from decoder: Decoder) throws {
        let dateString: String
        if let container = try? decoder.container(keyedBy: CodingKeys.self) {
            dateString = try container.decode(String.self, forKey: .datetime)
        } else if let string = try? decoder.singleValueContainer().decode(String.self) {
            dateString = string
        } else {
            let timestamp = try decoder.singleValueContainer().decode(Double.self)
            self.init(timeIntervalSinceReferenceDate: timestamp)
            return
        }
        if let date = Utils.date(from: dateString) {
            self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
        } else if let date = Utils.date(from: dateString, with: "yyyy-MM-dd") {
            self.init(timeIntervalSinceReferenceDate: date.timeIntervalSinceReferenceDate)
        } else {
            let context = DecodingError.Context(codingPath: [], debugDescription: "Date format was unparseable.")
            throw DecodingError.dataCorrupted(context)
        }
    }

}

但是从不调用此函数。然后我尝试在KeyedDecodingContainer中扩展Date以更改decode(_:forKey)解码,如下所示:

extension KeyedDecodingContainer {

    private enum TimeCodingKeys: String, CodingKey {
        case datetime
    }

    func decode(_ type: Date.Type, forKey key: K) throws -> Date {
        let dateString: String
        if let timeContainer = try? self.nestedContainer(keyedBy: TimeCodingKeys.self, forKey: key) {
            dateString = try timeContainer.decode(String.self, forKey: .datetime)
        } else if let string = try? self.decode(String.self, forKey: key) {
            dateString = string
        } else {
            let value = try self.decode(Double.self, forKey: key)
            return Date(timeIntervalSinceReferenceDate: value)
        }
        if let date = Utils.date(from: dateString) {
            return date
        } else if let date = Utils.date(from: dateString, with: Globals.standardDateFormat) {
            return date
        } else {
            let context = DecodingError.Context(codingPath: [], debugDescription: "Date format was not parseable.")
            throw DecodingError.dataCorrupted(context)
        }
    }

}

但是,如果调用此方法来解码我通过调用Date编码的container.encode(date, forKey: .date),则会收到typeMismatch错误,表明该数据不是Double。我完全不知道发生了什么,因为encode(to:)的{​​{1}}函数显式编码了Double。我尝试通过Swift源代码中的Date调用来跟踪,并且它似乎没有调用decode

所以我想知道,是否有可能改变Date.init(from:Decoder)类型通过这种扩展解码的方式?我是唯一一个在每个模型中复制自定义Date解码的选项吗?究竟是什么叫Date

1 个答案:

答案 0 :(得分:3)

我终于找到了使用以下代码执行此操作的方法:

fileprivate struct DateWrapper: Decodable {
    var date: Date

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        date = try container.decode(Date.self)
    }
}

extension KeyedDecodingContainer {

    private enum TimeCodingKeys: String, CodingKey {
        case datetime
    }

    func decode(_ type: Date.Type, forKey key: K) throws -> Date {
        let dateString: String
        if let timeContainer = try? self.nestedContainer(keyedBy: TimeCodingKeys.self, forKey: key) {
            dateString = try timeContainer.decode(String.self, forKey: .datetime)
        } else if let string = try? self.decode(String.self, forKey: key) {
            dateString = string
        } else {
            return try self.decode(DateWrapper.self, forKey: key).date
        }
        if let date = Utils.date(from: dateString) {
            return date
        } else if let date = Utils.date(from: dateString, with: "yyyy-MM-dd") {
            return date
        } else {
            let context = DecodingError.Context(codingPath: [], debugDescription: "Date format was not parseable.")
            throw DecodingError.dataCorrupted(context)
        }
    }

}

尝试重新创建Date.init(from:Decoder)代码的问题是类型信息也在plist条目中编码,所以即使我知道日期条目编码为Double,它不会让我提取Double,因为这不是类型标签所说的。我也无法调用decode(Date.self, forKey: key)的默认实现,因为这是我正在编写的函数,这不是子类,因此我无法调用super。我尝试了一些聪明的事情,试图从Decoder中提取具体的KeyedDecodingContainer,这样我就可以直接调用Date.init(from:Decoder),但这不起作用,因为当特定键的上下文丢失时我得到Decoder了。 (如果您对提取Decoder s感到好奇,请参阅https://stablekernel.com/understanding-extending-swift-4-codable/

我知道通过使用Date周围的包装来进行奇怪的解码,我可以达到我想要的效果,但我不想将.date附加到我使用日期的所有地方在我的代码库中。然后我意识到,对于这个我坚持使用的默认情况,包装器允许我从SingleValueDecodingContainer而不是KeyedDecodingContainer中提取日期,允许我调用默认的{{1}解码代码而不是在调用我的自定义函数的无限循环中结束。

这可能是超级猛烈而且不合适,但它确实有效,并且在我能够将API标准化之前,它将为我节省很多样板。

编辑:我重新安排了一点,以便更好地分工,并使其适用于更多的容器类型

Date