HappyCodable
中文介绍
一个更快乐的 Codable 框架,使用 SourceKittenFramework 自动生成与 Codable 相关的代码。
有什么问题吗?
- 不支持对单个编码键进行更改,一旦更改一个编码键,就需要设置所有编码键。
- 不支持忽略特定的编码键。
- 不支持非 RawRepresentable 枚举的自动合成,即使每个元素都是可编码的。
- 不支持在解码时将多个键映射到一个属性。
- 调试困难。
- 在 json 数据中缺少相应的键时,无法自动使用定义中的默认值。
- 不支持自动映射类型,如将 0/1 转换为 false/true。
使用 HappyCodable 可以解决所有这些问题。
安装
这似乎有些麻烦,但我可以保证这是值得的。
您需要将这些添加到您的项目中
- HappyCodable - 提供您所需的协议和功能
- HappyCodable/CommandLine,基于 SourceKitten,用于生成所需的 Codable 代码
准备
创建 HappyCodable/CommandLine 的主机命令行,这里命名为 HappyCodableCommandLine,并选择 Swift 作为语言
替换 HappyCodableCommandLine 由 Xcode 创建的 main.swift
import Foundation
import HappyCodable
let path: String = CommandLine.arguments[1]
let createdFilePath: String = CommandLine.arguments[2]
main(path: path, createdFilePath: createdFilePath)
dispatchMain()
在 Xcode 中打开您的项目目标,切换到“构建阶段”选项卡
打开依赖项,然后将其添加到主目标。
在文件列表中单击您的项目,选择您的目标下的“TARGETS”,点击“构建阶段”选项卡,单击左侧上角的小加号创建新的运行脚本书签,将其拖放到编译源代码阶段上方和 Check Pods Manifest.lock 下方,展开它并粘贴以下脚本
# The finish complied HappyCodableCommandLine path, no need to change
commandLine="${SYMROOT}/${CONFIGURATION}/HappyCodableCommandLine"
# The scan path, ${SRCROOT}/${PRODUCT_NAME} mean scan the whole project by default, change if you need
scanPath="${SRCROOT}/${PRODUCT_NAME}"
# The save path of grenerated code, change if you need
generatedPath="${SRCROOT}/HappyCodable.generated.swift"
echo "${commandLine} ${scanPath} ${generatedPath}"
${commandLine} ${scanPath} ${generatedPath}
CocoaPods
- 在 Podfile 的主目标中添加 `pod 'HappyCodable'
- 在 Podfile 的 HappyCodableCommandLine 目标中添加 `pod 'HappyCodable/CommandLine'
完成之后
target 'HappyCodableDemo' do
pod 'HappyCodable'
end
target 'HappyCodableCommandLine' do
pod 'HappyCodable/CommandLine'
end
- 运行 pod install
在你的项目中使用
-
首次构建可能会花费一些时间,因为需要编译 HappyCodableCommandLine,完成后,应在您的选择路径中看到
HappyCodable.generated.swift
-
将
HappyCodable.generated.swift
文件拖入您的项目,如果需要,取消选中复制项提示:将 *.generated.swift 模式添加到您的 .gitignore 文件中,以防止不必要的冲突。
-
将
HappyCodable
添加到类似 struct/class/enum 的结构中
import HappyCodable
struct Person: HappyCodable {
var name: String = "abc"
@Happy.codingKeys("🆔")
var id: String = "abc"
@Happy.codingKeys("secret_number", "age") // the first key will be the coding key
var age: Int = 18
@Happy.uncoding
var secret_number: String = "3.1415" // Build fail if coded, in this case, we can "uncoding" it.
}
HappyCodableCommandLine 将自动创建代码
extension Person {
enum CodingKeys: String, CodingKey {
case name
case id = "🆔"
case age = "secret_number"
}
mutating
func decode(happyFrom decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringCodingKey.self)
var errors = [Error]()
do { self.name = try container.decode(key: "name") } catch { errors.append(error) }
do { self.id = try container.decode(key: "🆔") } catch { errors.append(error) }
do { self.age = try container.decode(key: "secret_number", alterKeys: { ["age"] }) } catch { errors.append(error) }
if !Self.allowHappyDecodableSkipMissing, !errors.isEmpty {
throw errors
}
}
func encode(happyTo encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
var errors = [Error]()
do { try container.encodeIfPresent(self.name, forKey: .name) } catch { errors.append(error) }
do { try container.encodeIfPresent(self.id, forKey: .id) } catch { errors.append(error) }
do { try container.encodeIfPresent(self.age, forKey: .age) } catch { errors.append(error) }
if !Self.allowHappyEncodableSafely, !errors.isEmpty {
throw errors
}
}
}
也支持非 RawRepresentable Enum(您需要先确保参数类型是 Codable)
import HappyCodable
enum EnumTest: HappyCodableEnum {
case value(num: Int, name: String)
// case call(() -> Void) // Build fails if added, since (() -> Void) can't be codable
case name0(String)
case name1(String, last: String)
case name2(first: String, String)
case name3(_ first: String, _ last: String)
}
生成代码
extension EnumTest {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let content = try container.decode([String: [String: String]].self)
let error = DecodingError.typeMismatch(EnumTest.self, DecodingError.Context(codingPath: [], debugDescription: ""))
guard let name = content.keys.first else {
throw error
}
let decoder = JSONDecoder()
switch name {
case ".value(num:name:)":
guard
let _0 = content[name]?["num"]?.data(using: .utf8),
let _1 = content[name]?["name"]?.data(using: .utf8)
else {
throw error
}
self = .value(
num: try decoder.decode((Int).self, from: _0),
name: try decoder.decode((String).self, from: _1)
)
case ".name0(_:)":
guard
let _0 = content[name]?["$0"]?.data(using: .utf8)
else {
throw error
}
self = .name0(
try decoder.decode((String).self, from: _0)
)
case ".name1(_:last:)":
guard
let _0 = content[name]?["$0"]?.data(using: .utf8),
let _1 = content[name]?["last"]?.data(using: .utf8)
else {
throw error
}
self = .name1(
try decoder.decode((String).self, from: _0),
last: try decoder.decode((String).self, from: _1)
)
case ".name2(first:_:)":
guard
let _0 = content[name]?["first"]?.data(using: .utf8),
let _1 = content[name]?["$1"]?.data(using: .utf8)
else {
throw error
}
self = .name2(
first: try decoder.decode((String).self, from: _0),
try decoder.decode((String).self, from: _1)
)
case ".name3(_:_:)":
guard
let _0 = content[name]?["first"]?.data(using: .utf8),
let _1 = content[name]?["last"]?.data(using: .utf8)
else {
throw error
}
self = .name3(
try decoder.decode((String).self, from: _0),
try decoder.decode((String).self, from: _1)
)
default:
throw error
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let encoder = JSONEncoder()
switch self {
case let .value(_0, _1):
try container.encode([
".value(num:name:)": [
"num": String(data: try encoder.encode(_0), encoding: .utf8),
"name": String(data: try encoder.encode(_1), encoding: .utf8)
]
])
case let .name0(_0):
try container.encode([
".name0(_:)": [
"$0": String(data: try encoder.encode(_0), encoding: .utf8)
]
])
case let .name1(_0, _1):
try container.encode([
".name1(_:last:)": [
"$0": String(data: try encoder.encode(_0), encoding: .utf8),
"last": String(data: try encoder.encode(_1), encoding: .utf8)
]
])
case let .name2(_0, _1):
try container.encode([
".name2(first:_:)": [
"first": String(data: try encoder.encode(_0), encoding: .utf8),
"$1": String(data: try encoder.encode(_1), encoding: .utf8)
]
])
case let .name3(_0, _1):
try container.encode([
".name3(_:_:)": [
"first": String(data: try encoder.encode(_0), encoding: .utf8),
"last": String(data: try encoder.encode(_1), encoding: .utf8)
]
])
}
}
}
限制
-
由于HappyCodable使用扩展文件来生成Codable的函数,因此它不能用于私有模型类型,也不能用于在函数中定义的模型。
func getNumber() { struct Package: Codable { let result: Int } }
-
HappyCodable需要一个
init()
来为模型创建一个默认对象(HappyCodableEnum不需要),然后使用Codable更改属性,因此它要求编码属性可变。因此,它不能用于只读模型,例如,上面的Package结构体。
问答
-
为什么要创建HappyCodable,为什么不用HandyJSON呢?
我的项目之前使用的是HandyJSON,但由于它是基于Swift的低级数据结构,并且Swift结构变动后,HandyJSON出现了问题,所以我写了HappyCodable。
也许有人会说:更新HandyJSON就可以了,但你不能保证在Swift数据结构再次改变后HandyJSON不会停止更新,或者你的APP不会突然停止开发,然后你的用户在更新iOS后不能再用它了,对吧?
对于迁移到HappyCodable,API主要参考HandyJSON,我实际上可以写一份关于此的指南(可能只有100字左右)。
-
我的项目使用基于Codable的另一个框架(如WCDB.swift),这是否可行?
我测试了WCDB.swift,如果你手动实现了CodingKeys,HappyCodable将不会生成CodingKeys。你还可以在HappyCodable完成后扩展模型的CodingKeys以实现WCDB.swift的协议;这要简单得多。
extension LevelInfo.CodingKeys: WCDBSwift.CodingTableKey { typealias Root = LevelInfo static let objectRelationalMapping = TableBinding(Self.self) static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? { return [ "MultiPrimaryConstraint": MultiPrimaryBinding(indexesBy: [subjectId, id]) ] } }
-
您的框架存在许多限制。您可以选择使用私有属性,同时也需要使属性可变
因为Swift自己生成的代码在编译时直接插入定义中,然后如果其他基于Codable的库的协议在同一文件中编写,Swift要求您在定义中实现Codable的方法。然后HappyCodable在扩展中实现init(from decoder: Decoder),Swift将不会在模型扩展中使用init(from decoder: Decoder)...简而言之,经过多次测试,我最终选择了这个麻烦的解决方案,在init(from decoder: Decoder)中调用另一个方法来分配属性值,因此属性必须可变,对于像WCDB.swift这样需要数据映射的情况,属性也必须可变