Swift Codable 精华——手动decoder需要掌握知识点

注:代码基于Swift4.0

导读:Swift 4 现在可以支持很方便的转模型了。例:
  1. Book结构体 遵守Decodable协议
struct Book: Decodable {
    var title: String
    var author: String
    var rating: Float
}
  1. 自动解码
override func viewDidLoad() {
        super.viewDidLoad()
        
        
        let jsonString = """
{   "title": "War and Peace: A protocol oriented approach to diplomacy",
    "author": "A. Keed Decoder",
    "rating": 5.0
}
"""
        if let data = jsonString.data(using: .utf8) {
            let decoder = JSONDecoder()
            if let book = try? decoder.decode(Book.self, from: data) {
                print(book.title) //War and Peace: A protocol oriented approach to diplomacy
            } else {
                print("decode failed") 
            }
            
        }
    }
正题:老代码可能出现手动Decoder,那就探索一下吧。
一: 可选,解码用decodeIfPresent
struct Book: Decodable {
    var title: String
    var author: String
    var rating: Float?

    init(from decoder: Decoder) throws {
        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        title = try keyedContainer.decode(String.self, forKey: .title)
        author = try keyedContainer.decode(String.self, forKey: .author)
        rating =  try keyedContainer.decodeIfPresent(Float.self, forKey: CodingKeys.rating)
    }

    enum CodingKeys: String, CodingKey {
        case title
        case author
        case rating
    }
}
二: 非可选,但json数据为nil时解码失败怎么办(不想用可选,就设置个默认值吧)
struct Book: Decodable {
    var title: String
    var author: String
    var rating: Float

    init(from decoder: Decoder) throws {
        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        title = try keyedContainer.decode(String.self, forKey: .title)
        author = try keyedContainer.decode(String.self, forKey: .author)
        if let ratingValue = try keyedContainer.decodeIfPresent(Float.self, forKey: CodingKeys.rating) {
            rating = ratingValue
        } else {
            rating = 0
        }
    }

    enum CodingKeys: String, CodingKey {
        case title
        case author
        case rating
    }
}
三:decode失败, rating为非可选,但服务给的json偏偏就不返这个字段,或者返的这个字段为nil

客户端这么写rating

struct Book: Decodable {
    var title: String
    var author: String
    var rating: Float
    
    init(from decoder: Decoder) throws {
        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        title = try keyedContainer.decode(String.self, forKey: .title)
        author = try keyedContainer.decode(String.self, forKey: .author)
        rating = try keyedContainer.decode(Float.self, forKey: .rating)
    }

    enum CodingKeys: String, CodingKey {
        case title
        case author
        case rating
    }
}

服务端就想这么给数据

        let jsonString = """
{   "title": "War and Peace: A protocol oriented approach to diplomacy",
    "author": "A. Keed Decoder",
    "rating":
}
"""

或者这么返

        let jsonString = """
{   "title": "War and Peace: A protocol oriented approach to diplomacy",
    "author": "A. Keed Decoder",
}
"""

好的。 print("decode failed")
失败倒也没啥,反正不是崩溃,但是Fabric上面捕获到N多条Non-Fatals log(就是研究这个log才有了这篇记录)。 当然还是要处理啦。 所以尽量用可选,或者不想给可选至少也给个默认值吧。

四:如何通过Crashlytics捕获异常。

在book结构体中加一个出版日期属性

struct Book: Decodable {
    var title: String
    var author: String
    var rating: Float
    let publishedAt: Date
    
    private static func dateDecode(_ container: KeyedDecodingContainer, key: CodingKeys) throws -> Date {
        let date: Date
        do {
            date = try container.decode(Date.self, forKey: key)
        }
        catch {
            date = Date(timeIntervalSince1970: 0)
            let dateAtString = try container.decode(String.self, forKey: key)
            let bookTitle = try container.decode(String.self, forKey: CodingKeys.title)
            
            let error = CrashlyticsError.bookDateParsingFailed(codingPath: key,
                                                               bookTitle: bookTitle,
                                                                dateValue: dateAtString)
            print(error)
//            Crashlytics.sharedInstance().recordError(error) //Fabric 可以直接记录
        }
        
        return date
    }
    
    init(from decoder: Decoder) throws {
        let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
        title = try keyedContainer.decode(String.self, forKey: .title)
        author = try keyedContainer.decode(String.self, forKey: .author)
        rating = try keyedContainer.decode(Float.self, forKey: .rating)
        publishedAt = try Book.dateDecode(keyedContainer, key: CodingKeys.publishedAt)
    }

    enum CodingKeys: String, CodingKey {
        case title
        case author
        case rating
        case publishedAt
    }
    
//string 转date会用到
    static func dateFormatter() -> DateFormatter {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
        return dateFormatter
    }
}

加一个捕获异常CrashlyticsError文件

import UIKit

enum CrashlyticsError: CustomNSError {
    case bookParsingFailed(codingPath: [CodingKey], debugDescription: String)
    case bookDateParsingFailed(codingPath: CodingKey, bookTitle: String, dateValue: String)
    
    static var errorDomain: String {
        return "XXDecoderDemo"
    }
    
    var errorCode: Int {
        switch self {
        case .bookParsingFailed(_, _):
            return 7780
        case .bookDateParsingFailed(_,_,_):
            return 7781
        }
    }
    
    var errorUserInfo: [String : Any] {
        switch self {
        case .bookParsingFailed(let codingPath, let debugDescription):
            var userInfo = [NSLocalizedDescriptionKey :  "Book Parsing",
                            NSLocalizedFailureReasonErrorKey : "Can't parse",
                            "Description" : debugDescription]
            for (index, element) in codingPath.enumerated() {
                userInfo["Coding Key \(index)"] = element.stringValue
            }
            return userInfo
            
        case .bookDateParsingFailed(let codingPath, let bookTitle, let dateValue):
            return [NSLocalizedDescriptionKey :  "Book Date Parsing",
                    NSLocalizedFailureReasonErrorKey : "Can't parse \(codingPath.stringValue)",
                "Book Title"   : bookTitle,
                "Date value" : dateValue]
        }
    }
}

viewDidLoad代码。这里publishedAt的jsonString故意写成错的格式,然后进入到捕获异常程序。正常格式应是:2018-01-01T00:00:00.000Z(和dateFormatter保持一致)

override func viewDidLoad() {
        super.viewDidLoad()
        
        
        let jsonString = """
{   "title": "War and Peace: A protocol oriented approach to diplomacy",
    "author": "A. Keed Decoder",
    "rating": 5.0,
    "publishedAt": "019-04-16T9:24:37TPM.000Z"

}
"""
        if let data = jsonString.data(using: .utf8) {
            let decoder = JSONDecoder()
            decoder.dateDecodingStrategy = .formatted(Book.dateFormatter())
            if let book = try? decoder.decode(Book.self, from: data) {
                print(book.title) //War and Peace: A protocol oriented approach to diplomacy
            } else {
                print("decode failed")
            }
            
        }
    }

可想而知,最终解码不会失败,还是会打印出书名。

总结:虽然获取到的publishedAt是错的格式,转化成date会不成功,但是进行了异常捕获,在catch代码块中,将date设置成date = Date(timeIntervalSince1970: 0),也相当于是设置默认值了。解码不失败, 同时还记录了为什么没有转化成功的log。 可以快速定位到出问题的那条信息。 完美!

捕获到的error信息如下(Fabric记录的话,会更加清晰):

print(error)  //bookDateParsingFailed(codingPath: CodingKeys(stringValue: "publishedAt", intValue: nil), bookTitle: "War and Peace: A protocol oriented approach to diplomacy", dateValue: "019-04-16T9:24:37TPM.000Z")

空了会将demo上传。

每天学习一点点,加油⛽️!

你可能感兴趣的:(Swift Codable 精华——手动decoder需要掌握知识点)