SwiftUI 官方教程:SwiftUI Tutorials 仅是几个表现 SwiftUI 简略运用的小 demo 而已,简略易学,循序渐进,先看完能够对 SwiftUI 有一个大约的认知。

二:Building Lists and Navigation

 SwiftUI Essentials – Creating and Combining Views 创立和组合 Views。

 设置根本地标详细信息视图后,需求为用户供给一种检查地标完整列表以及检查每个方位的详细信息的办法。创立可显现有关任何地标的信息的视图,并动态生成翻滚列表,用户能够点击该翻滚列表以检查地标的详细信息视图。要微调 UI,将运用 Xcode 的 canvas(画布)以不同的设备大小出现多个预览。

Landmark.swift

 创立一个 Landmark 模型(struct Landmark 结构体)在上个教程中,咱们都是硬编码信息输入到所有自界说视图中(ContentView 中是 “Turtle Rock”,CircleImage 中指定的名字是 “turtlerock” 的图片,MapView 中固定的经纬度坐标)。现在咱们创立一个模型(Landmark 结构体)来存储能够传递到视图中的数据。在 Landmark 结构体中包括一些与 landmarkData.json 数据文件中某些 keys 的称号匹配的特点。(landmarkData.json 中是一组地址信息数据)

 使 Landmark 结构体 遵照 Codable 协议,能够更轻松地在 Landmark 结构体和 landmarkData.json 数据文件之间移动数据。在后面,咱们将依靠 Codable protocol 的可解码组件(Decodable component )从 landmarkData.json 文件中读取数据。

import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    // imageName 私有,不必对外界露出,外界只需求下面的经过 imageName 从 asset catalog 中读取 Image 的核算特点 iamge 即可
    private var imageName: String
    var image: Image {
        Image(imageName)
    }
    // 同上,私有的 coordinates 特点,记载从本地 json 文件中读取的经纬度信息,locationCoordinate 核算特点,依据 coordinates 中的经纬度信息,构建 CLLocationCoordinate2D 实例
    private var coordinates: Coordinates
    // var coordinatesArray: [Coordinates] // 测验 Codable,测验 Json 字符串中存在数组类型
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }
    // 嵌套界说一个记载经纬度的结构体 Coordinates,而且遵照 Codable
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

 看到 Landmark、Coordinates 结构体都遵照了 Codable 协议,在接下来的学习之前,咱们先对 Swift 4.0 推出的 Codable 协议进行学习。

Codable

 Apple 官方在 Swift 4.0 的规范库中,引入了 Codable,它实际上是:public typealias Codable = Decodable & Encodable,即把编码和解码的功用归纳在一同,它能够将程序内部的数据结构(结构体、枚举、类)序列化成可交换数据(Json 字符串),也能够将通用数据格局(Json 字符串)反序列化为内部运用的数据结构(结构体、枚举、类),即在 Json 这种弱类型数据和代码中运用的强类型数据之间相互转化,大大提升目标和其表明之间相互转化的体验。

 或许更直白的理解为官方下场来和 YYModel、MJExtension、SwiftyJSON… 等这些第三方的 Json 数据转化库卷。

 下面从 Codable 协议为咱们带来的默许功用开端学习。

 Decodable 协议供给了一个初始化函数,遵照 Decodable 协议的类型能够运用任何 Decoder(let decoder = JSONDecoder())目标进行初始化,完结一个解码进程。

// A type that can decode itself from an external representation.
public protocol Decodable {
    // Creates a new instance by decoding from the given decoder.
    // This initializer throws an error if reading from the decoder fails, or if the data read is corrupted or otherwise invalid.
    init(from decoder: Decoder) throws
}

 这儿有点绕,咱们来分析一下,首要 struct Landmark 结构体遵照了 Decodable 协议,那么它就有了 init(from decoder: Decoder) throws 的能力,然后在咱们的解码进程中,咱们运用 let decoder = JSONDecoder() 构建了一个默许的 JSONDecoder 目标,接着调用它的 open func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable 函数,第一个参数 type 限制为遵照 Decodable 协议,第二个参数 data 则是 Json 字符串转化而来,针对当时的示例,最终回来一个 struct Landmark 结构体实例,假如咱们想自界说解码进程的话,则能够重写 struct Landmark 结构体的 init(from decoder: Decoder) throws 函数。

 开端以为 init(from decoder: Decoder) throws 函数的 decoder 参数便是咱们自己构建的 let decoder = JSONDecoder() 目标,发现并不是这样,经过打断点发现是这样的,一个是 open 的体系类 JSONDecoder,一个则是体系内部的私有类 _JSONDecoder

(lldb) p decoder
(JSONDecoder) $R0 = 0x000060000224c0a0 {
    ...
}
(lldb) p decoder
(Foundation.__JSONDecoder) $R1 = 0x00006000021506c0 {
    ...
}

 然后又顺着搜了一下发现要卷 Swift 源码才干理清 Codable 的功用,暂且不在本篇捋了,当时的核心是学习 SwiftUI,本篇只学一些 Codable 的常规运用。

 Encodable 协议供给了一个编码办法,遵照 Encodable 协议的类型的实例能够运用任何 Encoder(let encoder = JSONEncoder())目标创立表明(Data),完结一个编码进程。

// A type that can encode itself to an external representation.
public protocol Encodable {
    // Encodes this value into the given encoder.
    // If the value fails to encode anything, `encoder` will encode an empty keyed container in its place.
    // This function throws an error if any values are invalid for the given encoder's format.
    func encode(to encoder: Encoder) throws
}

 只要类型遵照 Codable 协议,那么就会默许支撑 Codable 协议的 init(from:) 和 encode(to:) 办法。因为 Swift 规范库中的类型,比如 String、Int、Double 和 Foundation 结构中 Data、Date、URL 都是默许支撑 Codable 协议的,所以咱们自界说的结构体模型根本只需声明遵照 Codable 协议即可,便可取得编码和解码能力。

 遵照 Codable 协议的类型,只针对类型的存储特点进行解码和编码,核算特点是不包括在内的(核算特点能够理解为是一个类型的函数,不参加类型的内存布局)。

 因为 Codable 协议被规划出来用于替代 NSCoding 协议,所以遵照 Codable 协议的目标就能够无缝的支撑 NSKeyedArchiver 和 NSKeyedUnarchiver 目标进行 Archive&UnArchive 操作,把结构化的数据经过简略的办法耐久化和反耐久化。原有的解码进程和耐久化进程需求独自处理,现在经过新的 Codable 协议一同搞定,大大提高了功率。

Codable 的默许完成

 以上面的 Landmark 结构体(这儿为了测验数组类型的解码,给 Landmark 增加了一个数组类型的成员变量:var coordinatesArray: [Coordinates])和 landmarkData.json 中一个景点的原始 Json 数据为例:

let turtleRockString = """
    {
        "id": 1001,
        "name": "Turtle Rock",
        "park": "Joshua Tree National Park",
        "state": "California",
        "description": "Suscipit ...",
        "imageName": "turtlerock",
        "coordinates": {
            "longitude": -116.166868,
            "latitude": 34.011286
        },
        "coordinatesArray": [
            {
                "longitude": -1,
                "latitude": 3
            },
            {
                "longitude": -11,
                "latitude": 34
            }
        ]
    }
"""
func decode<T: Decodable>(_ jsonString: String) -> T {
    guard let data = jsonString.data(using: .utf8) else {
        fatalError("\(jsonString) cannot be converted to Data")
    }
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(jsonString) as \(T.self):\n\(error)")
    }
}
func encode<T: Encodable>(_ model: T) -> String {
    let data: Data
    do {
        let encoder = JSONEncoder()
        encoder.outputFormatting = .prettyPrinted
        data = try encoder.encode(model)
    } catch {
        fatalError("\(model) cannot be converted to Data")
    }
    guard let string = String(data: data, encoding: .utf8) else {
        fatalError("\(data) cannot be converted to string")
    }
    return string
}
let turtleRock: Landmark = decode(turtleRockString)
let encodeTurtleRockString: String = encode(turtleRock)

 decode 函数中整个解码的进程非常简略,创立一个解码器(JSONDecoder()),这个解码器的 decode 办法需求传入两个参数,第一个参数指定 data 转成的数据结构的类型,这个类型是将弱类型(Json 数据)转化成强类型的关键,第二个参数传入原始的 data 数据。

 编码进程与解码进程根本对应,体系供给了一个 JSONEncoder 目标用于编码。创立编码器,然后传入值给它进行编码,编码器经过 Data 实例的办法回来一个字节的调集,这儿为了便利显现,咱们将它转为了字符串回来。

 然后咱们直接打印 turtleRock 实例,便可看到 turtleRockString Json 串中的数据已经悉数转化到 Landmark 结构体实例中。然后咱们直接打印 encodeTurtleRockString 字符串,便可看到 turtleRock 实例的成员变量可被编码为一个结构化的 Json 字符串。

// print(turtleRock)
Landmark(id: 1001, name: "Turtle Rock", park: "Joshua Tree National Park", state: "California", description: "Suscipit ...", imageName: "turtlerock", coordinates: Landmarks.Landmark.Coordinates(latitude: 34.011286, longitude: -116.166868), coordinatesArray: [Landmarks.Landmark.Coordinates(latitude: 3.0, longitude: -1.0), Landmarks.Landmark.Coordinates(latitude: 34.0, longitude: -11.0)])
// print(encodeTurtleRockString)
{
  "coordinates" : {
    "longitude" : -116.16686799999999,
    "latitude" : 34.011285999999998
  },
  "coordinatesArray" : [
    {
      "longitude" : -1,
      "latitude" : 3
    },
    {
      "longitude" : -11,
      "latitude" : 34
    }
  ],
  "id" : 1001,
  "park" : "Joshua Tree National Park",
  "description" : "Suscipit ...",
  "imageName" : "turtlerock",
  "state" : "California",
  "name" : "Turtle Rock"
}

 这儿看到了一个细节点,原始 Json 字符串中 “coordinates”: { “longitude”: -116.166868, “latitude”: 34.011286 } 的经纬度值是 -116.166868/34.011286 当转化为 Landmark 实例时仍是相同的值相同的精确度,可是当咱们把 Landmark 实例编码为 Json 字符串时,发现精度值产生了改变:-116.16686799999999/34.011285999999998。(暂时不知道怎么处理这种精确度丢掉问题)

 除了上面 Json 字符串中都是根底类型的键值对外,还有其他一些特殊状况:

  1. Json(JavaScript Object Notation)字符串中存在嵌套(目标、字典)

 上面的示例中 Landmark 结构体还嵌套了一个 Coordinates 结构体,只要 Coordinates 相同也遵照 Codable,那么就能从 turtle_rockString json 串中直接解析出经纬度的值赋值到 Landmark 结构体的 var coordinates: Coordinates 成员变量中。

  1. Json 字符串中包括数组(数组中的模型要遵照 Codable)

 上面的示例中 Landmark 结构体中的 var coordinatesArray: [Coordinates] 成员变量,数据也得到了正确的解析。

 针对上面的 1 和 2 条,因为 Swift 4.0 支撑条件一致性,所有当数组(Array)中每个元素遵照 Codable 协议、字典(Dictionary)中对应的 key 和 value 遵照 Codable 协议,整体目标就遵照 Codable 协议。

 在 Swift/Collection/Array 中可看到 Array 遵照 Codable 协议:

extension Array : Encodable where Element : Encodable {
    public func encode(to encoder: Encoder) throws
}
extension Array : Decodable where Element : Decodable {
    public init(from decoder: Decoder) throws
}

 在 Swift/Collection/HashedCollections/Dictionary 中看到 Dictionary 遵照 Codable 协议:

extension Dictionary : Encodable where Key : Encodable, Value : Encodable {
    // 这句注释超重要,下面咱们会学习到 keyed container 和 unkeyed container
    // If the dictionary uses `String` or `Int` keys, the contents are encoded in a keyed container. Otherwise, the contents are encoded as alternating key-value pairs in an unkeyed container.
    public func encode(to encoder: Encoder) throws
}
extension Dictionary : Decodable where Key : Decodable, Value : Decodable {
    public init(from decoder: Decoder) throws
}
  1. Json 字符串是一个模型数组时,如下形式时,此刻在 return try decoder.decode(T.self, from: data) 中传入类型时需求传输数组类型,例如: [Landmark]
[
    {
        "id": 1001,
        "name": "Turtle Rock",
        ...
    },
    {
        "id": 1002,
        "name": "Silver Salmon Creek",
        ...
    },
]

 在下面的 ModelData.swift 文件中:var landmarks: [Landmark] = load("landmarkData.json") 正是,从 landmarkData.json 文件中读出一个模型数组的 Json 字符串,然后解码为一个 Landmark 数组。

  1. Json 字符串中有 Optional values 时(空值 null),此刻在模型界说时也指定对应的成员变量为可选类型即可。
let turtle_rockString = """
    {
        "id": 1001,
        "name": null,
        "park": "Joshua Tree National Park",
        ...
    }
"""
struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String?
    var park: String
    ...
}

 Json 字符串中 name 为可选,那么在 Landmark 中把 name 成员变量界说为一个可选类型,不然当 Json 字符串中的 name 回来 null 时会打印如下错误信息(valueNotFound,Expected String value but found null instead. 预期为 String 的值却发现了 null):

valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "name", intValue: nil)], debugDescription: "Expected String value but found null instead.", underlyingError: nil))
  1. Json 字符串中存在嵌套目标,且此目标有可能是个 空目标({},留意这儿和上面的 null 是不同的处理状况)时,如下把上面的 turtleRockString 字符串中的 coordinates 置为一个空目标:
let turtleRockString = """
    {
        ...
        "coordinates": {},
        ...
    }
"""

 此刻咱们假如直接运转的话就会打印如下错误信息(keyNotFound,No value associated with key CodingKeys(stringValue: “latitude”, intValue: nil) (“latitude”). 没有找到 CodingKeys 中与 latitude 相关的值):

keyNotFound(CodingKeys(stringValue: "latitude", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "coordinates", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: \"latitude\", intValue: nil) (\"latitude\").", underlyingError: nil))

 那么咱们会联想到上面的 Optional values(空值 null)的状况,会想到把 Landmark 结构体中的 private var coordinates: Coordinates 成员变量也设置为可选类型:Coordinates?,再次运转会发现仍然打印上面相同的错误信息。此刻咱们定睛一看,错误信息中说到没有找到与 latitude 相关的值,那么咱们直接把 Coordinates 结构体的 latitudelongitude 两个成员变量设置为可选类型。此刻便可正常解码和编码。

struct Coordinates: Hashable, Codable {
    var latitude: Double?
    var longitude: Double?
}

 把 Coordinates 的每个成员变量设置为可选类型,这样当 coordinates 回来为 {} 时,latitude 和 longitude 自动解析为 nil。(print(turtleRock.coordinates): Coordinates(latitude: nil, longitude: nil))

// print(turtleRock)
Landmark(id: 1001, name: "Turtle Rock", park: "Joshua Tree National Park", state: "California", description: "Suscipit ...", imageName: "turtlerock", coordinates: Optional(Landmarks.Landmark.Coordinates(latitude: nil, longitude: nil)), coordinatesArray: [Landmarks.Landmark.Coordinates(latitude: Optional(3.0), longitude: Optional(-1.0)), Landmarks.Landmark.Coordinates(latitude: Optional(34.0), longitude: Optional(-11.0))])
// coordinatesArray: [Landmarks.Landmark.Coordinates(latitude: 3.0, longitude: -1.0), Landmarks.Landmark.Coordinates(latitude: 34.0, longitude: -11.0)]
// 和上面的比照,coordinatesArray 成员变量中的 Coordinates 的 latitude 和 longitude 都变成了可选,当咱们运用时需求解包。    
// print(encodeTurtleRockString)
{
  "coordinates" : {
  },
  "coordinatesArray" : [
    {
      "longitude" : -1,
      "latitude" : 3
    },
    {
      "longitude" : -11,
      "latitude" : 34
    }
  ],
  "id" : 1001,
  "park" : "Joshua Tree National Park",
  "description" : "Suscipit ...",
  "imageName" : "turtlerock",
  "state" : "California",
  "name" : "Turtle Rock"
}

 所以为了确保当服务器给咱们回来 {} 或许 null 时程序都能正常解码,咱们需求把 coordinates 特点,以及 Coordinates 结构的各个成员变量都界说为可选类型。

 所以到了这儿咱们可能会发现一个问题,便是咱们不知道 Json 字符串中哪些字段会回来空值,咱们又不能彻底信任服务器回来给咱们的字段必定是有值的,哪天忽略了回来了空值可咋整,此刻咱们在界说类型时就会不得不把所有的成员变量都定为可选值了。

  1. 遵照 Codable 协议的类型中界说了一个非可选的特点值,例如在上面的 struct Landmark 结构体中增加一个 var xxx: String 成员变量,然后在解码时 Json 字符串中又不包括此特点的话会打印如下错误信息(keyNotFound,No value associated with key CodingKeys(stringValue: “xxx”, intValue: nil) (“xxx”). 没有找到 CodingKeys 中与 xxx 相关的值):
keyNotFound(CodingKeys(stringValue: "xxx", intValue: nil), Swift.DecodingError.Context(codingPath: [], debugDescription: "No value associated with key CodingKeys(stringValue: \"xxx\", intValue: nil) (\"xxx\").", underlyingError: nil))

 此刻咱们需求把 xxx 界说为可选类型才干正常解码。(例如某天 Web 没有回来之前预定的必定回来的字段时,而此字段又指定的是非可选的话,那么 Codable 解码时会产生 crash,所以这儿又增加了一条原因,此刻咱们在界说类型时就会不得不把所有的成员变量都定为可选值了。)

Codable 的进阶运用

 上面的嵌套、数组类型的成员变量、可选的成员变量、Json 字符串自身是模型数组、空目标、空值等等,这些状况中都是采用了 Codable 的默许完成,咱们不需求增加什么自界说操作,Codable 自动帮咱们完结了数据到模型的转化。那有哪些需求咱们自界说的操作才干完结数据到模型的转化呢?下面一同来梳理一下。

 虽然 Codable 的默许完成满足敷衍大多数景象了,可是有时候咱们仍是存在一些自界说需求。为了处理这类自界说问题,咱们就必须自己掩盖 Codable 的一些默许完成。

  1. protocol Decoder 协议中 unkeyedContainer 的运用。
/// A type that can decode values from a native format into in-memory representations.
public protocol Decoder {
    ...
    /// Returns the data stored in this decoder as represented in a container appropriate for holding values with no keys.
    ///
    /// - returns: An unkeyed container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not an unkeyed container.
    func unkeyedContainer() throws -> UnkeyedDecodingContainer
    ...
}

 那么什么状况下咱们会遇到,不带键的数据呢?没错,大约便是根本类型构成的数组,例如上面示例中的经纬度坐标,直接把经纬度坐标放在一个数组中时:

let turtleRockString = """
    {
        ...
        "coordinates": [-116.166868, 34.011286],
        ...
    }
"""

 那么此刻咱们能够把 struct Landmark 结构体的 coordinates 成员变量的类型由 struct Coordinates 类型修正为 [Double] 数组,没错,这样的确也能正常解码,可是假如咱们便是想要运用 struct Coordinates 类型的 coordinates 呢,而且当数据回来的是经纬度的 Double 数组时,也能把经纬度正常解码到 struct Coordinates 结构体的 latitude 和 longitude 两个成员变量上,那么咱们能够如下修正 struct Coordinates 结构体:

    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
        init(from decoder: Decoder) throws {
            var contaioner = try decoder.unkeyedContainer()
            latitude = try contaioner.decode(Double.self)
            longitude = try contaioner.decode(Double.self)
        }
    }

 打印编码解码成果,可看到 turtleRock 和 encodeTurtleRockString 都正常打印了,且 encodeTurtleRockString 编码的字符串中,coordinates 是依据 struct Coordinates 结构体来编码的,假如咱们想 latitude 和 longitude 的值转回 Double 数组的话咱们需求自己重写 struct Coordinates 结构体的 func encode(to encoder: Encoder) throws 函数。

// print(turtleRock)
Landmark(id: 1001, name: "Turtle Rock", park: "Joshua Tree National Park", state: "California", description: "Suscipit ...", imageName: "turtlerock", coordinates: Landmarks.Landmark.Coordinates(latitude: -116.166868, longitude: 34.011286))
// print(encodeTurtleRockString)
{
  "coordinates" : {
    "longitude" : 34.011285999999998,
    "latitude" : -116.16686799999999
  },
  "id" : 1001,
  "park" : "Joshua Tree National Park",
  "description" : "Suscipit ...",
  "imageName" : "turtlerock",
  "state" : "California",
  "name" : "Turtle Rock"
}
  1. protocol Decoder 协议中 container 的运用。
/// A type that can decode values from a native format into in-memory representations.
public protocol Decoder {
    ...
    /// Returns the data stored in this decoder as represented in a container keyed by the given key type.
    ///
    /// - parameter type: The key type to use for the container.
    /// - returns: A keyed decoding container view into this decoder.
    /// - throws: `DecodingError.typeMismatch` if the encountered stored value is not a keyed container.
    func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey
    ...
}

 针对上述的 4 5 6 的状况,咱们能够经过把类型的成员变量定为可选类型即能够应对服务器回来 空目标/空值/字段缺失的状况,那么假如咱们便是不想运用可选类型,然后后续运用时的层层解包怎么处理呢?咱们能够如下重写 init(from decoder: Decoder) 函数,用 decoder.container(keyedBy: CodingKeys.self) 为指定的成员变量赋值:

struct Landmark: Hashable, Codable, Identifiable {
    ...
    private enum CodingKeys: String, CodingKey {
        case id
        case name
        case park
        case state
        case description
        case imageName
        case coordinates
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        park = try container.decode(String.self, forKey: .park)
        state = try container.decode(String.self, forKey: .state)
        description = try container.decode(String.self, forKey: .description)
        imageName = try container.decode(String.self, forKey: .imageName)
        do {
            coordinates = try container.decode(Coordinates.self, forKey: .coordinates)
//            coordinates = try container.decodeIfPresent(Coordinates.self, forKey: .coordinates)
        } catch {
            coordinates = Coordinates(latitude: 0, longitude: 0)
        }
    }
}

 这样当 coordinates 不管是回来 {}、null、直接不回来,都能正常解析(给一个默许值,这不是一个好办法,仍是运用可选,当没有回来是指定为 nil 比较好)。

let turtleRockString = """
    {
        ...
        "coordinates": null,
        // "coordinates": {},
    }
"""
  1. open class JSONDecoder 类的 open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy 特点的运用。(日期的转化策略)

 咱们经常需求需求跟日期打交道,日期数据可能以不同形式展现下发,最常见的日期规范是 ISO8601 和 RFC3339,举例来说:

1985-04-12T23:20:50.52Z          // 1
1996-12-19T16:39:57-08:00        // 2
1996-12-20T00:39:57Z             // 3
1990-12-31T23:59:60Z             // 4
1990-12-31T15:59:60-08:00        // 5
1937-01-01T12:00:27.87+00:20     // 6

 上面这些都是日期表明格局,可是只要第二个和第三个示例是 Swift 中 Codable 能够解码的,咱们首要来看怎么解码:

let turtleRockString = """
    {
        ...
        "updated":"2018-04-20T14:15:00-0700"
    }
"""
struct Landmark: Hashable, Codable, Identifiable {
    ...
    var updated: Date?
    ...
}
func decode<T: Decodable>(_ jsonString: String) -> T {
        ...
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        ...
}
// print(turtleRock) 打印中 updated 值如下:
updated: Optional(2018-04-20 21:15:00 +0000)

 JSONDecoder 供给了一个便利的机制能够解析日期类型,依据你的需求设置一下 dateDecodingStrategy 特点为 DateDecodingStrategy.iso8601 就能够解码契合规范(ISO8601 DateFormatter)的日期格局了。

 另一种常用的日期格局是时刻戳(timestamp),时刻戳是指格林威治时刻 1970 年 01 月 01 日 00 时 00 分 00 秒起至现在的总秒数。

let turtleRockString = """
    {
        ...
        "updated":1540650536
    }
"""
struct Landmark: Hashable, Codable, Identifiable {
    ...
    var updated: Date?
    ...
}
func decode<T: Decodable>(_ jsonString: String) -> T {
        ...
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .secondsSince1970
        ...
}
// print(turtleRock) 打印中 updated 值如下:
 updated: Optional(2018-10-27 14:28:56 +0000)

 解码时刻戳格局日期需求将 JSONDecoder 的 dateDecodingStrategy 设置为 DateDecodingStrategy.secondsSince1970(秒为单位)或 DateDecodingStrategy.millisecondsSince1970(毫秒为单位)。

 那么假如不是方才说到的能够默许支撑的解码格局怎么办?JSONDecoder 目标也供给了定制化办法:咱们以前面说到的第一种格局为例,1985-04-12T23:20:50.52Z,经过扩展 DateFormatter 界说一个新的 iso8601Full,把这个作为参数传入 dateDecodingStrategy。

extension DateFormatter {
    static let iso8601Full: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
        formatter.calendar = Calendar(identifier: .iso8601)
        formatter.timeZone = TimeZone(secondsFromGMT: 0)
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter
    }()
}
...
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
...

 decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full) 供给一个定制化的日期格局东西,咱们能够依据需求定制日期的解码格局。Swift之Codable实战技巧

 dateDecodingStrategy 特点在 JSONDecoder 类中的相关信息如下:

open class JSONDecoder {
    /// The strategy to use for decoding `Date` values.
    public enum DateDecodingStrategy {
        /// Defer to `Date` for decoding. This is the default strategy.
        case deferredToDate
        /// Decode the `Date` as a UNIX timestamp from a JSON number.
        case secondsSince1970
        /// Decode the `Date` as UNIX millisecond timestamp from a JSON number.
        case millisecondsSince1970
        /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format).
        @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
        case iso8601
        /// Decode the `Date` as a string parsed by the given formatter.
        case formatted(DateFormatter)
        /// Decode the `Date` as a custom value decoded by the given closure.
        case custom((_ decoder: Decoder) throws -> Date)
    }
    ...
    /// The strategy to use in decoding dates. Defaults to `.deferredToDate`.
    open var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
    ...
}    
  1. open class JSONDecoder 类的 open var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy 特点的运用。(体系供给的变量名从蛇形指令法到小驼峰命名法的自动转化)

 Web 服务中运用 Json 时一般运用蛇形命名法(snake_case_keys),把称号转化为小写字符串,并用下划线(_)代替空格来连接这些字符,与此不同的是 Swift API 规划指南中预先把对类型的转化界说为 UpperCamelCase(大驼峰命名),其他所有东西都界说为 lowerCamelCase(小驼峰命名)。因为这种需求非常遍及,在 Swift 4.1 时 JSONDecoder 增加了 keyDecodingStrategy 特点,能够在不同的书写惯例之间便利地转化。假如有这样的键值 image_Name,就会转化成 imageName。Swift之Codable实战技巧

 如上实例代码中,构建 JSONDecoder 目标后直接指定其 keyDecodingStrategy 特点。(键解码策略)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

 keyDecodingStrategy 特点在 JSONDecoder 类中的相关信息如下:

open class JSONDecoder {
    ...
    /// The strategy to use for automatically changing the value of keys before decoding.
    public enum KeyDecodingStrategy {
        /// Use the keys specified by each type. This is the default strategy.
        case useDefaultKeys
        /// Convert from "snake_case_keys" to "camelCaseKeys" before attempting to match a key with the one specified by each type.
        /// 
        /// The conversion to upper case uses `Locale.system`, also known as the ICU "root" locale. This means the result is consistent regardless of the current user's locale and language preferences.
        ///
        /// Converting from snake case to camel case:
        /// 1. Capitalizes the word starting after each `_`
        /// 2. Removes all `_`
        /// 3. Preserves starting and ending `_` (as these are often used to indicate private variables or other metadata).
        /// For example, `one_two_three` becomes `oneTwoThree`. `_one_two_three_` becomes `_oneTwoThree_`.
        ///
        /// - Note: Using a key decoding strategy has a nominal performance cost, as each string key has to be inspected for the `_` character.
        case convertFromSnakeCase
        /// Provide a custom conversion from the key in the encoded JSON to the keys specified by the decoded types.
        /// The full path to the current decoding position is provided for context (in case you need to locate this key within the payload). The returned key is used in place of the last component in the coding path before decoding.
        /// If the result of the conversion is a duplicate key, then only one value will be present in the container for the type to decode from.
        case custom((_ codingPath: [CodingKey]) -> CodingKey)
    }
    ...
    /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
    open var keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy
    ...
}

 可是还可能有特殊状况,Web 服务的开发者可能某些时候粗心了,也没有恪守蛇形命名法,而是很随意的处理了,那么假如咱们想对键值进行校正,该怎么处理?这就引出了下个点。

 关于 case custom((_ codingPath: [CodingKey]) -> CodingKey) 的运用,咱们能够参阅:Swift 4.1 新特性 (4) Codable的改进。

  1. 当遵照 Codable 协议的类型特点(成员变量)名和 Json 字符串中的字段名不同时,怎么进行自界说匹配映射。

 解决办法是:运用 CodingKeys 枚举指定一个明确的映射。

 Swift 会寻找契合 CodingKey 协议的名为 CodingKeys 的子类型(如下枚举类型)。这是一个标记为 private 的枚举类型,关于称号不匹配的键对应的枚举值指定一个明确的 String 类型的原始值,如下:

struct Landmark: Hashable, Codable, Identifiable {
    ...
    private enum CodingKeys: String, CodingKey {
        case id
        case name
        case park
        case state
        case description
        case imageName = "imageNameXXX"
        case coordinates
    }
}

 如上默许会把 Json 字符串中 imageNameXXX 字段的值指定给 struct Landmark 实例的 imageName 成员变量,其它成员变量值的话还运用 Json 字符串中一一对应的字段值。

 上面示例代码中,咱们修正 turtleRockString 字符串中的 imageName 字段值为 imageNameXXX,然后进行解码编码:

let turtleRockString = """
    {
        ...
        "imageNameXXX": "turtlerock",
        ...
    }
"""

 打印编码解码成果,看到 “imageNameXXX”: “turtlerock” 的值解码到 Landmark 结构体实例的 imageName 成员变量中,然后在 encodeTurtleRockString 字符串中,看到编码的默许完成亦是把 Landmark 结构体实例的 imageName 成员变量编码为:”imageNameXXX” : “turtlerock”。

// print(turtleRock)
Landmark(id: 1001, name: "Turtle Rock", park: "Joshua Tree National Park", state: "California", description: "Suscipit ...", imageName: "turtlerock", coordinates: Landmarks.Landmark.Coordinates(latitude: 34.011286, longitude: -116.166868))
// print(encodeTurtleRockString)
{
  "name" : "Turtle Rock",
  "id" : 1001,
  "park" : "Joshua Tree National Park",
  "description" : "Suscipit ...",
  "coordinates" : {
    "longitude" : -116.16686799999999,
    "latitude" : 34.011285999999998
  },
  "state" : "California",
  "imageNameXXX" : "turtlerock"
}
  1. 由 字符串/整型 转化为枚举类型。

 在 TableView 的列表中咱们经常会遇到不同类型的 cell,例如:图片、视频、超链接等等类型,然后针对不同的类型,服务端一般会给咱们回来一个类型的字符串,如:pic、video、link,乃至直接回来数字:1、2、3 这样,而在代码中运用时,咱们一般更希望将这种类型字符串(整型数字)转化成枚举值,便利运用。下面举两个简略的例子来说明怎么从字符串或许整型数据转化成枚举类型。

let turtleRockString = """
    {
        ...
        "template": "video", // 也可能是:pic、link
        ...
    }
"""

 template 代表当时 Json 字符串模型的类型,其值是一个字符串类型:

struct Landmark: Hashable, Codable, Identifiable {
    ...
    var template: Template
    ...
    enum Template: String, Codable {
        case VIDEO = "video"
        case PIC = "pic"
        case LINK = "link"
    }
}

 咱们在 struct Landmark 结构体内部嵌套界说一个 enum Template 枚举,它的原始值是 String 类型,而且遵照 Codable 协议,列举出所有可能的类型和对应的字符串值,然后在 struct Landmark 结构体中界说 template 成员变量的类型为 Template 枚举,Codable 就能够自动完结从字符串到枚举类型的转化。

 打印编码解码成果,看到 template 的值是:Template.VIDEO,编码成果中 “template” : “video” 也正常编码。

// print(turtleRock)
Landmark(id: 1001, name: "Turtle Rock", park: "Joshua Tree National Park", state: "California", description: "Suscipit ...", imageName: "turtlerock", template: Landmarks.Landmark.Template.VIDEO, coordinates: Landmarks.Landmark.Coordinates(latitude: 34.011286, longitude: -116.166868))
// print(encodeTurtleRockString) 
{
  "template" : "video",
  "coordinates" : {
    "longitude" : -116.16686799999999,
    "latitude" : 34.011285999999998
  },
  "id" : 1001,
  "park" : "Joshua Tree National Park",
  "description" : "Suscipit ...",
  "imageName" : "turtlerock",
  "state" : "California",
  "name" : "Turtle Rock"
}

 相同,假如 template 的值是整型数字的话,咱们只需求把 enum Template 枚举值指定为对应的原始值即可,如下修正:

struct Landmark: Hashable, Codable, Identifiable {
    ...
    var template: Template
    ...
    enum Template: Int, Codable {
        case VIDEO = 1
        case PIC = 2
        case LINK = 3
    }
}
  1. 别的还有一些 扁平化目标、目标承继 等的特殊处理,能够参阅:Swift 4 JSON 解析进阶。

  2. 还有一种状况,当服务器回来的字段类型和咱们预界说的模型的类型不匹配时,也会解码失利!需求处理可参阅:针对 swift4 的JSONDecoder的特殊状况处理。

 原本学习 SwiftUI 的跑题跑到 Codable 跑太远了,那干脆就跑远吧。这时咱们在看一下 struct Landmark: Hashable, Codable, Identifiable { ... } 可看到,Landmark 还遵照 Hashable 协议。

Hashable

 只要遵照了 Hashable 协议 才干被增加到 Set 中,或许用作 Dictionary 的 key 值。

public protocol Hashable : Equatable {
    /// The hash value.
    ///
    /// Hash values are not guaranteed to be equal across different executions of your program. Do not save hash values to use during a future execution.
    ///
    /// - Important: `hashValue` is deprecated as a `Hashable` requirement. To conform to `Hashable`, implement the `hash(into:)` requirement instead.
    var hashValue: Int { get }
    /// Hashes the essential components of this value by feeding them into the given hasher.
    ///
    /// Implement this method to conform to the `Hashable` protocol.
    /// The components used for hashing must be the same as the components compared in your type's `==` operator implementation.
    /// Call `hasher.combine(_:)` with each of these components.
    ///
    /// - Important: Never call `finalize()` on `hasher`. Doing so may become a compile-time error in the future.
    ///
    /// - Parameter hasher: The hasher to use when combining the components of this instance.
    func hash(into hasher: inout Hasher)
}

 例如咱们想运用咱们的自界说 Class 作为 Dictionary 的 key 的话,咱们就需求自己完成 Hashable 协议。

Identifiable

 只需求有一个 id 特点即可,在这儿用于指示 landmarks 数组中的 Landmarks 实例与 List 的每个 LandmarkRow 绑在一同。

/// A class of types whose instances hold the value of an entity with stable identity.
///
/// Use the `Identifiable` protocol to provide a stable notion of identity to a class or value type. 
/// For example, you could define a `User` type with an `id` property that is stable across your app and your app's database storage. 
/// You could use the `id` property to identify a particular user even if other data fields change, such as the user's name.
///
/// `Identifiable` leaves the duration and scope of the identity unspecified.
/// Identities could be any of the following:
///
/// - Guaranteed always unique (e.g. UUIDs).
/// - Persistently unique per environment (e.g. database record keys).
/// - Unique for the lifetime of a process (e.g. global incrementing integers).
/// - Unique for the lifetime of an object (e.g. object identifiers).
/// - Unique within the current collection (e.g. collection index).
///
/// It is up to both the conformer and the receiver of the protocol to document the nature of the identity.
///
/// Conforming to the Identifiable Protocol
/// =======================================
///
/// `Identifiable` provides a default implementation for class types (using
/// `ObjectIdentifier`), which is only guaranteed to remain unique for the lifetime of an object.
/// If an object has a stronger notion of identity, it may be appropriate to provide a custom implementation.
@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Identifiable {
    /// A type representing the stable identity of the entity associated with an instance.
    associatedtype ID : Hashable
    /// The stable identity of the entity associated with this instance.
    var id: Self.ID { get }
}

ModelData.swift

 上面 struct Landmark 结构体的内容看完了,接下来便是 ModelData 中读取 landmarkData.json 文件中的 Json 字符串,然后转化为 Landmark(数组)强类型数据。load 函数是 Codable 的一个最根底的运用。

var landmarks: [Landmark] = load("landmarkData.json")
func load<T: Decodable>(_ filename: String) -> T {
    let data: Data
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
    else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
    }
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

 这儿调用 load 函数的范型 T 是 [Landmark] Landmark 结构体数组,即履行 return try decoder.decode(T.self, from: data) 时其实履行的是 return try decoder.decode([Landmark].self, from: data),与咱们上面学习 Codable 时的 3. Json 字符串是一个模型数组时 状况相同。

 假如咱们把 var landmarks: [Landmark] = load("landmarkData.json") 修正为 var landmarks = load("landmarkData.json") 则会报:Generic parameter ‘T’ could not be inferred。

 这儿数据部分就预备好了,下面咱们接着看视图部分。

LandmarkRow.swift

 LandmarkRow 姑且能够理解为咱们在 UIKit 中运用的 UITableViewCell,用于展现列表中的一行数据。

 首要为 LandmarkRow 视图增加一个 Landmark 类型的存储特点,记载用于展现的数据。

import SwiftUI
struct LandmarkRow: View {
    // Add landmark as a stored property of LandmarkRow.
    var landmark: Landmark
    var body: some View {
        // Embed the existing text view in an HStack,Modify the text view to use the landmark property’s name.
        HStack {
            // Complete the row by adding an image before the text view, and a spacer after it.
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

LandmarkList.swift

 Instead of specifying a list’s elements individually, you can generate rows directly from a collection. 能够直接从调集生成行,而不是独自指定列表的元素。

 经过传递数据调集(landmarks 数组)和为调会集的每个元素供给视图的闭包,能够创立一个显现调集元素的列表(List)。该列表经过运用供给的闭包将调会集的每个元素转化为子视图。

 这儿为列表中的每个 LandmarkRow 说到了 \.id,它有点相似与咱们在 UIKit 中运用 UITableView 时为 cell 指定标识,可是又不同,一类 cell 指定的是同一个标识,而这儿的别的一种简练办法是从数据源动身的,需求 Landmark 结构体遵照 Identifiable 协议,而遵照 Identifiable 协议的类型需求有一个名为 id 的特点,而咱们的 Landmark 结构体正契合此要求。

import SwiftUI
struct LandmarkList: View {
    var body: some View {
        // 在 NavigationView 中嵌入动态生成的地标列表。
        NavigationView {
            List(landmarks) { landmark in
                // Inside the list’s closure, wrap the returned row in a NavigationLink, specifying the LandmarkDetail view as the destination.
                NavigationLink {
                    LandmarkDetail(landmark: landmark)
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            // 调用 navigationTitle(_:) 修饰符办法,用于在显现列表时设置导航栏的标题
            .navigationTitle("Landmarks")
        }
    }
}

LandmarkDetail.swift

 别离修正 MapView 和 CircleImage 中上一节为它们硬编码的坐标和图片,这儿别离传递给它们 landmark 的 locationCoordinate 和 image。

import SwiftUI
struct LandmarkDetail: View {
    var landmark: Landmark
    var body: some View {
        // 将容器从 VStack 更改为 ScrollView,以便用户能够翻滚阅读描述性内容,并删除不再需求的间隔符。
        ScrollView {
            MapView(coordinate: landmark.locationCoordinate)
                .ignoresSafeArea(edges: .top)
                .frame(height: 300)
            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)
            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)
                HStack {
                    Text(landmark.park)
                    Spacer()
                    Text(landmark.state)
                }
                .font(.subheadline)
                .foregroundColor(.secondary)
                Divider()
                Text("About \(landmark.name)")
                    .font(.title2)
                Text(landmark.description)
            }
            .padding()
        }
        // 最终,调用  navigationTitle(_:) 修饰符,用于在显现详细信息视图时为导航栏指定标题,以及 navigationBarTitleDisplayMode(_:) 修饰符,使标题以内联办法显现。仅当视图是导航仓库的一部分时,导航更改才会收效。
        .navigationTitle(landmark.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

MapView.swift

 增加一个根据坐标值更新 region 的办法:setRegion,将 onAppear 视图修饰符增加到 Map 中,以触发根据当时 coordinate 的区域核算。

import SwiftUI
import MapKit
struct MapView: View {
    var coordinate: CLLocationCoordinate2D
    @State private var region = MKCoordinateRegion()
    var body: some View {
        Map(coordinateRegion: $region)
            // Add an onAppear view modifier to the map that triggers a calculation of the region based on the current coordinate.
            .onAppear {
                setRegion(coordinate)
            }
    }
    // Add a method that updates the region based on a coordinate value.
    private func setRegion(_ coordinate: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(
            center: coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    }
}

 至此 BuildingListsAndNavigation 中的内容就悉数看完了,虽然重心都放在了 Codable 上,可是它的确至关重要,下节咱们持续把目光首要会集到 SwiftUI 的代码中去。

参阅链接

参阅链接:

  • [SwiftUI 知识碎片] 为什么 SwiftUI 用 “some View” 作为视图类型?
  • SwiftUI 中的 some 关键字
  • Opaque Types
  • SwiftUI状况绑定:@State
  • [译]理解 SwiftUI 里的特点装饰器@State, @Binding, @ObservedObject, @EnvironmentObje
  • SwiftUI为啥能够这样写代码?
  • swift–Codable
  • Swift 4.1 新特性 (4) Codable的改进
  • Swift 4.1 新特性 (3) 合成 Equatable 和 Hashable
  • SwiftUI 根底之06 Identifiable 有什么用
  • iOS开发 – Swift中的Codable, Hashable, CaseIterable, Identifiable…..
  • Swift之Codable实战技巧
  • Swift 4 JSON 解析进阶