Swift 如何扩展日期和其他内置类型的可编码功能?

Swift 如何扩展日期和其他内置类型的可编码功能?,swift,extension-methods,decodable,Swift,Extension Methods,Decodable,我正在尝试扩展Date.init(from:Decoder)的功能,以处理从服务器传递的不同格式。有时日期将被编码为字符串,有时该字符串嵌套在字典中。根据Swift来源,日期被解码/编码如下: extension Date : Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let timesta

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

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(u: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)编码的
日期时,我会得到一个
类型不匹配
错误,即数据不是
双精度
。我完全搞不懂发生了什么,因为
日期
编码(to:)
函数显式地对双精度编码。我试图通过Swift源代码中的
decode
调用追踪我的路径,但它似乎从未调用
Date.init(from:Decoder)


所以我想知道,是否有可能通过这种扩展来改变
日期
类型的解码方式?我是否可以在每个型号中复制自定义的
日期
解码?究竟是什么调用了
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)
,但这不起作用,因为当我拿回
解码器时,特定密钥的上下文丢失了。(查看您是否对提取
解码器
s感兴趣)

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

这可能是超级jank和不合适的,但它可以工作,并将为我节省大量的样板文件,直到我可以使我的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
    }

}

我终于找到了一种使用以下代码实现这一点的方法:

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)
,但这不起作用,因为当我拿回
解码器时,特定密钥的上下文丢失了。(查看您是否对提取
解码器
s感兴趣)

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

这可能是超级jank和不合适的,但它可以工作,并将为我节省大量的样板文件,直到我可以使我的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
    }

}

您不能通过在扩展中提供自己的版本来覆盖其他人的方法。如果您使用的是
JSONDecoder
,有什么原因不能使用它吗?如果没有,或者您正试图为所有
编码器和
解码器解决此问题-写入解决方案是编写一个适配器类型,它包装
日期,并提供自己的
init(from:)
,如上所述。然后,在编码/解码之前,将
日期
包装在适配器类型中。我不能使用
日期解码策略
,因为我需要一个可以与所有解码器一起工作的解决方案,并且可以处理从我使用的API传递的嵌套日期对象。我想你是对的,我只需要创建一个
Date
的子类,而不是试图添加到默认的
Date
行为中,我越深入地研究它,就越觉得我的方法不正确。因为
Date
是一个
struct
而不是一个类,所以你不能对它进行子类化-