在iOS项目开发过程中,我们经常会用到将从服务器获取的 json 转 model 的操作,我们可以使用 Swift 提供的setValuesForKeys 或者 Objective-C 提供的setValuesForKeysWithDictionary 方法来完成这一操作。

使用上面两个方法只能将字典转换成 model,如果 json 最外层是个数组,那么我们就必须在循环中使用这个方法,这非常不方便, 而且还有个条件,就是 model 中的所有属性名必须跟字典中的 key 完全对应,这样就会遇到另外一个问题,如果我们字典中的一个 key 与系统关键字重名,那我们在 model 就不能使用这个 key 作为属性名了。

为了解决上面的问题,我们会使用一些第三方库去完成字典转模型的操作,例如 MJExtension 。由于它是一个 OC 的库,所以在 Swift 项目中需要引入桥接文件。在 Swift 中使用其 API 时其实是很不 swift 的。所以现在我们就用 Swift 3.0 来写一个 swift style 的 json 转模型的库吧。

例如我们有这样的两个 model:

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}

最终我们想实现这样的调用:

let repos = json ~> Repos.self    // 将一个字典转成一个Repos的实例
 
let viewers  = viewers => User.self  //将一个数组转换成User的数组

~>=> 是自定义的运算符,主要是为了调用方便。它们的定义是这样的:

public func ~><T: NSObject>(lhs: Any,rhs: T.Type) -> T?
public func =><T: NSObject>(lhs: Any,rhs: T.Type) -> [T]?

这里给出我的实现 ModelSwift。大家可以先看看我的实现然后试着写出自己的实现。好了,现在就让我们开始吧。

要解决的问题

由于将数组转成模型数组,其实要做的工作跟将字典转模型是一样的,只是做了个循环而已。所以我们首先要解决的问题是:如何在 Swift 将字典转成模型。这里我们是使用 KVC就可以了。我们使用 NSObject 的 setValue(_ value: Any?,forKey key: String) 方法来给对象设置值。

从上面要实现的效果来看,我们在使用前并不用先实例化一个对象。所以我们要解决的第二个问题是:如何通过类型来实例化一个对象。

另一个要解决的问题是字典中的 key 与关键字重名,或者我们想使用自己的名字。所以我们要实现自己的映射的策略。

还有一个问题是,如果我们服务器返回的字典数据中包含另外一个字典数组,对应我们的 model 中就是一个对象包含另外一个对象的数组。那么我们怎样才能知道这个数组中对象的类型呢?

实现思路

对于上面提到的第一问题我在上面已经给出了解决方案,就是让我们的 model 继承 NSObject,然后使用 setValue(_ value: Any?,forKey key: String) 方法来给对象设置值。这里的 value 其实是要根据 model 中的属性名去字典中获取的。如果我们能拿到 model 所有的属性名,那么给 model 设置值的操作就完成了。那么如何获取到 model 的属性名呢?这就必须得使用到 Swift 中的反射机制了。

Mirror

Swift 的反射机制是基于一个叫 Mirror 的 struct 来实现的。对于 Mirror 的详细结构大家可以按住 cmd 点进去查看。这里我们主要关注的是 public typealias Child = (label: String?,value: Any) 这个 typealias,它其实是一个元祖,label 就表示我们的属性名,是 Optional 的。value 表示的是属性的值。这里 label 为什么是 Optional 的?如果你仔细考虑下,其实这是非常有意义的,并不是所有支持反射的数据结构都包含有名字的子节点。 Mirror 会以属性的名字做为 label,但是 Collection 只有下标,没有名字。Tuple 同样也可能没有给它们的条目指定名字。

Mirror 有个 children 的存储属性,它的定义是这样的:

public let children: Mirror.Children

这里的 Mirror.Children 也是一个 typealias,它是这样定义的:

public typealias Children = AnyCollection<Mirror.Child>

可以看到它是 Child 的集合。所以我们可以通过 Mirror 的 children 属性来获得 model 的所有属性名。

我们写个类来测试一下:

class Person: NSObject {
    var name = ""
    var age = 0
    var friends: [Person]?
}

let mirror = Mirror(reflecting: Person())
for case let (label?,value) in mirror.children {
    print ("\(label) = \(value)")
}

运行结果是如下:

name = 
age = 0
friends = nil

Mirror 还有一个类型为 Any.TypesubjectType 存储属性,表示该映射对象的类型,例如上面的 mirror.subjectType 就是 User。使用 subjectType 就可以获得对象的类型以及其所有属性的类型。为了实现这个效果,我们可以写出下面的代码:

func subjectType(of subject: Any) -> Any.Type {
    let mirror = Mirror(reflecting: subject)
    return mirror.subjectType
}

func children(of subject: Any) {
    let mirror = Mirror(reflecting: subject)
    for case let(label?,value) in mirror.children {
        print ("\(label) = \(subjectType(of: value))")
    }
}

children(of: Person())

打印结果是这样的:

name = String
age = Int
friends = Optional<Array<Person>>

我原本想使用这个方法来得到 model 中包含的另外对象的类型和数组中对象的类型,例如 Person 中有 fatherfriends 属性:

class Person: NSObject {
    var name = ""
    var age = 100
    var father: Person?
    var friends: [Person]?
}

但是发现结果是 Optional<Person>Optional<Array<Person>>。所以我们还是得显示地指出一个 model 中包含的其他对象的类型,以及数组中对象的类型。在后面我会给出自己的实现。大家可以给出自己的实现。

通过类型来实例化一个对象

要使用 Mirror 来获得反射对象的所有属性名,就必须先使用 init(reflecting subject: Any) 来创建一个 Mirror。而创建 Mirror 就必须传入一个 subject(在这里我们主要传入一个NSObject类型的对象)。所以我们的首要任务就是通过类型来实例化一个对象。

有些同学可能有疑问了:我要转换成 Person 的对象,我直接传入一个
Person 的实例就行了啊。如果你看看我们 josn 转模型的方法定义就能明白了。 func ~><T: NSObject>(lhs: Any,rhs: T.Type) -> T?

还是以上面的 Person 为例,我们看看这样的调用:

Person.self().age
// 结果是:100

所以我们通过一个类的 self()方法可以得到一个类的实例。其实我们还可以通过 AnyClass 来实例化对象。AnyClass 是类的类型,其定义是这样的:

public typealias AnyClass = AnyObject.Type

我们通过类的self属性可以得到类的类型:

Person.self     
//结果是:Person.Type

得到类的类型后,通过调用其 init()方法就可以创建一个实例了:

Person.self.init().age
// 结果是:100

使用类型创建对象的类中的init方法前面必须是 required 的,因为这么创建方式是使用Meta type来创建的。由于我们 json 转模型的 model 是继承自 NSObject 的,所以不用在每个类中显示地实现。

写个简单的 josn 转模型

有了上面的基础就可以来实现我们的 josn 转模型了。首先我们来写出 ~> 的定义,并通过类来创建一个对象

infix operator ~>

func ~><T: NSObject>(lhs: Any,rhs: T.Type) -> T? {
    guard let json = lhs as? [String: Any],!json.isEmpty else {
        return nil
    }
    
    let obj = T.self()
    let mirror = Mirror(reflecting: obj)
    
    for case let(label?,value) in mirror.children {
        print ("\(label) = \(value)")
    }
    
    return obj
}

class Person: NSObject {
    var name = ""
    var age = 0

    override var description: String {
        return "name = \(name),age = \(age)"
    }
}
let json: [String: Any] = ["name": "jewelz","age": 100]
let p = json ~> Person.self
// 打印结果:
// name = 
// age = 0

通过上面的几行代码我们确实成功的创建了一个 Person 的实例了。下一步就是给实例设置值了。我们在上面的 for 循环中添加如下代码:

// 从字典中获取值
if let value = json[label] {
     obj.setValue(value,forKey: label)
}

整个代码就是这样的:

infix operator ~>

func ~><T: NSObject>(lhs: Any,_) in mirror.children {
        // 从字典中获取值
        if let value = json[label] {
            obj.setValue(value,forKey: label)
        }
    }
    return obj
}

let p = json ~> Person.self
print(p!)
//结果:name = jewelz,age = 100

有了上面 ~> 的实现,=> 的实现就很简单了:

infix operator =>
func =><T: NSObject>(lhs: Any,rhs: T.Type) -> [T]? {
    guard let array = lhs as? [Any],!array.isEmpty else {
        return nil
    }
    
    return array.flatMap{ $0 ~> rhs }
}

上面只是实现了一个简单 josn 转模型,其实在实际项目中要解决的问题还有很多。现在再来看看我在文章开头给出的 User 类和 Respo 类:

class User: NSObject {
    var name: String?
    var age = 0
    var desc: String?
}
class Repos: NSObject {
    var title: String?
    var owner: User?
    var viewers: [User]?
}

只简单的用上面的实现是无法得到想要的结果的。对于 User 类来说,desc 属性对应 json 的 description key,所以我们还要进行 model 的属性与 json 的键的映射。这里的思路就是将 model 的属性名作为 key,以要替换的 json 的键作为 value 存入字典中。我们可以拓展 NSObject ,添加一个计算属性并提供一个空实现。不过这样的倾入性太大,毕竟不是所有的类都需要做这个映射。所以最后的方式是 POP。比如我们可以制定这样一个协议:

public protocol Reflectable: class {
    var reflectedobject: [String: Any.Type] { get }
}

在需要做映射的类中去实现该协议。

对于更复杂的 Repos 类来说,要做的事情更多。比如 owner的类型怎么知道?owner 这个对象怎么完成赋值?viewers 数组中的类型是什么,怎样才能完成赋值? 虽然通过上面提到的 Mirro 可以得到所有的类型,但得到的是 Optional<User>以及 Optional<Array<User>>。我的解决的办法就跟上面做属性名替换是一样的。这里就不详细地说明了,大家可以各显神通。写出自己的实现。

写在最后

通过上面的几个步骤,我们就能很快的实现一个简单的 json 转模型的需求了。总结起来就是以下几点:

  • 所有要转换的 model 继承 NSObject

  • 使用类的类型来实例化对象

  • 通过反射获得对象的所有属性名

  • 通过 setValue(_ value: Any?,forKey key: String) 方法来给属性设置值

对于在最后提出的几个问题,我这里就不一一详细地说明了。大家可以点这里看看我的实现。大家可以使用 CocoaPods 或者 Carthage 将 ModelSwift 集成到项目中。如果在使用中有什么问题可以 issue 我,也可以给个 star 持续关注。

教你如何用Swift写个json转模型的开源库的更多相关文章

  1. HTML5 WebSocket实现点对点聊天的示例代码

    这篇文章主要介绍了HTML5 WebSocket实现点对点聊天的示例代码的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. 移动端html5模拟长按事件的实现方法

    这篇文章主要介绍了移动端html5模拟长按事件的实现方法的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. ios – 在Swift的UIView中找到UILabel

    我正在尝试在我的UIViewControllers的超级视图中找到我的UILabels.这是我的代码:这是在Objective-C中推荐的方式,但是在Swift中我只得到UIViews和CALayer.我肯定在提供给这个方法的视图中有UILabel.我错过了什么?我的UIViewController中的调用:解决方法使用函数式编程概念可以更轻松地实现这一目标.

  4. ios – 声明NSDictionary并在Swift中添加键值对?

    我一直在尝试使用类类型键和值来声明一个NSDictionary,如下所示:这里,“Category”和“SubCategory”是全局类.我知道我不能将类类型用于关键字段.但是,无论如何,我应该做到这一点.有没有办法做到这一点?如何声明专门的NSDictionary或类似的东西来做到这一点?

  5. ios – 在Swift中将输入字段字符串转换为Int

    所以我非常擅长制作APP广告Swift,我试图在文本字段中做一些非常简单的输入,取值,然后将它们用作Int进行某些计算.但是’vardistance’有些东西不正确它是导致错误的最后一行代码.它说致命错误:无法解开Optional.None解决方法在你的例子中,距离是一个Int?否则称为可选的Int..toInt()返回Int?因为从String到Int的转换可能失败.请参阅以下示例:

  6. ios – Swift相当于`[NSDictionary initWithObjects:forKeys:]`

    Swift的原生字典是否与[NSDictionaryinitWithObjects:forKeys:]相当?假设我有两个带键和值的数组,并希望将它们放在字典中.在Objective-C中,我这样做:当然我可以通过两个数组迭代一个计数器,使用vardict:[String:Int]并逐步添加东西.但这似乎不是一个好的解决方案.使用zip和enumerate可能是同时迭代两者的更好方法.然而,这种方法

  7. 如何在iOS中检测文本(字符串)语言?

    例如,给定以下字符串:我想检测每个声明的字符串中使用的语言.让我们假设已实现函数的签名是:如果没有检测到语言,则返回可选字符串.因此,适当的结果将是:有一个简单的方法来实现它吗?

  8. ios – NSArray indexOfObject返回nil

    任何想法为什么我不能得到一个我确定在数组中存在的对象的索引?相反,我没有……

  9. xamarin – 崩溃在AccountStore.Create().保存(e.Account,“);

    在Xamarin.Forms示例TodoAwsAuth中https://developer.xamarin.com/guides/xamarin-forms/web-services/authentication/oauth/成功登录后,在aOnAuthenticationCompleted事件中,应用程序在尝试保存到Xamarin.Auth时崩溃错误说不能对钥匙串说期待着寻求帮助.解决方法看看你

  10. ios – 将视频分享到Facebook

    我正在编写一个简单的测试应用程序,用于将视频从iOS上传到Facebook.由于FacebookSDK的所有文档都在Objective-C中,因此我发现很难在线找到有关如何使用Swift执行此操作的示例/教程.到目前为止我有这个在我的UI上放置一个共享按钮,但它看起来已禁用,从我读到的这是因为没有内容设置,但我看不出这是怎么可能的.我的getVideoURL()函数返回一个NSURL,它肯定包含视

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部