Codextended

Swift Package Manager Mac + Linux Twitter: @johnsundell

欢迎使用 Codextended —— 一套扩展,旨在通过提供类型推断能力和便利性,使 Swift 的 Codable API 更易于使用。它不是一个包装器,也不是一个全新的框架,而是以非常轻量级的方式直接增强 Codable

Codable 非常棒!

没有第三方序列化框架可以胜过 Codable 的便利性。由于它是内置的,因此它既可以利用编译器自动合成许多情况下所需的所有序列化代码,也可以用作多个不同模块之间的通用桥梁——而无需引入任何共享依赖项。

但是,一旦需要某种形式的自定义——例如转换解码数据的部分,或为某些键提供默认值——标准的 Codable API 就会变得非常冗长。它也没有利用 Swift 强大的类型推断功能,从而产生了大量不必要的样板代码。

这正是 Codextended 旨在解决的问题。

示例

以下是一些示例,展示了使用“原生” CodableCodextended 添加到它的 API 之间的区别。目标是将所有常见的序列化操作变成一行代码,而不是必须设置大量的样板代码。

🏢 顶层 API

Codextended 对用于编码和解码值的顶层 API 进行了一些细微的调整,从而可以利用类型推断并使用正在编码或解码的实际值的方法。

🍨 使用原生 Codable

// Encoding
let encoder = JSONEncoder()
let data = try encoder.encode(value)

// Decoding
let decoder = JSONDecoder()
let article = try decoder.decode(Article.self, from: data)

🦸‍♀️ 使用 Codextended

// Encoding
let data = try value.encoded()

// Decoding
let article = try data.decoded() as Article

// Decoding when the type can be inferred
try saveArticle(data.decoded())

🔑 覆盖单个键的行为

只要序列化数据的格式与将使用它的 Swift 类型的格式完全匹配,Codable 就非常出色——但只要我们需要进行一个小小的调整,事情就会很快从非常方便变为非常冗长。

例如,假设我们只想为一个属性提供一个默认值(而不必将其设为可选,这会使其在代码库的其余部分中更难处理)。为此,我们需要完全手动实现我们类型的解码——如下所示,为 Article 类型的 tags 属性。

🍨 使用原生 Codable

struct Article: Codable {
    enum CodingKeys: CodingKey {
        case title
        case body
        case footnotes
        case tags
    }

    var title: String
    var body: String
    var footnotes: String?
    var tags: [String]

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        body = try container.decode(String.self, forKey: .body)
        footnotes = try container.decodeIfPresent(String.self, forKey: .footnotes)
        tags = (try? container.decode([String].self, forKey: .tags)) ?? []
    }
}

🦸‍♂️ 使用 Codextended

struct Article: Codable {
    var title: String
    var body: String
    var footnotes: String?
    var tags: [String]

    init(from decoder: Decoder) throws {
        title = try decoder.decode("title")
        body = try decoder.decode("body")
        footnotes = try decoder.decodeIfPresent("footnotes")
        tags = (try? decoder.decode("tags")) ?? []
    }
}

Codextended 包括基于 CodingKey 的值和字符串字面量的解码重载,因此我们可以选择最适合/方便每种特定情况的方法。

📆 使用日期格式化器

Codable 已经支持通过将 DateFormatter 分配给 JSONEncoderJSONDecoder 来支持自定义日期格式。但是,要求每个调用站点都知道每种类型使用的特定日期格式并不总是很好——因此使用 Codextended,类型本身可以轻松选择它需要使用的日期格式。

当处理第三方数据时,这非常方便,并且我们只想为我们的一些类型自定义日期格式,或者当我们想在编码值时生成更易读的日期字符串时。

🍨 使用原生 Codable

struct Bookmark: Codable {
    enum CodingKeys: CodingKey {
        case url
        case date
    }

    struct DateCodingError: Error {}

    static let dateFormatter = makeDateFormatter()

    var url: URL
    var date: Date

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        url = try container.decode(URL.self, forKey: .url)

        let dateString = try container.decode(String.self, forKey: .date)

        guard let date = Bookmark.dateFormatter.date(from: dateString) else {
            throw DateCodingError()
        }

        self.date = date
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(url, forKey: .url)

        let dateString = Bookmark.dateFormatter.string(from: date)
        try container.encode(dateString, forKey: .date)
    }
}

🦹‍♀️ 使用 Codextended

struct Bookmark: Codable {
    static let dateFormatter = makeDateFormatter()

    var url: URL
    var date: Date

    init(from decoder: Decoder) throws {
        url = try decoder.decode("url")
        date = try decoder.decode("date", using: Bookmark.dateFormatter)
    }

    func encode(to encoder: Encoder) throws {
        try encoder.encode(url, for: "url")
        try encoder.encode(date, for: "date", using: Bookmark.dateFormatter)
    }
}

同样,我们可以选择使用上面的 CodingKeys 枚举来表示我们的键,而不是使用内联字符串。

混合搭配

由于 Codextended 100% 通过扩展实现,您可以轻松地在同一个项目中将它与“原生” Codable 代码混合搭配。它也不会改变 Codable 的优点——事实上,它通常不需要任何手动代码,并且可以用作框架之间的桥梁。

它所做的只是在需要某种形式的自定义时给 Codable 一个帮助之手

安装

由于 Codextended 在单个文件中实现,因此使用它的最简单方法是简单地将其拖放到您的 Xcode 项目中。

但是,如果您希望使用依赖管理器,则可以使用 Swift Package Manager,方法是在您的 Package.swift 文件中将 Codextended 声明为依赖项。

.package(url: "https://github.com/JohnSundell/Codextended", from: "0.1.0")

有关更多信息,请参阅 Swift Package Manager 文档

您也可以使用 CocoaPods,方法是将以下行添加到您的 Podfile

pod "Codextended"

贡献 & 支持

Codextended 完全公开地开发,非常欢迎您的贡献。

在您开始在任何项目中使用 Codextended 之前,强烈建议您花几分钟时间熟悉其文档和内部实现(它全部位于 单个文件 中!),这样您就可以准备好解决可能遇到的任何问题或边缘情况。

要了解有关用于实现 Codextended 的原则的更多信息,请查看 Swift by Sundell 上的“Swift 中类型推断驱动的序列化”

本项目不提供基于 GitHub Issues 的支持,而是鼓励用户积极参与其持续开发——通过修复他们遇到的任何错误,或改进发现不足的文档。

如果您希望进行更改,请打开一个 Pull Request ——即使它只包含您计划的更改草案,或者重现问题的测试——我们可以在那里进一步讨论它。

希望您喜欢使用 Codextended! 😀