前言

对于大多数的应用程序来说,最常见的任务就是进行网络数据的发送和接收,但是在执行此操作之前,我们需要通过编码或者序列化的方式将数据转换为合适的格式来发送,然后还需要将收到的网络数据转换为合适的格式,这样才能在应用中使用它们,这样的过程叫做解码或着叫反序列化。
Codable发布我就不学,摸鱼爽歪歪!

那如何去定义这个格式呢!这里就不得不提 JSON 了,JSON 目前是网络通信发送和接收数据最常用的格式,但是在 Swift4.0 之前,大家都是用一些第三方的开源库来对 JSON 格式进行解析。

终于, Apple 在 Swift4.0 的 Foundtion 模块中添加了对 JSON 解析的原生支持,它的功能强大而且易于使用,接下来就让我带大家 了解下在 swift 里如何来对你的数据进行 encoding 和 decoding 吧!

基础知识介绍

在 swift 里要对 JSON 进行处理的话,首先需要了解的概念就是:Codable, Codable 其实它不是一个协议,而是另外俩个协议的组合:Decodable 和 Encodable,它的源码如下所示:

public typealias Codable = Decodable & Encodable

所以聪明的你一定可以猜到,只要数据模型遵行了 Codable 协议,那么就可以方便的进行 JSON 和数据模型的相互转换了。

在 Swift4.0 中,Apple 提供了 JSONEncoder 和 JSONDecoder 俩对象来处理 JSON 的编码和解码,核心代码如下:

let encoder = JSONEncoder()
let decoder = JSONDecoder()

相关的概念已介绍完毕,你准备好迎接挑战了吗?

Codable发布我就不学,摸鱼爽歪歪!

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

JSON 转数据模型

TASK 1:简单的数据结构

如果你的 JSON 结构和你使用的数据模型结构一致的话,那么解析过程将会非常简单,请看下面内容:

下面给出的是一个歌曲的 JSON 数据,我现在要将其转换为 SongModel。

let song = """
        {
            "singer": "The Chainsmokers",
            "name": "Something Just Like This",
        }
    """

SongModel

/// SongModel模型,遵循 Codable 协议
struct SongModel: Codable {
    var singer: String?
    var name: String?
}

转换过程如下

if let jsonData = song.data(using: String.Encoding.utf8) {
    if let sSong = try? JSONDecoder().decode(SongModel.self, from: jsonData) {
        dump(sSong)
    }
}

输出结果如下

▿ JSONDecoderDemo.SongModel
  ▿ singer: Optional("The Chainsmokers")
    - some: "The Chainsmokers"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"

这样就完成了解析,是不是很简单!

NOTE:在数据模型的成员变量中,基本数据类型如:String、Int、Float等都已经实现了 Codable 协议,因此如果你的数据类型只包含这些基本数据类型的属性,只需要在类型声明中加上 Codable 协议就可以了,不需要写任何实际实现的代码。

TASK 2: 解析数组

假如这是我们收到的一张专辑 Album 的 JSON 数据,现在要把它转化成 AlbumModel 数据模型。

let album = """
        {
            "singer": "The Chainsmokers",
            "name": "Something Just Like This",
            "songs":[
                "Something Just Like This",
                "Closer",
                "Young",
                "All We Know"
            ]
        }
    """

AlbumModel

struct AlbumModel: Codable {
    var singer: String?
    var name: String?
    var songs: [String]?
}

转换过程如下

if let jsonData = album.data(using: String.Encoding.utf8) {
    if let sSong = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
        dump(sSong)
    }
}

输出结果为

▿ JSONDecoderDemo.AlbumModel
  ▿ singer: Optional("The Chainsmokers")
    - some: "The Chainsmokers"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"
  ▿ songs: Optional(["Something Just Like This", "Closer", "Young", "All We Know"])
    ▿ some: 4 elements
      - "Something Just Like This"
      - "Closer"
      - "Young"
      - "All We Know"

和上面的转换如出一辙,想必你现在心里已经在默默的嘀咕:这么简单还用你讲?

Codable发布我就不学,摸鱼爽歪歪!

那接下来就请准备迎接新的挑战把!

TASK 3:结构不一致

上面所演示的 JSON 数据格式都是与数据模型里的成员变量一一对应的,但是,在实际开发中,你会经常遇到数据源的格式和数据模型结构 不一致的情况,很多情况下可能是服务端与客户端没有统一好接口的格式,然后各自就开始开发,到需要进行调试的时候,客户端一收到消息,就懵逼了:

Codable发布我就不学,摸鱼爽歪歪!

NOTE: 所以在这里我非常建议大家在做功能开发之前一定要先把接口文档定义好,定义好,定义好,重要的事情讲三遍。

这样服务端和客户端之间定义的数据格式就存在了差异,无论怎样当然总有一方需要作出让步来做到兼容,那么当客户端想要做兼容时,该怎么处理呢!我们先来看个例子:

例如服务端返回的数据为:

let album = """
        {
            "singer": "The Chainsmokers",
            "name": "Something Just Like This",
            "songList":[
                "Something Just Like This",
                "Closer",
                "Young",
                "All We Know"
            ]
        }
    """

可以看到,原来的 songs 换成了 songList,我们执行下原先的代码,看下输出:

▿ JSONDecoderDemo.AlbumModel
  ▿ singer: Optional("The Chainsmokers")
    - some: "The Chainsmokers"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"
  - songs: nil

发现 songs 字段变成了 nil, 这个解析就失败了,那如何做到不修改我之前定义的数据模型的成员变量,来做到兼容呢!这时候就需要用到 CodingKey 协议了, 借助 CodingKey 可以用来映射数据模型的成员变量,首先在数据模型中添加一个特殊的枚举类型:

private enum CodingKeys: String, CodingKey

添加完后的数据模型代码如下:

struct AlbumModel: Codable {
    var singer: String?
    var name: String?
    var songs: [String]?

    enum CodingKeys: String, CodingKey {
        case singer = "singer"
        case name = "name"
        case songs = "songList"
    }
}

输出结果为

▿ JSONDecoderDemo.AlbumModel
  ▿ singer: Optional("The Chainsmokers")
    - some: "The Chainsmokers"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"
  ▿ songs: Optional(["Something Just Like This", "Closer", "Young", "All We Know"])
    ▿ some: 4 elements
      - "Something Just Like This"
      - "Closer"
      - "Young"
      - "All We Know"

这样,我们是不是就可以正常解析 JSON 数据了,是不是很强大。

接下来再增加一个难度!

当给你的唱片的 JSON 结构是这样的,你该怎么解析呢!

let album = """
        {
            "singer": "The Chainsmokers",
            "name": "Something Just Like This",
            "songs": "Something Just Like This"
        }
    """

根据上面给出的例子,你会发现它依然与你的数据模型不匹配,原来的 songs 字段不是数组格式了,那如何才能正常的解析到数据模型上去呢,这时候就需要我们自己来实现编码解码的逻辑了。

首先在 AlbumModel 中加入以下代码:

struct AlbumModel: Codable {
    var singer: String?
    var name: String?
    var songs: [String]?

    // 1
    enum CodingKeys: String, CodingKey {
        case singer = "singer"
        case name = "name"
        case songs = "songs"
    }

    // 解码: JSON 转 Model
    init(from decoder: Decoder) throws {
        // 2
        let container = try decoder.container(keyedBy: CodingKeys.self)
        // 3
        singer = try container.decode(String.self, forKey: .singer)
        name = try container.decode(String.self, forKey: .name)
        let ss = try container.decode(String.self, forKey: .songs)
        songs = [ss]
    }

    // 编码: Model 转 JSON
    func encode(to encoder: Encoder) throws {
        // 4
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(singer, forKey: .singer)
        try container.encode(name, forKey: .name)
        try container.encode(songs, forKey: .songs)
    }
}

解释如下

  1. 创建 CodingKeys 枚举,用于映射 JSON 字段。
  2. 创建一个解码器容器,来存储 JSON 里的属性。
  3. 使用适当的类型和编码键从容器中提取歌手和专辑名和歌单,由于歌单是数组类型的,所以需要将提取到的歌转换成数组。
  4. 创建 KeyedEncodingContainer 容器来对数据模型里的属性进行编码。

转换过程如下

if let jsonData = album.data(using: String.Encoding.utf8) {
    if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
        dump(aAlbum)
    }
}

结果如下

▿ JSONDecoderDemo.AlbumModel
  ▿ singer: Optional("The Chainsmokers")
    - some: "The Chainsmokers"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"
  ▿ songs: Optional(["Something Just Like This"])
    ▿ some: 1 element
      - "Something Just Like This"

可以看到通过上面的代码,已经可以将 JSON 中原先的 String 转换成数据模型中的数组类型了。

注意:如果需要借助 CodingKeys 解决字段不一致的情况,即使其他的属性不需要映射,也必须将其包含在枚举中,譬如:singer, name,否则会报错。

TASK 4: 复杂的嵌套

除了处理简单的数据模型,Codable 还可以处理复杂的嵌套数据模型,首先解释下什么是嵌套数据模型:

譬如我有个专门处理专辑的数据模型叫 AlbumModel,它里面内嵌了 SongModel 的属性,这就是嵌套。这里必须要说明的就是嵌套的数据模型以及嵌套的子模型都必须遵循 Codable 协议,下面我们举个嵌套的数据模型的例子来说明一下:

/// 专辑模型
struct AlbumModel: Codable {
    // 专辑名
    var albumName: String?
    // 发布时间
    var releaseTime: String?
    // 歌单(嵌套)
    var songs: [SongModel]?
}

/// 歌曲模型
struct SongModel: Codable {
    // 歌手(嵌套)
    var singer: SingerModel?
    // 歌曲
    var name: String?
}

/// 歌手模型
struct SingerModel: Codable {
    // 姓名
    var name: String?
    // 年龄
    var age: Int?
}

JSON 数据结构

    let album = """
        {
            "albumName": "Something Just Like This",
            "releaseTime": "2017-02-22",
            "songs":[
                {
                    "singer": {
                        "name":"The Chainsmokers",
                        "age": 30
                    },
                    "name": "Something Just Like This"
                },
                {
                    "singer": {
                        "name":"The Chainsmokers",
                        "age": 30
                    },
                    "name": "Closer"
                },
                {
                    "singer": {
                        "name":"The Chainsmokers",
                        "age": 30
                    },
                    "name": "Young"
                },
                {
                    "singer": {
                        "name":"The Chainsmokers",
                        "age": 30
                    },
                    "name": "All We Know"
                }
            ]
        }
    """

转换过程如下

if let jsonData = album.data(using: String.Encoding.utf8) {
    if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
        dump(aAlbum)
    }
}

输出结果为

JSONDecoderDemo.AlbumModel
  ▿ albumName: Optional("Something Just Like This")
    - some: "Something Just Like This"
  ▿ releaseTime: Optional("2017-02-22")
    - some: "2017-02-22"
  ▿ songs: Optional([JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Something Just Like This")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Closer")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Young")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("All We Know"))])
    ▿ some: 4 elements
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Something Just Like This")
          - some: "Something Just Like This"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Closer")
          - some: "Closer"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Young")
          - some: "Young"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("All We Know")
          - some: "All We Know"

这样这个嵌套就被解决了,接下来再挑战一个难度更大的,请看代码:

let album = """
        {
            "albumName": "Something Just Like This",
            "releaseTime": "2017-02-22",
            "songs": {
                "favorite":[
                    {
                        "singer": {
                            "name":"The Chainsmokers",
                            "age": 30
                        },
                        "name": "Something Just Like This"
                    },
                    {
                        "singer": {
                            "name":"The Chainsmokers",
                            "age": 30
                        },
                        "name": "Closer"
                    },
                    {
                        "singer": {
                            "name":"The Chainsmokers",
                            "age": 30
                        },
                        "name": "Young"
                    },
                    {
                        "singer": {
                            "name":"The Chainsmokers",
                            "age": 30
                        },
                        "name": "All We Know"
                    }
                ]
            }
        }
    """

可以看到,在歌单 Songs 中又嵌套了一个 favorite 字段,这个 JSON 结构相比 AlbumModel 这个数据模型又加深了一层,那该如何解析呢!

Codable发布我就不学,摸鱼爽歪歪!

这里我们就要用到 nestedContainer 来处理这种嵌套,首先在 AlbumModel 加入如下代码:

/// 专辑模型
struct AlbumModel: Codable {
    // 专辑名
    var albumName: String?
    // 发布时间
    var releaseTime: String?
    // 歌单
    var songs: [SongModel]?

    // 1
    enum CodingKeys: String, CodingKey {
        case albumName, releaseTime, songs
    }
    // 2
    enum favoriteKeys: CodingKey {
        case favorite
    }

    // 解码: JSON 转 Model
    init(from decoder: Decoder) throws {
        // 3
        let container = try decoder.container(keyedBy: CodingKeys.self)
        albumName = try container.decode(String.self, forKey: .albumName)
        releaseTime = try container.decode(String.self, forKey: .releaseTime)
        // 4
        let favoriteContainer = try container.nestedContainer(keyedBy: favoriteKeys.self, forKey: .songs)
        songs = try favoriteContainer.decode([SongModel].self, forKey: .favorite)
    }

    // 编码: Model 转 JSON
    func encode(to encoder: Encoder) throws {
        // 5
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(albumName, forKey: .albumName)
        try container.encode(releaseTime, forKey: .releaseTime)
        // 6
        var favoriteContainer = container.nestedContainer(keyedBy: favoriteKeys.self, forKey: .songs)
        try favoriteContainer.encode(songs, forKey: .favorite)
    }
}
/// 歌曲模型
struct SongModel: Codable {
    // 歌手
    var singer: SingerModel?
    // 歌曲
    var name: String?
}

/// 歌手模型
struct SingerModel: Codable {
    // 姓名
    var name: String?
    // 年龄
    var age: Int?
}

解析如下

  1. 首先创建最顶层的 CodingKeys
  2. 创建嵌套层的 CodingKeys
  3. 创建顶层 CodingKeys 对应的容器,并对其解码
  4. 创建嵌套层的容器,并对 favorite 解码
  5. 创建编码容器,并对 albumName 和 releaseTime 编码
  6. 获取嵌套容器,并对 favorite 编码

转换过程

if let jsonData = album.data(using: String.Encoding.utf8) {
    if let aAlbum = try? JSONDecoder().decode(AlbumModel.self, from: jsonData) {
        dump(aAlbum)
    }
}

输出如下

▿ JSONDecoderDemo.AlbumModel
  ▿ albumName: Optional("Something Just Like This")
    - some: "Something Just Like This"
  ▿ releaseTime: Optional("2017-02-22")
    - some: "2017-02-22"
  ▿ songs: Optional([JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Something Just Like This")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Closer")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("Young")), JSONDecoderDemo.SongModel(singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30))), name: Optional("All We Know"))])
    ▿ some: 4 elements
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Something Just Like This")
          - some: "Something Just Like This"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Closer")
          - some: "Closer"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("Young")
          - some: "Young"
      ▿ JSONDecoderDemo.SongModel
        ▿ singer: Optional(JSONDecoderDemo.SingerModel(name: Optional("The Chainsmokers"), age: Optional(30)))
          ▿ some: JSONDecoderDemo.SingerModel
            ▿ name: Optional("The Chainsmokers")
              - some: "The Chainsmokers"
            ▿ age: Optional(30)
              - some: 30
        ▿ name: Optional("All We Know")
          - some: "All We Know"

挑战成功,看到这里是不是已经有点晕了,说实话其实我自己也不知道我在表达啥,我也晕了,哈哈!

Codable发布我就不学,摸鱼爽歪歪!

Task 6:处理派生类

下面我们来看下一个特殊的数据模型结构,它应该怎么去转换呢!

当一个类遵循了 Codable 协议,那么它自身是可以很方便的使用 JSONEncoder 和 JSONDecoder 来 JSON 化和反 JSON 化的,但是如果有别的类继承了它,那么对该子类的 JSON 化和反 JSON 化就不是那么方便了。

首先来看个例子, SongMusic 的子类:

class Music: Codable {
    var kind: String?
}

class Song: Music {
    var name: String?
}

JSON 数据为:

let jsonString = """
        {
            "kind": "popular",
            "name": "Something Just Like This"
        }
    """

数据解析

if let jsonData = jsonString.data(using: String.Encoding.utf8) {
    if let song = try? JSONDecoder().decode(Song.self, from: jsonData) {
        dump(song)
    }
}

结果

▿ JSONDecoderDemo.Song #0
  ▿ super: JSONDecoderDemo.Music
    ▿ kind: Optional("popular")
      - some: "popular"
  - name: nil

通过上面的结果发现,Song 类的实例只解析出了父类中的 kind 字段,而自己的 name 未能解析,这说明 Codable 在继承中是无效的,当你在派生类中声明遵循该协议时,会报如下错误:

Redundant conformance of 'Song' to protocol 'Decodable'
Redundant conformance of 'Song' to protocol 'Encodable'

那如何才能解决这个问题呢!

这时候,就需要我们自行实现 Codable 协议了,代码如下:

class Song: Music {
    var name: String?

    enum CodingKeys: String, CodingKey {
        case type
        case name
    }

    init(type: String, name songName:String) {
        self.name = songName
        super.init(type: type)
    }

    required init(from decoder: Decoder) throws {
        try super.init(from: decoder)
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decode(String.self, forKey: .type)
        name = try container.decode(String.self, forKey: .name)
    }

    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(type, forKey: .type)
        try container.encode(name, forKey: .name)
    }
}

转换结果

▿ JSONDecoderDemo.Song #0
  ▿ super: JSONDecoderDemo.Music
    ▿ type: Optional("popular")
      - some: "popular"
  ▿ name: Optional("Something Just Like This")
    - some: "Something Just Like This"

通过上面的结果显示,我已经成功将 JSON 转成了相应的数据模型,那么对派生类的处理,我们只需要参考上面的代码,自行实现 Codable 协议,就可以避免上述的错误。

数据模型转 JSON

当实现 Codable 协议的某个对象想要转为 JSON 时,则可以借助 JSONEncoder 编码器来实现。

这个转换相对来说就比较简单了,这里就举个简单的例子吧!

let song = Song(type: "popular", name: "Something Just Like This")

if let jsonData = try? JSONEncoder().encode(song){
    if let jsonString = String.init(data: jsonData, encoding: String.Encoding.utf8){
        print("jsonString:" + "\(jsonString)")
    }
}

输出结果

jsonString:{"type":"popular","name":"Something Just Like This"}

数据模型转 JSON 就完成了,So Easy

Codable发布我就不学,摸鱼爽歪歪!

结语

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS开发交流群:130 595 548,不管你是小白还是大牛都欢迎入驻 ,让我们一起进步,共同发展!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)

到这里本篇文章就结束了,首先非常感谢大家能耐着性子看到这里,说实话我在准备这篇文章的时候也有点痛苦,越写越无聊,时常在写的过程中脑子一直在想:这么无聊的内容连我自己都写不下去了,会有读者愿意看吗?但是开弓没有回头箭,毕竟我也花了几天时间准备了素材,所以还是耐着寂寞写完了,内容过于枯燥,希望大家别嫌弃。