‎DataKit

DataKit 提供了一个现代、直观且声明式的接口,用于在 Swift 中读写二进制格式数据。

🏃‍♂️开始使用

作为对如何使用此库更轻松地处理二进制格式数据的介绍,首先让我向您介绍我们将要读/写的类型。 假设我们正在构建一个气象站,并且我们使用以下类型来提供有关当前测量值的更新。

struct WeatherStationFeatures: OptionSet, ReadWritable {
    var rawValue: UInt8

    static var hasTemperature = Self(rawValue: 1 << 0)
    static var hasHumidity = Self(rawValue: 1 << 1)
    static var usesMetricUnits = Self(rawValue: 1 << 2)
}

struct WeatherStationUpdate {

    var features: WeatherStationFeatures
    var temperature: Measurement<UnitTemperature>
    var humidity: Double

}

编码格式应为:

写入数据

您有两种选择将上述类型 WeatherStationUpdate 转换为数据:DataBuilderWritable 协议。 如果您打算同时读取和写入数据 - 请务必阅读“读取和写入数据”部分

DataBuilder

DataBuilder 为您提供了一个非常简单且有限的接口。借助结果构建器的强大功能,您可以简单地声明要按给定顺序写入的值,DataBuilder 将接管所有工作,以对值进行编码并将各个字节附加到以形成 Data 对象。 DataBuilder 始终期望返回一个 Data 对象,而不会抛出错误,这就是为什么此处不支持转换 - 您可能需要查看 Writable 协议!

extension WeatherStationUpdate {

    @DataBuilder var data: Data {
        UInt8(0x02)
        features
        if features.contains(.hasTemperature) {
            Float(temperature.converted(to: features.contains(.usesMetricUnits) ? .celsius : .fahrenheit).value)
        }
        if features.contains(.hasHumidity) {
            UInt8(humidity * 100)
        }
        CRC32.default
    }

}

通过此添加,您可以轻松地使用其 data 属性获取此对象的数据。

Writable

借助 keyPaths 和结果构建器的强大功能,您还可以使用 Writable 协议及其 writeFormat 属性将对象写入 Data。 只需声明单个固定值(例如,字节前缀)、带有 Writable 值的 keyPaths 或本文档“Extras”部分中进一步解释的其他构造。

extension WeatherStationUpdate: Writable {

    static var writeFormat: WriteFormat {
        Scope {
            UInt8(0x02)

            \.features

            Using(\.features) { features in
                if features.contains(.hasTemperature) {
                    let unit: UnitTemperature =
                    features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
                    Convert(\.temperature) {
                        $0.converted(to: unit).cast(Float.self)
                    }
                }
                if features.contains(.hasHumidity) {
                    Convert(\.humidity) {
                        Double($0) / 100
                    } writing: {
                        UInt8($0 * 100)
                    }
                }
            }

            CRC32.default
        }
        .endianness(.big)
    }

}

通过遵循 Writable 协议,您现在可以简单地调用其 write 函数来写出其数据

let message: WeatherStationUpdate = ...
let messageData = try message.write() // You can also inject a custom environment here, if needed

读取数据

支持将数据读取到对象中稍微复杂一些。 遵循 Readable 协议将要求您实现一个初始化程序,以从给定的 ReadContext 创建对象,并实现一个静态 readFormat 属性来描述数据的对齐方式。

ReadContext 为您提供使用 readFormat 读取的值。 确保在初始化程序和 readFormat 中使用相同的 keyPaths,以确保值的顺利读取。

extension WeatherStationUpdate: Readable {

    init(from context: ReadContext<WeatherStationUpdate>) throws {
        features = try context.read(for: \.features)
        temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin)
        humidity = try context.readIfPresent(for: \.humidity) ?? .nan
    }

    static var readFormat: ReadFormat {
        Scope {
            UInt8(0x02)

            \.features

            Using(\.features) { features in
                if features.contains(.hasTemperature) {
                    let unit: UnitTemperature =
                    features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
                    Convert(\.temperature) {
                        $0.converted(to: unit).cast(Float.self)
                    }
                }
                if features.contains(.hasHumidity) {
                    Convert(\.humidity) {
                        Double($0) / 100
                    } writing: {
                        UInt8($0 * 100)
                    }
                }
            }

            CRC32.default
        }
        .endianness(.big)
    }

}

通过实现 Readable 协议的所有这些要求,您现在可以获得另一个初始化程序 init(_: Data) throws,以从 Data 对象读取对象

let data: Data = ...
let message = try WeatherStationUpdate(data) // You can also inject a custom environment here, if needed

读取 & 写入数据

要使类型同时为 ReadableWritable,您可以使您的类型符合 ReadWritable 协议。 无需为读取和写入提供单独的格式,您可以定义一个 Format 属性,该属性用于读取和写入。 对于我们的示例类型,我们可以简单地将两个格式合并为一个,并提供一个初始化程序以从给定的 ReadContext 创建对象。

extension WeatherStationUpdate: ReadWritable {

    init(from context: ReadContext<WeatherStationUpdate>) throws {
        features = try context.read(for: \.features)
        temperature = try context.readIfPresent(for: \.temperature) ?? .init(value: .nan, unit: .kelvin)
        humidity = try context.readIfPresent(for: \.humidity) ?? .nan
    }
    
    static var format: Format {
        Scope {
            UInt8(0x02)

            \.features

            Using(\.features) { features in
                if features.contains(.hasTemperature) {
                    let unit: UnitTemperature =
                    features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
                    Convert(\.temperature) {
                        $0.converted(to: unit).cast(Float.self)
                    }
                }
                if features.contains(.hasHumidity) {
                    Convert(\.humidity) {
                        Double($0) / 100
                    } writing: {
                        UInt8($0 * 100)
                    }
                }
            }

            CRC32.default
        }
        .endianness(.big)
    }

}

万岁,您现在可以读取和写入您的对象了! 🎉

🤸‍♂️ 附加功能

读取/写入数据通常非常复杂,并且不同的格式会带来不同的挑战,以最大程度地减少有效负载、减少带宽、提高性能等。为了轻松处理不同的常见场景,DataKit 提供了一些额外的功能来处理最常见的挑战。

✏️ 转换 / 自定义 / 属性

在某些特殊情况下,您可能需要更多地控制数据的读取/写入方式。 对于这些情况,包装器 CustomConvertProperty 可能会引起您的兴趣。

💱 转换 / 可逆转换

对于某些类型,没有一个“正确”的格式(例如,考虑 Pascal 与 C 字符串),这就是为什么 DataKit 使用所谓的 Conversion 值来允许定义一次转换然后多次使用。 特别有帮助的是 ReversibleConversion 类型,它允许同时在两个方向上提供转换。

假设我们的类型有一个带有 String 值的 \.string keyPath,您可以选择使用后缀 0 字节来使用 UTF8 对字符串进行编码(类似于 C 字符串)

Convert(\.string) { // UTF8 string with a suffix 0-byte
    $0.encoded(.utf8).dynamicCount
}
.suffix(0 as UInt8)

或者,您可以使用包含字节数的 UTF8 前缀字节来编码字符串(类似于 Pascal ShortString)

Convert(\.string) { // Ascii string with a prefix count byte
    $0.encoded(.ascii).prefixCount(UInt8.self)
}

还有更多可用的转换,例如,用于在整数/浮点类型之间进行转换,从而可以轻松地直接转换为您喜欢的类型,而无需自行转换。

🤓 使用

使用 Using 构造,您可以访问 ReadContext 中的值或要写入的值。 如果数据本身中的值相互依赖或各个值的读取或写入方式,则 Using 可能非常有用。

环境

与 SwiftUI 的环境类似,您还可以使用 Environment 修改 DataKit 中各个组件的行为。 您可以在启动读取/写入过程时或在 readFormat/writeFormat/format 属性中使用修饰符来修改环境。 要访问环境值,您可能需要查看要在格式构建器之一中使用的 Environment 类型 - 或者为了更直接的访问,ReadContainerWriteContainer 都具有 environment 属性。

以下是一些您可能想要使用的修饰符

随意使用 EnvironnentKey 协议和 EnvironmentValues 结构的扩展(与 SwiftUI 非常相似)添加您自己的环境值。

🚧 范围

某些构造(例如 CRC 校验和)对整个数据而不是仅对读取/写入特定值的部分进行假设。 通过使用 Scope,您可以将该数据上下文限制为单独放置它的位置。

例如,假设我们的 WeatherStationUpdate 应该忽略校验和计算的前缀 0x02 字节。 我们可以简单地将其从范围中排除

static var format: Format {
    UInt8(0x02)
    
    Scope {
        \.features

        Using(\.features) { features in
            if features.contains(.hasTemperature) {
                let unit: UnitTemperature =
                    features.contains(.usesMetricUnits) ? .celsius : .fahrenheit
                Convert(\.temperature) {
                    $0.converted(to: unit).cast(Float.self)
                }
            }
            if features.contains(.hasHumidity) {
                Convert(\.humidity) {
                    Double($0) / 100
                } writing: {
                    UInt8($0 * 100)
                }
            }
        }

        CRC32.default
    }
    .endianness(.big)
}

通过此更改,仅将在 Scope 本身上验证 CRC,并且前缀字节将被忽略!

✅ 校验和

DataKit 的依赖项 crc-swift 提供了 CRC 校验和以及一个易于遵循的协议,用于您自己的自定义校验和值。

您可以简单地在其中一个格式构建器中指定校验和值本身。 或者,将 ChecksumProperty 与 keyPath 一起使用,以便您可以将校验和存储在属性中并从属性中编写自定义校验和。 结合 skipChecksumVerification 环境值,您还可以在以后的阶段验证校验和,例如。

🛠 安装

DataKit 目前仅支持 Swift 包管理器。

Swift 包管理器

有关如何在您的应用中采用 Swift 包的更多信息,请参见 此 WWDC 演示文稿

指定 https://github.com/QuickBirdEng/DataKit.git 作为包链接。

手动

如果您不想使用依赖项管理器,则可以通过下载源代码并将文件放置在项目目录中来手动将 DataKit 集成到您的项目中。

👤 作者

DataKit 由 QuickBird 用 ❤️ 创建。

❤️ 贡献

欢迎提出问题以寻求帮助、发现的错误或讨论新的功能请求。 乐于助人! 如果您想建议更改 DataKit,请打开一个拉取请求。

📃 许可证

DataKit 在 MIT 许可证下发布。 有关更多信息,请参见 License.md