Mappable

Swift Swift Package Manager Build Status Codecov branch

Mappable 是一个轻量级、灵活、易于使用的框架,用于将 JSON 转换为模型,特别针对不可变属性的初始化进行了优化。

struct Flight: Mappable {
    let number: String
    let time: Date
    
    init(map: Mapper) throws {

        // with the help of @dynamicMemberLookup feature
        number = try map.id()
        time   = try map.time()

        // or use the old way
        // number = try map.from("id")
        // time   = try map.from("time")
    }
}
// Flight(JSONString: json)

同时提供了一个 Xcode 插件,用于自动生成实现代码。

特性

为什么选择 Mappable?

大多数 JSON 转换为模型的库不能很好地处理不可变属性的初始化。它们需要使用 `var` 和可空类型来声明属性,这违反了 Swift 的精神并导致代码质量下降。 Mappable 诞生就是为了解决这个问题。

优点 缺点
Codable - Swift 原生支持
- 自动转换(无需指定映射关系)
- 支持双向转换
- 不够灵活
- 不支持继承的类
HandyJSON - 自动转换(无需指定映射关系)
- 双向转换
不支持不可变属性
ObjectMapper 双向转换 - 对不可变属性的支持较弱*
- 多种模式导致混乱
- 缺少对某些类型组合的支持。
SwiftyJSON 不是一个 JSON 对象转换器。
它只是一个处理 JSON 数据的便捷工具。

* 1) 不能方便地处理可选类型。 2) 不支持兼容的类型转换,这会导致 JSON 中哪怕是很小的格式错误都会导致整个对象转换失败。

Mappable 很大程度上受到了 ObjectMapper 的启发。您可以将 Mappable 视为 ObjectMapper 中 `ImmutableMappable` 的改进版本。

用法

基础知识

为了支持映射,一个类型应该实现 `Mappable` 协议,该协议只有一个初始化方法

class Country: Mappable {
    let name: String
    let cities: [City]   // struct City: Mappable { ... }
    let atContinent: Continent // enum Continent: Mappable { ... }
    
    required init(map: Mapper) throws {
        name        = try map.from("name")
        cities      = try map.from("city")
        atContinent = try map.from("location.continent")
    }
}

您只需要编写映射关系:属性的键路径。虽然这些行只是普通的赋值语句,但不需要指定类型,因此您可以将这些行视为映射关系的特殊表示形式。(您可以将该行理解为“尝试从 XXX 映射(值))😆 )

然后你可以这样初始化一个对象

// NOTE: these initializer throw errors, you should do error handling
let c = try Country(JSON: jsonDict)
let d = try? Country(JSONString: jsonString)

支持的类型

默认值

// just use `??`
cities = try map.from("city") ?? []

可选类型处理

即使 JSON 中没有对应的数据或数据格式错误,`Optional` 类型也不会抛出错误。在这种情况下,将分配一个 `nil` 值。

如果您将属性声明为可选类型,可能意味着 JSON 中并非严格要求此数据。因此,您希望在实际没有数据时获得一个 nil 值。

struct User: Mappable {
    let ID: String
    let summary: String?
    
    init(map: Mapper) throws {
        ID      = try map.from("id")
        summary = try map.from("summary")
    }
}
let json = ["id": "a123"]
let user = try! User(JSONObject: json) // It won't crash.

兼容的类型转换

从以下类型转换
Int, Double, Float, CGFloat String
Bool Int, "true", "True", "TRUE", "YES", "false", "False", "FALSE", "NO", "0", "1"
String Int, NSNumber
URL String
Date Double(secondsSince1970), String (RFC 3339, e.g. 2016-06-13T16:00:00+00:00)

更多详情请参见此处

自定义转换

初始化器中的内容只是简单的赋值,因此您可以对数据进行任何操作。使用 `map.getRootValue()` 和 `map.getValue(keyPath:)` 获取原始 JSON 值并执行您想要的操作。

为了方便日期转换,`Mapper` 中还有一个 `options` 属性可以设置自定义的日期策略。(更复杂的例子请参见此处

枚举

符合 `RawRepresentable` 的枚举具有 `Mappable` 的默认实现。您只需将 `Mappable` 的符合性声明为您的枚举类型,它就可以工作了。

对于具有关联值的枚举,您可以手动实现

enum EnumWithValues: Mappable {
    case a(Int)
    case b(String)
    
    init(map: Mapper) throws {
        // It could be initialized with a json date like:
        // {"type": "a", value: 123}
        let value = try map.getValue("type", as: String.self)
        switch value {
        case "a":
            self = .a(try map.from("value"))
        case "b":
            self = .b(try map.from("value"))
        default:
            throw ErrorType.JSONStructureNotMatchDesired(value, "string a/b")
        }
    }
}

类继承

class ChildModel: BaseModel {
    let b : Int
    required init(map: Mapper) throws {
        b = try map.from("b")
        try super.init(map: map)
    }
}

嵌套键路径

使用键路径 "AAA.BBB" 来映射 JSON 中的多级路径值

// let json = """ {"AAA": {"BBB": 1}} """
b = try map.from("AAA.BBB")

// use `n` to get a n-th value in array
// let json = """ {"AAA": [11,22,33]} """
b = try map.from("AAA.`2`") // b = 33

如果一个普通键自然包含 `.`,你可以使用像 `map.from("a.file", keyPathIsNested: false)` 这样的方法,将该键视为单级路径。

安装

Cocoapods

pod 'Mappable'

Swift Package Manager

.Package(url: "https://github.com/leavez/Mappable.git", from: "1.5.0"),

Carthage

github "leavez/Mappable"

许可证

Mappable 在 MIT 许可下可用。有关更多信息,请参见 LICENSE 文件。