diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85fc6ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/osx,xcode,swift +# Edit at https://www.toptal.com/developers/gitignore?templates=osx,xcode,swift + +### OSX ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +.swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### +# Xcode +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + + + + +## Gcc Patch +/*.gcno + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/osx,xcode,swift diff --git a/0.x.md b/0.x.md new file mode 100644 index 0000000..3964e15 --- /dev/null +++ b/0.x.md @@ -0,0 +1,257 @@ +--- +layout: default +title: ExCodable 0.x +description: KeyMapping Extensions for Swift Codable +--- + +ExCodable 0.x +======== + +[![ExCodable](https://iwill.im/images/ExCodable-1920x500.png)](https://github.com/ExCodable/ExCodable#readme) + +[![Swift 5.0](https://img.shields.io/badge/Swift-5.0-orange.svg)](https://swift.org/) +[![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) +[![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#) +
+[![Build and Test](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml) +[![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/ExCodable/ExCodable.svg?sort=semver)](https://github.com/ExCodable/ExCodable/releases) +[![Deploy to CocoaPods](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml) +[![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable) +
+[![LICENSE](https://img.shields.io/github/license/ExCodable/ExCodable.svg)](https://github.com/ExCodable/ExCodable/blob/master/LICENSE) +[![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) +[![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) + +[En](https://github.com/ExCodable/ExCodable) \| 中文 + +-------- + +> - [**1.x**: Recently Released](./index.md) +> - 0.x: DEPRECATED + +## [ExCodable](https://github.com/ExCodable/ExCodable) 是我在春节期间、带娃之余、用了几个晚上完成的一个 Swift 版的 JSON-Model 转换工具。 + +别走!你可能会想“嗷,又一个轮子”,但是这个真的有点不一样: +- `ExCodable` 是在 Swift `Codable` 基础上的扩展,**可以享受到诸多便利**,比如与 `NSCoding`、[Alamofire](https://github.com/Alamofire/Alamofire) 等无缝对接 —— `ExCodable` 没有做任何额外处理,躺赚那种; +> [NSKeyedArchiver.encodeEncodable(_:forKey:)](https://developer.apple.com/documentation/foundation/nskeyedarchiver/2924373-encodeencodable) +> +> [NSKeyedUnarchiver.decodeTopLevelDecodable(_:forKey:)](https://developer.apple.com/documentation/foundation/nskeyedunarchiver/2924375-decodetopleveldecodable) +> +> [An Answer from Stack Overflow](https://stackoverflow.com/a/49952202/456536) +> +> [Response Decodable Handler from Alamofire](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#response-decodable-handler) +> +- 基于 `KeyPath` 实现 Key-Mapping,无需逐个属性 Encode/Decode; +- 支持丰富的特性,差不多实现了 Objective-C 版的 [YYModel](https://github.com/ibireme/YYModel) 的所有特性; +- 轻量,1 个文件、500 行代码。 + +当然我不是一开始就决定要造轮子的。近期我们团队准备开始使用 Swift,节前开始寻找一些开源框架。网络请求用 [Alamofire](https://github.com/Alamofire/Alamofire)、自动布局用 [SnapKit](https://github.com/SnapKit/SnapKit),这都毫无悬念,但是 JSON-Model 转换并没有找到合适的。 + +## Swift 内置的 `Codable` 可以满足刚需,但也有官方框架的通病 —— 繁琐; + +最基本的使用其实还是很便利的,**建议优先选用**: + +```swift +struct TestAutoCodable: Codable { + private(set) var int: Int = 0 // `int` 一次 + private(set) var string: String? +} + +``` + +但是,一旦不得不手动 Encode/Decode 就完蛋了,相同字段要出现 5 次,而且还要夹杂很多其它代码: + +```swift +struct TestManualCodable: Codable { + + private(set) var int: Int = 0 // `int` 一次 + private(set) var string: String? + + enum Keys: CodingKey { + case int, i // `int` 两次,这里省掉,在第三次、第五次的那里直接写字符串?不要! + case nested, string + } + + init(from decoder: Decoder) throws { + if let container = try? decoder.container(keyedBy: Keys.self) { + if let int = (try? container.decodeIfPresent(Int.self, forKey: Keys.int)) // `int` 三次 + ?? (try? container.decodeIfPresent(Int.self, forKey: Keys.i)) { + self.int = int // `int` 四次 + } + if let nestedContainer = try? container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested), + let string = try? nestedContainer.decodeIfPresent(String.self, forKey: Keys.string) { + self.string = string + } + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Keys.self) + try? container.encodeIfPresent(int, forKey: Keys.int) // `int` 五次 + var nestedContainer = container.nestedContainer(keyedBy: Keys.self, forKey: Keys.nested) + try? nestedContainer.encodeIfPresent(string, forKey: Keys.string) + } +} + +``` + +## [Codextended](https://github.com/JohnSundell/Codextended) 对 `Codable` 做了大量的简化,但还是要逐个属性 Encode/Decode: + +```swift +struct TestCodextended: Codable { + + let int: Int // `int` 一次 + let string: String? + + /* Codextended 给的例子没有这个,而是在 `init` 和 `decode` + 方法里分别写两个 `"int"`、两个 `"string"`,但那是不对的,代码 + 很多时可能会改了一个忘掉另一个 */ + enum Keys: CodingKey { + case int, i // `int` 两次 + case string + } + + init(from decoder: Decoder) throws { + int = try decoder.decodeIfPresent(Keys.int) // `int` 三次 + ?? try decoder.decodeIfPresent(Keys.i) + ?? 0 + // !!!: Codextended 目前不支持 Nested-Keys + string = try decoder.decodeIfPresent(Keys.string) + } + + func encode(to encoder: Encoder) throws { + try encoder.encode(int, for: "int") // `int` 四次 + try encoder.encode(string, for: "string") + } +} + +``` + +## 另外 GitHub 上 Star 比较多的 [ObjectMapper](https://github.com/tristanhimmelman/ObjectMapper)、[HandyJSON](https://github.com/alibaba/HandyJSON)、[KakaJSON](https://github.com/kakaopensource/KakaJSON) 等: + +他们都各自构建了整套的序列化、反序列化机制,各有所长,但是相比 Objective-C 的 [YYModel](https://github.com/ibireme/YYModel) 多少都欠了点意思。与 `Codable` 不兼容、代码很复杂不说,甚至还有直接读写内存的 —— “依赖于从 Swift Runtime 源码中推断的内存规则,任何变动我们将随时跟进”,这就不大靠谱了,至少不够优雅。 + +## ExCodable: + +调研一番之后倾向于用 Codextended,起初有考虑直接基于它做扩展来实现 Key-Mapping,但是受到限制较多,只能自己动手了。 + +Codextended 最欠缺的是 Key-Mapping,经过各种摸索、尝试,确定 `KeyPath` 方式可行。解决掉关键问题后面就简单了,很快差不多实现了 YYModel 的所有特性;同时借鉴了 Codextended,重新写了关键部分的实现,有些调整、也有些舍弃。 + +于是便有了 `ExCodable`: +- 使用 `KeyPath` 映射到 Coding-Key,实现 Key-Mapping; +- 支持多个候选 Coding-Key; +- 支持 Coding-Key 嵌套; +- 支持使用 Subscript 进行自定义 Encode/Decode; +- 支持数据类型自动转换以及自定义转换; +- 支持 `struct`、`class`、subclass; +- 支持 JSON、PList 以及自定义 Encoder/Decoder,默认使用 JSON; +- 使用类型推断,支持 `Data`、`String`、`Array`、`Object` 类型 JSON 数据; +- 使用 `Optional` 类型取代抛出没什么用的 `error`,避免到处写 `try?`,有时还要套上括号 —— 现在也支持抛出异常了 [可选]。 + +> 这里要多说两句:一般情况下抛出错误是有用的,但是在 JSON-Model 转换的场景略有不同。经常遇到的错误无非就是字段少了、类型错了。如果是关键数据有问题抛出错误也还好,但是有时不痛不痒的字段出错(这种更容易出错),导致整个解析都失败就不好了。确实这样可以及时发现返回结果中的问题,但是大家可能也知道经常有新“发现”是什么样的体验。老司机可以回忆一下 YYModel 出现之前的岁月。所以,永远不要相信关于 API 的任何承诺,不管它返回什么,App 不要动不动就死给人看,这会严重影响一个开发者的名声!可能有人会问,它真的给你返回一坨🍦怎么办?可以加个关键数据校验环节,只校验关键数据,而不是依赖异常。 + +> 为了满足不同的编程习惯,`ExCodable` - 0.5.0 版本开始支持了个别/全部字段是否非空 - `nonnull`(Encode/Decode 时是否使用带有 `IfPresent` 的方法)、以及遇到异常时是否抛出 - `throws`。这两个参数都是 `Bool` 类型,组合使用可以产生不同的效果。比如某内嵌的对象指定某字段 `nonnull = true`、`throws = false`,遇到非空字段无法解析会导致该字段所属对象为 `nil`,但如果它外层对象没有指定该对象 `nonnull = true`,则会继续解析其它字段,而不是完全终止解析。 + +## 上面场景,用 `ExCodable` 就简单多了: + +```swift +struct TestExCodable: ExCodable, Equatable { + + private(set) var int: Int = 0 // `int` 一次 + private(set) var string: String? + + static var keyMapping: [KeyMap] = [ + KeyMap(\.int, to: "int", "i"), // `int` 两次 + KeyMap(\.string, to: "nested.string") + ] + + init(from decoder: Decoder) throws { + decode(from: decoder, with: Self.keyMapping) + } +} + +``` + +## `ExCodable` 用法解析: + +定义 `struct`,使用 `var` 声明变量、并设置默认值,可以使用 `private(set)` 来防止属性被外部修改; + +> `Optional` 类型不需要默认值; +> 想用 `let` 也不是不可以,参考 [Usage](https://github.com/ExCodable/ExCodable#usage); + +```swift +struct TestStruct: Equatable { + private(set) var int: Int = 0 + private(set) var string: String? +} + +``` + +实现 `ExCodable` 协议,通过 `keyMapping` 设置 `KeyPath` 到 Coding-Key 的映射,`init` 和 `encode` 方法都只要一行代码; + +> 当 `encode` 方法只需这一行代码时它也是可以省略的,`ExCodable` 提供了默认实现。但是受 Swift 对初始化过程的严格限制,`init` 方法不能省略。 + +```swift +extension TestStruct: ExCodable { + static var keyMapping: [KeyMap] = [ + KeyMap(\.int, to: "int", "i"), + KeyMap(\.string, to: "nested.string") + ] + init(from decoder: Decoder) throws { + decode(from: decoder, with: Self.keyMapping) + // 特殊逻辑同样可以在这里手动解决 + // 比如 `string = (int == 0) ? decoder["a"] : decoder["b"]` + } + // func encode(to encoder: Encoder) throws { + // encode(to: encoder, with: Self.keyMapping) + // // 特殊逻辑同样可以在这里手动解决 + // // 比如 `encoder["string"] = (string == "") ? nil : string` + // } +} + +``` + +序列化、反序列化也很方便; + +```swift +let test = TestStruct(int: 100, string: "Continue") +let data = test.encoded() as Data? // Model encode to JSON + +let copy1 = data?.decoded() as TestStruct? // decode JSON to Model +let copy2 = TestStruct.decoded(from: data) // or Model decode from JSON + +XCTAssertEqual(copy1, test) +XCTAssertEqual(copy2, test) + +``` + +更多示例可参考 [Usage](https://github.com/ExCodable/ExCodable#usage) 以及单元测试代码。 + +## 将下面代码片段添加到 Xcode,只要记住 `ExCodable` 就可以了: + +> Language: Swift +> Platform: All +> Completion: ExCodable +> Availability: Top Level + +```swift +<#extension/struct/class#> <#Type#>: ExCodable { + static var <#keyMapping#>: [KeyMap<<#SelfType#>>] = [ + KeyMap(\.<#property#>, to: <#"key"#>), + <#...#> + ] + init(from decoder: Decoder) throws { + decode<#Reference#>(from: decoder, with: Self.<#keyMapping#>) + } + func encode(to encoder: Encoder) throws { + encode(to: encoder, with: Self.<#keyMapping#>) + } +} + +``` + +## 感谢 + +在此,要特别感谢 John Sundell 的 [Codextended](https://github.com/JohnSundell/Codextended) 的非凡创意、以及 ibireme 的 [YYModel](https://github.com/ibireme/YYModel) 的丰富特性,他们给了我极大的启发。 + +`ExCodable` 还是一个崭新的框架,使用中遇到任何问题欢迎反馈:[i+ExCodable@iwill.im](mailto:i+ExCodable@iwill.im)。 diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..5e0cc3d --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +excodable.iwill.im \ No newline at end of file diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/index.md b/index.md new file mode 100644 index 0000000..3783751 --- /dev/null +++ b/index.md @@ -0,0 +1,431 @@ +--- +layout: default +title: ExCodable +--- + +ExCodable 1.0 +======== + +[![ExCodable](https://iwill.im/images/ExCodable-1.x-1920x500.png)](https://github.com/ExCodable/ExCodable#readme) + +[![Swift 5.10](https://img.shields.io/badge/Swift-5.10-orange.svg)](https://swift.org/) +[![Swift Package Manager](https://img.shields.io/badge/spm-compatible-brightgreen.svg?style=flat)](https://swift.org/package-manager/) +[![Platforms](https://img.shields.io/cocoapods/p/ExCodable.svg)](#) +
+[![Build and Test](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/build-and-test.yml) +[![GitHub Releases (latest SemVer)](https://img.shields.io/github/v/release/ExCodable/ExCodable.svg?sort=semver)](https://github.com/ExCodable/ExCodable/releases) +[![Deploy to CocoaPods](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml/badge.svg)](https://github.com/ExCodable/ExCodable/actions/workflows/deploy_to_cocoapods.yml) +[![Cocoapods](https://img.shields.io/cocoapods/v/ExCodable.svg)](https://cocoapods.org/pods/ExCodable) +
+[![LICENSE](https://img.shields.io/github/license/ExCodable/ExCodable.svg)](https://github.com/ExCodable/ExCodable/blob/master/LICENSE) +[![GitHub stars](https://badgen.net/github/stars/ExCodable/ExCodable)](https://github.com/ExCodable/ExCodable/stargazers/) +[![@minglq](https://img.shields.io/twitter/url?url=https%3A%2F%2Fgithub.com%2Fiwill%2FExCodable)](https://twitter.com/minglq) + +[En](https://github.com/ExCodable/ExCodable) \| 中文 + +-------- + +ExCodable 是一个 Swift 版 JSON-Model 转换工具,现在迎来重要升级,发布 1.0 版本。 + +「若非必要,勿造轮子」。但显然,我又造了一个,所以它一定是必要的: + +- Swift 内置的 `Codable` 能用,但也有官方框架的通病 —— 繁琐 +- [Codextended](https://github.com/JohnSundell/Codextended) 想法特别好,对 `Codable` 做了大量的简化,但还是要逐个属性 encode/decode +- 其它 star 较多的 [ObjectMapper](https://github.com/tristanhimmelman/ObjectMapper)、[HandyJSON](https://github.com/alibaba/HandyJSON)、[KakaJSON](https://github.com/kakaopensource/KakaJSON) 等等(不知道最近两年有没有新的),都不兼容 `Codable`,语法奇怪、繁琐,甚至还有读写内存的,说实话 —— 丑得要死! + +ObjC 时代,最好的 JSON-Model 转换非 [YYModel](https://github.com/ibireme/YYModel) 莫属,可惜没有 Swift 版,所以 [自己写一个吧](./0.x.md/#excodable-是我在春节期间带娃之余用了几个晚上完成的一个-swift-版的-json-model-转换工具)。 + +## 主要特性 + +```swift +struct TestExCodable: ExAutoCodable { + @ExCodable + var int: Int = 0 + @ExCodable("string", "s", "nested.nested.string") + var string: String? = nil +} + +``` + +上面代码虽少,但足可以体现 ExCodable 的强大。 + +主要特性: + +- ExCodable 是对 Swift 内置的 `Codable` 的扩展,因此可以享受到诸多便利,比如与 `NSCoding`、[SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)、[GenericJSON](https://github.com/iwill/generic-json-swift)、[Alamofire](https://github.com/Alamofire/Alamofire) 等都能无缝对接 +- 在属性上添加注解(`@propertyWrapper`)绑定 JSON key ,JSON key 与属性同名时,可以简写为 `@ExCodable` —— 目前位置最简单、直观的方式 +- 支持多个候选 JSON key,依次解析 +- 使用 `.` 拼接多层嵌套的 JSON key +- 需要手动 encode/decode 时,可以使用便捷的 subscript 语法 +- 支持常见的数据类型转换,以及灵活的自定义转换 +- 灵活的异常处理,出错时默认返回 `nil`,也支持设置抛出 `error` +- 支持 `struct`、`enum`、`class` 以及子类 +- 支持 `Data`、`String`、`Array`、`Dictionary` 等 JSON 格式 +- 支持类型推断 +- 声明实现 `ExAutoCodable` 协议,即可自动获得 `Codable` 方法的默认实现,无需对属性逐个 encode/decode +- 支持 JSON、PList 以及自定义 encoder/decoder,默认使用 JSON +- 非常轻量 + +## 使用方法 + +### 1、ExCodable + +目前,使用 ExCodable 定义属性需要使用 `var`(可以使用 `private(set)` 避免属性被意外修改),并且要提供默认值。 + +```swift +struct TestStruct: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("string") private(set) + var string: String? = nil +} + +``` + +### 2、多个候选 JSON key + +```swift +struct TestAlternativeKeys: ExAutoCodable { + @ExCodable("string", "str", "s") private(set) + var string: String! = nil +} + +``` + +### 3、多层嵌套的 JSON key + +```swift +struct TestNestedKeys: ExAutoCodable { + @ExCodable("nested.nested.string") private(set) + var string: String! = nil +} + +``` + +### 4、`RawRepresentable` 类型的 `enum` + +```swift +enum TestEnum: Int, Codable { + case zero = 0, one = 1 +} + +struct TestStructWithEnum: ExAutoCodable { + @ExCodable private(set) + var `enum` = TestEnum.zero +} + +``` + +非 `RawRepresentable` 类型的 `enum` 需要自定义的 encode/decode。 + +### 5、自定义 Encode/Decode + +`@ExCodable` 支持自定义 `encode` 和 `decode`,并且 `encoder`、`decoder` 支持使用 subscript 方式访问。 + +```swift +struct TestManualEncodeDecode: ExAutoCodable { + @ExCodable(encode: { encoder, value in + encoder["int"] = value <= 0 ? 0 : value + }, decode: { decoder in + if let int: Int = decoder["int"], int > 0 { + return int + } + return 0 + }) private(set) + var int: Int = 0 +} + +``` + +### 6、自动类型转换 + +ExCodable 支持灵活的类型转换,兼容多层嵌套的 `Optional` 类型,例如 `Int???`。并且这些转换同样适用于 `RawRepresentable` 类型的属性,自动转换到 `RawValue` 类型并调用 `init(rawValue:)`。 + +A、内置支持的类型转换: + +- boolean: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `String` +- integer: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` +- float: + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` +- string: + - `Bool` + - `Int`, `Int8`, `Int16`, `Int32`, `Int64` + - `UInt`, `UInt8`, `UInt16`, `UInt32`, `UInt64` + - `Double`, `Float` + - `String` + +B、单个属性的自定义类型转换: + +```swift +struct TestCustomEncodeDecode: ExAutoCodable { + @ExCodable(decode: { decoder in + if let string: String = decoder["string"] { + return string.count + } + return 0 + }) private(set) + var int: Int = 0 +} + +``` + +C、model 中的自定义类型转换: + +```swift +struct TestCustomTypeConverter: ExAutoCodable { + @ExCodable private(set) + var doubleFromBool: Double? = nil + @ExCodable private(set) + var floatFromBool: Double? = nil +} + +extension TestCustomTypeConverter: ExCodableDecodingTypeConverter { + + public static func decode(_ container: KeyedDecodingContainer, codingKey: K, as type: T.Type) -> T? { + + // for nested optionals, e.g. `var int: Int??? = nil` + let wrappedType = T?.wrappedType + + // decode Double from Bool + if type is Double.Type || wrappedType is Double.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + // decode Float from Bool + else if type is Float.Type || wrappedType is Float.Type { + if let bool = try? container.decodeIfPresent(Bool.self, forKey: codingKey) { + return (bool ? 1.0 : 0.0) as? T + } + } + + return nil + } +} + +``` + +D、全局的自定义类型转换: + +```swift +struct TestCustomGlobalTypeConverter: ExAutoCodable, Equatable { + @ExCodable private(set) + var boolFromDouble: Bool? = nil +} + +extension ExCodableGlobalDecodingTypeConverter: ExCodableDecodingTypeConverter { + + public static func decode(_ container: KeyedDecodingContainer<_K>, codingKey: _K, as type: T.Type) -> T? { + + // for nested optionals, e.g. `var int: Int??? = nil` + let wrappedType = T?.wrappedType + + // decode Bool from Double + if type is Bool.Type || wrappedType is Bool.Type { + if let double = try? container.decodeIfPresent(Double.self, forKey: codingKey) { + return (double != 0) as? T + } + } + + return nil + } +} + +``` + +### 7、使用 Subscript 手动 Encode/Decode + +对于一个未使用 `ExCodable` 的类型: + +```swift +struct TestManualEncodingDecoding { + let int: Int + let string: String +} + +``` + +使用 subscript 手动 encode/decode,要比 Swift 原生语法简单得多: + +```swift +extension TestManualEncodingDecoding: Codable { + + enum Keys: CodingKey { + case int, string + } + + init(from decoder: Decoder) throws { + int = decoder[Keys.int] ?? 0 + string = decoder[Keys.string] ?? "" + } + func encode(to encoder: Encoder) throws { + encoder[Keys.int] = int + encoder[Keys.string] = string + } +} + +``` + +### 8、更灵活的异常处理 + +ExCodable 默认忽略 JSON-Model 转换时遇到的 `EncodingError.invalidValue`、`DecodingError.keyNotFound`、`DecodingError.valueNotFound` 和 `DecodingError.typeMismatch` 等错误,出错的属性不处理 —— encode 时跳过、decode 时保持默认值。只有 JSON 数据本身有问题时才会抛出错误。 + +ExCodable 也支持出错时终止转换,抛出错误: + +- 设置 `nonnull: true` 允许抛出 `EncodingError.invalidValue`、`DecodingError.keyNotFound` 和 `DecodingError.valueNotFound` +- 设置 `throws: true` 允许抛出 `DecodingError.typeMismatch` + +```swift +struct TestNonnullAndThrows: ExAutoCodable { + @ExCodable(nonnull: true, throws: true) private(set) + var int: Int! = 0 +} + +``` + +### 9、`class` 以及子类 + +```swift +class TestClass: ExAutoCodable { + + @ExCodable private(set) + var int: Int = 0 + @ExCodable private(set) + var string: String? = nil + + required init() {} + init(int: Int, string: String?) { + (self.int, self.string) = (int, string) + } +} + +``` + +```swift +class TestSubclass: TestClass { + + @ExCodable private(set) + var bool: Bool = false + + required init() { super.init() } + required init(int: Int, string: String, bool: Bool) { + self.bool = bool + super.init(int: int, string: string) + } +} + +``` + +### 10、类型推断 + +```swift +struct TestStruct: ExAutoCodable, Equatable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable private(set) + var string: String? = nil +} + +// 正常的转换 +let json = Data(#"{"int":200,"string":"OK"}"#.utf8) +let model = try? TestStruct.decoded(from: json) + +// 类型推断 +let dict = try? model.encoded() as [String: Any] +let copy = try? dict.decoded() as TestStruct + +``` + +> 更多用法参考代码中的单元测试。 + +## 安装 + +Swift Package Manager: + +```swift +.package(url: "https://github.com/ExCodable/ExCodable", from: "1.0.0") +``` + +CocoaPods: + +```ruby +pod 'ExCodable', '~> 1.0.0' +``` + +## 升级 + +如果你用过 0.x 版本,感谢支持!但是时候升级了,ExCodable 依然保留了旧的 API,从而降低升级的难度。 + +首先,升级到 1.0 之后可以继续使用废弃的 API —— 快速、工作量小: + +- 全局搜索 `ExCodable`,替换成 `ExCodableDEPRECATED` +- 如果你实现了 `KeyedDecodingContainerCustomTypeConversion` 的 `decodeForTypeConversion(_:codingKey:as:)` 方法,在前面添加一个 `static` + +```swift +struct TestExCodable { + private(set) var int: Int = 0 + private(set) var string: String? +} + +extension TestExCodable: ExCodableDEPRECATED { + static let keyMapping: [KeyMap] = [ + KeyMap(\.int, to: "int"), + KeyMap(\.string, to: "nested.nested.string", "string", "str", "s") + ] + init(from decoder: Decoder) throws { + try decode(from: decoder, with: Self.keyMapping) + } +} + +``` + +然后逐步升级到新的语法: + +- 使用 `ExAutoCodable` 协议替代 `ExCodable` +- 删除 `init(from decoder: Decoder) throws` 方法 +- 删除 `keyMapping` 静态属性 +- 改用 `@ExCodable("", "", ...)` 绑定 JSON key +- 具体参考上面 [使用方法](#使用方法) + +```swift +struct TestExCodable: ExAutoCodable { + @ExCodable private(set) + var int: Int = 0 + @ExCodable("nested.nested.string", "string", "str", "s") private(set) + var string: String? +} + +``` + +## 未来 + +Swift 5.9 发布时引入了 [Macros](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/),很快我就看到了基于它实现的 [MetaCodable](https://github.com/SwiftyLab/MetaCodable),这是目前最科学的「实现方式」。这让我一度想放弃维护 ExCodable,但我还是更喜欢 ExCodable 灵活的[「使用方式」](#使用方法)。 + +计划未来也使用 Macros 实现重写 ExCodable,在保持目前良好特性的同时,突破 Swift 语法对目前方案的种种限制,敬请期待 —— 不确定多久 🫣 + +## 星星 + +如果你喜欢 ExCodable,欢迎 [给个星星](https://github.com/ExCodable/ExCodable#repository-container-header) ⭐️ 🤩 + +## 致敬 + +在此,再次,致敬 John Sundell 和 ibireme,[Codextended](https://github.com/JohnSundell/Codextended) 的非凡创意和 [YYModel](https://github.com/ibireme/YYModel) 的丰富特性给了我的极大的启发! + +## 关于 + +我是 [Míng](https://github.com/iwill),使用中遇到任何问题,欢迎 [反馈](https://github.com/ExCodable/ExCodable/issues) / [i+ExCodable@iwill.im](mailto:i+ExCodable@iwill.im)。 + +## 开源 + +[代码](https://github.com/ExCodable/ExCodable) 在 [MIT](https://github.com/ExCodable/ExCodable/blob/master/LICENSE) 协议下开源。