Giới thiệu

Làm cách nào để bạn decode một file JSON có nhiều định dạng ngày khác nhau? Nếu bạn may mắn bằng cách sử dụng một dateDecodingStrategy có thể là đủ. Thật không may, nó có hỗ trợ giới hạn cho định dạng .iso8601 và bạn chỉ có thể thiết lập một giải pháp tại một thời điểm vì vậy nó không giúp đỡ khi bạn có hai hoặc nhiều định dạng ngày khác nhau. Vậy chúng ta phải xử lý các định dạng khác nhau trong cùng một file JSON như thế nào, hôm nay tôi sẽ giới thiệu các bạn một giải pháp mà không cần phải sử dụng library hỗ trợ bên ngoài, mà nó sẽ được hỗ trợ trên swift 4.

Ví dụ về iTunes RSS

Ở ví dụ của chúng ta sử dụng file JSON như sau:

// https://rss.itunes.apple.com/api/v1/gb/podcasts/top-podcasts/all/3/explicit.json
let json = """
{
  "feed": {
    "title":"Top Audio Podcasts",
    "country":"gb",
    "updated":"2017-11-16T02:02:55.000-08:00",
    "results":[
      {
      "artistName":"BBC Radio",
      "name":"Blue Planet II: The Podcast",
      "releaseDate":"2017-11-12",
      "url":"https://itunes.apple.com/gb/podcast/blue-planet-ii-the-podcast/id1296222557?mt=2"
    },
    {
      "artistName":"Audible",
      "name":"The Butterfly Effect with Jon Ronson",
      "releaseDate":"2017-11-03",
      "url":"https://itunes.apple.com/gb/podcast/the-butterfly-effect-with-jon-ronson/id1258779354?mt=2"
    },
    {
      "artistName":"TED",
      "name":"TED Talks Daily",
      "releaseDate":"2017-11-16",
      "url":"https://itunes.apple.com/gb/podcast/ted-talks-daily/id160904630?mt=2"
    }
    ]
  }
}
"""

Chú ý rằng ở đây có ngày 2 định dạng ngày khác nhau. Đầu tiên là một timestamp sử dụng cho lastupdated. Đây là định dạng quen thuộc iso8601:

"updated":"2017-11-16T02:02:55.000-08:00",

Thứ hai là release date format đơn giản yyyy-mm-dd

"releaseDate":"2017-11-12",

Swift Codable – A Recap

Swift 4 mang lại cho chúng ta một cách chuẩn hóa để encode / decode JSON sử dụng các loại tùy chỉnh của riêng chúng bằng cách sử dụng giao thức Codable. Các kiểu như String, URL và Date đã được Codable để chúng ta có thể sử dụng chúng để xây dựng các loại Codable cho podcast và feed. Chúng ta cũng có thể tổ chức các cấu trúc Swift để mô hình trực tiếp cấu trúc RSS Feed:

import Foundation

struct RSSFeed: Codable {
  struct Feed: Codable {
    struct Podcast: Codable {
      let name: String
      let artistName: String
      let url: URL
      let releaseDate: Date
    }

    let title: String
    let country: String
    let updated: Date
    let podcasts: [Podcast]

    private enum CodingKeys: String, CodingKey {
      case title
      case country
      case updated
      case podcasts = "results"
    }
  }

  let feed: Feed
}

typealias Feed = RSSFeed.Feed
typealias Podcast = Feed.Podcast

Để encode chuỗi JSON của chúng ta, chúng ta chuyển nó sang Data và nạp nó vào một JSONDecoder:

let data = Data(json.utf8)
let decoder = JSONDecoder()
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)

Thật không may khi ở đây chúng ta có 2 định dạng ngày.

Date Decoding Strategy

Bạn có thể thay đổi cách decode JSON xử lý ngày bằng cách sử dụng date decoding strategy. updated Feed có định dạng ngày tháng iso8601 được hỗ trợ nên chúng tôi có thể thử rằng:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

Chúng ta có thể sử dụng cách trên, tuy nhiên decodeingStategy của Foundation lại không hỗ trợ đến phần tử nhỏ của giây (02:55.000). Chúng ta có thể custome để có định dạng full format như sau:

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
  }()
}

Chú ý rằng .SSS trong date format, để sử dụng custom data formater khi decode JSON data:

let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)

Custom Transformation

Sử dụng dateDecodingStrategy tốt khi JSON data của bạn chỉ có một date format. Hãy cùng xem chúng ta decode Postcast xử lỹ releaseDate format (yyyy-MM-dd):

extension DateFormatter {    
  static let yyyyMMdd: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateFormat = "yyyy-MM-dd"
    formatter.calendar = Calendar(identifier: .iso8601)
    formatter.timeZone = TimeZone(secondsFromGMT: 0)
    formatter.locale = Locale(identifier: "en_US_POSIX")
    return formatter
  }()
}

Bây giờ chúng ta kế thừa required initializer init(from: Decoder) để xử lý custom date format:

extension Podcast {
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    name = try container.decode(String.self, forKey: .name)
    artistName = try container.decode(String.self, forKey: .artistName)
    url = try container.decode(URL.self, forKey: .url)

    let dateString = try container.decode(String.self, forKey: .releaseDate)
    let formatter = DateFormatter.yyyyMMdd
    if let date = formatter.date(from: dateString) {
        releaseDate = date
    } else {
        throw DecodingError.dataCorruptedError(forKey: .releaseDate,
              in: container,
              debugDescription: "Date string does not match format expected by formatter.")
    }
  }
}

Để decode full feed, chúng ta thực hiện như sau:

let data = Data(json.utf8)
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Full)
let rssFeed = try! decoder.decode(RSSFeed.self, from: data)

let feed = rssFeed.feed
print(feed.title, feed.country, feed.updated)

feed.podcasts.forEach {
  print($0.name)
}

Top Audio Podcasts gb 2017-11-16 10:02:55 +0000
Blue Planet II: The Podcast
The Butterfly Effect with Jon Ronson
TED Talks Daily

Get The Code

Bạn có thể lấy đoạn mã và dữ liệu mẫu JSON từ GitHub Gist này nếu bạn muốn thử nó trong playground.
Nguồn