Swift开源项目ObjectMapper实践

近期项目打算全面向swift迁移,虽然两三年前有写过swift项目但是很长时间没有开发很多知识点已经模糊,最近打算就热门的几个第三方库的使用方法进行一个调研

今天就先从ObjectMapper入手,ObjectMapper是一个由swift写的json和模型转换的开源库,目前已经有5950个star

先从官方文档入手,进行一个简单的介绍

支持的功能
  • JSON向模型的转换
  • 模型向JSON的转换
  • 嵌套结构的解析
  • mapping时的自定义转换
  • 结构体的支持
基础用法

ObjectMapper中定义了一个协议Mappable

Mappable协议中声明了两个方法

mutation func mapping(map: Map)

init?(map: Map)

我们需要在模型中遵循这个协议,官方给出了一个参考:

class User: Mappable {
    var username: String?
    var age: Int?
    var weight: Double!
    var array: [Any]?
    var dictionary: [String : Any] = [:]
    var bestFriend: User?                       // Nested User object
    var friends: [User]?                        // Array of Users
    var birthday: Date?

    required init?(map: Map) {

    }

    // Mappable
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
        weight      <- map["weight"]
        array       <- map["arr"]
        dictionary  <- map["dict"]
        bestFriend  <- map["best_friend"]
        friends     <- map["friends"]
        birthday    <- (map["birthday"], DateTransform())
    }
}

struct Temperature: Mappable {
    var celsius: Double?
    var fahrenheit: Double?

    init?(map: Map) {

    }

    mutating func mapping(map: Map) {
        celsius     <- map["celsius"]
        fahrenheit  <- map["fahrenheit"]
    }
}

一旦我们的类或结构体如上面的示例一样实现了协议,我们就可以方便的进行JSON和模型之间的转换

let user = User(JSONString: JSONString)

let JSONString = user.toJSONString(prettyPrint: true)

当然也可以通过Mapper类来进行转换

let user = Mapper<User>().map(JSONString: JSONString)

let JSONString = Mapper().toJSONString(user, prettyPrint: true)
嵌套对象的映射

正如前面所列,ObjectMapper支持嵌套对象的映射

列如:

{
    "distance" : {
        "text" : "102",
        "value" : 31
    }
}

我们想要直接取出distance对象中的value值,可以设置如下mapping

func mapping(map: Map) {
    distance <- map["distance.value"]
}
自定义转换规则

ObjectMapper允许开发者在数据映射过程中指定转换规则

class People: Mappable {
   var birthday: NSDate?
   
   required init?(_ map: Map) {
       
   }
   
   func mapping(map: Map) {
       birthday <- (map["birthday"], DateTransform())
   }
   
   let JSON = "\"birthday\":1458117795332"
   let result = Mapper<People>().map(JSON)
}

由于我们指定了birthday的转换规则,所以上述代码在解析JSON数据的时候会将long类型转换成Date类型

除了使用ObjectMapper给我们提供的转换规则外,我们还可以通过实现TransformType协议来自定义我们的转换规则

public protocol TransformType {
    typealias Object
    typealias JSON
    
    func transformFromJSON(value: AnyObject?) -> Object?
    func transformToJSON(value: Object?) -> JSON?
}

ObjectMapper为我们提供了一个TransformOf类来实现转换结果,TransformOf实际就是实现了TransformType协议的,TransformOf有两个类型的参数和两个闭包参数,类型表示参与转换的数据的类型,闭包表示转换的规则

let transform = TransformOf<Int, String>(fromJSON: { (value: String?) -> Int? in 
}, toJSON: { (value: Int?) -> String? in 
  // transform value from Int? to String?
  if let value = value {
      return String(value)
  }
  return nil
})

id <- (map["id"], transform)
泛型对象

ObjectMapper同样可以处理泛型类型的参数,不过这个泛型类型需要在实现了Mappable协议的基础上才可以正常使用

class User: Mappable {
    var name: String?
    
    required init?(_ map: Map) {
        
    }
    
    func mapping(_ map: Map) {
        name <- map["name"]
    }
}

class Result<T: Mappable>: Mappable {
    var result: T?
    
    required init?(_ map: Map) {
        
    }
    
    func mapping(map: Map) {
        result <- map["result"]
    }
}

let JSON = "{\"result\": {\"name\": \"anenn\"}}"
let result = Mapper<Result<User>>().map(JSON)

原理解析

ObjectMapper声明了一个Mappable协议,这个协议里声明了两个方法

init?(map: Map)

mutating func mapping(map: Map)

并且通过对Mappable协议的扩展,增加了JSON和模型之间的四个转换方法

public extension BaseMappable {
    
    /// Initializes object from a JSON String
    public init?(JSONString: String, context: MapContext? = nil) {
        if let obj: Self = Mapper(context: context).map(JSONString: JSONString) {
            self = obj
        } else {
            return nil
        }
    }
    
    /// Initializes object from a JSON Dictionary
    public init?(JSON: [String: Any], context: MapContext? = nil) {
        if let obj: Self = Mapper(context: context).map(JSON: JSON) {
            self = obj
        } else {
            return nil
        }
    }
    
    /// Returns the JSON Dictionary for the object
    public func toJSON() -> [String: Any] {
        return Mapper().toJSON(self)
    }
    
    /// Returns the JSON String for the object
    public func toJSONString(prettyPrint: Bool = false) -> String? {
        return Mapper().toJSONString(self, prettyPrint: prettyPrint)
    }
}

我们在使用ObjectMapper进行转换时,首先需要让模型遵循Mappable协议,并且实现Mappable声明的两个方法

class User: Mappable {
    var username: String?
    var age: Int?

    required init?(map: Map) {

    }

    // Mappable
    func mapping(map: Map) {
        username    <- map["username"]
        age         <- map["age"]
    }
}

因为ObjectMapper为Mappable增加了四个转换方法,所以User也继承了这四个转换方法

let user = User(JSONString: JSONString)

这个方法的调用其实就是帮助我们执行了

Mapper().map(JSONString: JSONString)

因此,我们也可以直接通过如下方法进行转换

let user = Mapper<User>().map(JSONString: JSONString)

接下来就是ObjectMapper的核心啦~
为什么通过Mapper类能完成转换呢?

我们来逐步了解Mapper的实现原理

public final class Mapper<N: BaseMappable> {
    
}

正如上面的代码块所示,首先Mapper类定义了一个泛型N,N必须要遵循Mappable协议,也就是我们的模型User
继续往下看,我们选择一个常用的方法

public func map(JSONString: String) -> N? {
        if let JSON = Mapper.parseJSONStringIntoDictionary(JSONString: JSONString) {
            return map(JSON: JSON)
        }
        
        return nil
    }

没错,我们前面写的Mapper的转换方法正是使用的这个方法

Mapper().map(JSONString: JSONString)

那我们就来一层一层详细分析一下这个方法的内部实现吧

// 1. 调用Mapper的静态方法parseJSONStringIntoDictionary来将JSON转换成字典
// 2. 调用map方法将转换后的字典转换成模型并返回
if let JSON = Mapper.parseJSONStringIntoDictionary(JSONString: JSONString) {
            return map(JSON: JSON)
        }
        
        return nil

先分解parseJSONStringIntoDictionary的实现

/// Convert a JSON String into a Dictionary<String, Any> using NSJSONSerialization
    public static func parseJSONStringIntoDictionary(JSONString: String) -> [String: Any]? {
        let parsedJSON: Any? = Mapper.parseJSONString(JSONString: JSONString)
        return parsedJSON as? [String: Any]
    }

    /// Convert a JSON String into an Object using NSJSONSerialization
    public static func parseJSONString(JSONString: String) -> Any? {
        let data = JSONString.data(using: String.Encoding.utf8, allowLossyConversion: true)
        if let data = data {
            let parsedJSON: Any?
            do {
                parsedJSON = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
            } catch let error {
                print(error)
                parsedJSON = nil
            }
            return parsedJSON
        }

        return nil
    }

通过上述代码的逻辑不难发现,parseJSONStringIntoDictionary就是利用了系统的JSONSerialization将JSON字符串转换成字典

理解了第一步,接下来我们就来看看第二步的实现原理

我们先整体看一下map方法的实现代码

/// Maps a JSON dictionary to an object that conforms to Mappable
    public func map(JSON: [String: Any]) -> N? {
        let map = Map(mappingType: .fromJSON, JSON: JSON, context: context, shouldIncludeNilValues: shouldIncludeNilValues)
        
        if let klass = N.self as? StaticMappable.Type { // Check if object is StaticMappable
            if var object = klass.objectForMapping(map: map) as? N {
                object.mapping(map: map)
                return object
            }
        } else if let klass = N.self as? Mappable.Type { // Check if object is Mappable
            if var object = klass.init(map: map) as? N {
                object.mapping(map: map)
                return object
            }
        } else if let klass = N.self as? ImmutableMappable.Type { // Check if object is ImmutableMappable
            do {
                return try klass.init(map: map) as? N
            } catch let error {
                #if DEBUG
                let exception: NSException
                if let mapError = error as? MapError {
                    exception = NSException(name: .init(rawValue: "MapError"), reason: mapError.description, userInfo: nil)
                } else {
                    exception = NSException(name: .init(rawValue: "ImmutableMappableError"), reason: error.localizedDescription, userInfo: nil)
                }
                exception.raise()
                #else
                NSLog("\(error)")
                #endif
            }
        } else {
            // Ensure BaseMappable is not implemented directly
            assert(false, "BaseMappable should not be implemented directly. Please implement Mappable, StaticMappable or ImmutableMappable")
        }
        
        return nil
    }

我们先忽略掉StaticMappableImmutableMappable这两种协议的处理逻辑,直接关注最重要的Mappable协议的实现

// 根据传入的JSON字典等数据创建一个map对象

let map = Map(mappingType: .fromJSON, JSON: JSON, context: context, shouldIncludeNilValues: shouldIncludeNilValues)

// 判断要转换成的模型是不是 遵循的Mappable协议
if let klass = N.self as? Mappable.Type 

// 创建一个N类型的对象
var object = klass.init(map: map) as? N

// 获取模型中定义的解析规则,完成解析
object.mapping(map: map)

// 返回生成好的模型
return object

这里还留了一个疑问,为何执行完object.mapping(map: map)后,模型就能完成解析呢

继续分析[Map]((https://github.com/Hearst-DD/ObjectMapper/blob/master/Sources/Mapper.swift)类

原来在Map类中使用了subscript来自定义下标
同样我们直接来分析最重要的那个自定义下标的方法

public subscript(key: String, nested nested: Bool, delimiter delimiter: String, ignoreNil ignoreNil: Bool) -> Map {
        // save key and value associated to it
        currentKey = key
        keyIsNested = nested
        nestedKeyDelimiter = delimiter
        
        if mappingType == .fromJSON {
            // check if a value exists for the current key
            // do this pre-check for performance reasons
            if nested == false {
                let object = JSON[key]
                let isNSNull = object is NSNull
                isKeyPresent = isNSNull ? true : object != nil
                currentValue = isNSNull ? nil : object
            } else {
                // break down the components of the key that are separated by .
                (isKeyPresent, currentValue) = valueFor(ArraySlice(key.components(separatedBy: delimiter)), dictionary: JSON)
            }
            
            // update isKeyPresent if ignoreNil is true
            if ignoreNil && currentValue == nil {
                isKeyPresent = false
            }
        }
        
        return self
    }

不难发现,在这方法中,我们从JSON字典中根据key获取了value,原来当我们调用object.mapping(map: map)时,就会依次根据我们配置的mapping值获取对应的value

ObjectMapper实践

分析完原理,我们还需要能够熟练的运用ObjectMapper来帮助我们完成解析功能,ObjectMapper能够帮助我们处理数据的方法有很多,这里我就先简单跟大家分享几种在项目中常用的方法,我已经将相关的Demo上传到github上,欢迎大家star

解析单一结构的模型

单一结构的模型解析比较简单,大家了解一下即可

{
  "name": "objectmapper",
  "age": 18,
  "nickname": "mapper",
  "job": "swifter"
}

当我们拿到如上所示的json数据时,只需要创建一个遵循Mappable的模型,并配置好解析路径即可

class User: Mappable {

    var name: String?
    var age: Int?
    var nickname: String?
    var job: String?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        name <- map["name"]
        age <- map["age"]
        nickname <- map["nickname"]
        job <- map["job"]
    }
    
}

// 使用方法
let user = Mapper<User>().map(JSONString: json.rawString()!)

解析模型中嵌套模型的情况

有时候我们拿到的json数据嵌套了好几层的结构,而我们刚好也需要逐层解析拿到每个模型的数据,下面我就通过一个两层结构给大家演示一下处理的方法

{
  "weather": "sun",
    "temperature": {
        "celsius": 70,
        "fahrenheit": 34
    },
}

正如上面的json数据所示,我们需要创建一个weather模型,同时包含一个temperature模型来解析json数据,这时我们就需要运用泛型来帮助我们达到嵌套的目的

class Weather<T: Mappable>: Mappable {

    var weather: String?
    var temperature: T?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        weather <- map["weather"]
        temperature <- map["temperature"]
    }
}

class Temperature: Mappable {
    
    var celsius: Double?
    var fahrenheit: Double?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        celsius <- map["celsius"]
        fahrenheit <- map["fahrenheit"]
    }
}

// 使用方法如下
let weather = Mapper<Weather<Temperature>>().map(JSONString: json.rawString()!)

解析模型中嵌套模型但是我们只需要拿到子模型的属性值

有些json数据嵌套的内容我们不需要分模型来获取,只需要将属性统一到一个模型中使用

{
    "distance": {
        "text": "102 ft",
        "value": 31
    }
}
class Distance: Mappable {
    var text: String?
    var value: Int?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        text <- map["distance.text"]
        value <- map["distance.value"]
    }

}
解析指定的一个数组
{
    "status": "200",
    "msg": "success",
    "features": [
       {
         "name": "json解析"
       },
       {
         "name": "模型转换"
       },
       {
         "name": "数据处理"
       },
       {
         "name": "mapper进阶"
       }
    ]
}

通过我们拿到的返回结果如上面的代码段所示,features是我们真正关心的数据,它是一个数组结构,我们期望能够得到的是一个数组,里面包含若干个feature模型

首先我们必不可少的就是要创建一个Feature模型

class Feature: Mappable {

    var name: String?
    
    required init?(map: Map) {
        
    }
    
    func mapping(map: Map) {
        name <- map["name"]
    }
    
}

在解析这个JSON数据时,我们可以忽略掉其他信息,先获取features对应的json数据

let featureJson = json["features"];

然后只需要使用Mapper的高级用法mapArray方法即可直接得到数组对象

let features = Mapper<Feature>().mapArray(JSONString: featureJson.rawString()!)

ObjectMapper的高级用法还有很多,掌握上面的这些用法基本已经可以在项目中使用ObjectMapper达到我们的需求了,后面有时间我会在此博客和Demo基础上更新更多的用法供大家参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • //我所经历的大数据平台发展史(三):互联网时代 • 上篇http://www.infoq.com/cn/arti...
    葡萄喃喃呓语阅读 51,170评论 10 200
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 黑白电影在礼堂上演 事件是一场灾难 许多人去看了 有人流泪 也有人在笑 电影的音响效果不好 放映机的声音很大 中途...
    关中陈镜阅读 173评论 0 2
  • 续《好久不见》 01-嘿,你发型变了耶。-是吗,不就是没梳头发,刘海又掉下来了呗。-嗯,这样比较好看。 那天接到个...
    维哈阅读 592评论 0 1
  • 杜蕾斯给京东做了广告《你不必》火了,你不必把胃喝穿,你不必总是笑,你不必买大房子。你不必… 火得原因正是说出了大多...
    芬芬vstar阅读 193评论 1 12