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

How to extend Codable functionality of Date and other built in types?

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

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 以更改 decode(_:forKey) 中的 Date 解码,如下所示:

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)
        }
    }

}

但是,当调用它来解码我通过调用 container.encode(date, forKey: .date) 编码的 Date 时,我得到一个 typeMismatch 错误,表明数据不是 Double .我对发生的事情感到非常困惑,因为 Dateencode(to:) 函数显式编码了一个 Double。我尝试通过 Swift 源代码中的 decode 调用来追踪我的方式,它似乎从未调用过 Date.init(from:Decoder)

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

我终于想出了一个方法来用下面的代码来做到这一点:

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。我尝试了一些聪明的事情,试图从 KeyedDecodingContainer 中提取具体的 Decoder,这样我就可以直接调用 Date.init(from:Decoder),但这没有用,因为特定键的上下文在我得到了 Decoder 回来。 (如果您对提取 Decoder 感到好奇,请参阅 https://stablekernel.com/understanding-extending-swift-4-codable/)。

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

这可能是超级垃圾和不合适的,但它有效,并且会在我 API 标准化之前为我节省大量样板文件。

编辑:我对它进行了一些重新安排,以便更好地划分职责并使其适用于更多容器类型

fileprivate struct DateWrapper: Decodable {

    var date: Date

    private enum TimeCodingKeys: String, CodingKey {
        case datetime
    }

    init(from decoder: Decoder) throws {
        let dateString: String
        if let timeContainer = try? decoder.container(keyedBy: TimeCodingKeys.self) {
            dateString = try timeContainer.decode(String.self, forKey: .datetime)
        } else {
            let container = try decoder.singleValueContainer()
            if let string = try? container.decode(String.self) {
                dateString = string
            } else {
                date = try container.decode(Date.self)
                return
            }
        }
        if let date = Utils.date(from: dateString) {
            self.date = date
        } else if let date = Utils.date(from: dateString, with: "yyyy-MM-dd") {
            self.date = date
        } else {
            let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Date format was not parseable.")
            throw DecodingError.dataCorrupted(context)
        }
    }
}

extension KeyedDecodingContainer {

    func decode(_ type: Date.Type, forKey key: K) throws -> Date {
        return try self.decode(DateWrapper.self, forKey: key).date
    }

    func decode(_ type: [Date].Type, forKey key: K) throws -> [Date] {
        var container = try nestedUnkeyedContainer(forKey: key)
        var dates: [Date] = []
        while !container.isAtEnd {
            dates.append(try container.decode(Date.self))
        }
        return dates
    }

}

extension UnkeyedDecodingContainer {

    mutating func decode(_ type: Date.Type) throws -> Date {
        return try self.decode(DateWrapper.self).date
    }

}