Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Decoder(), KeyDecodingStrategy, DateDecodingStrategy 사용 방법 (Date 여러 형태 처리) 본문

iOS 응용 (swift)

[iOS - swift] Decoder(), KeyDecodingStrategy, DateDecodingStrategy 사용 방법 (Date 여러 형태 처리)

jake-kim 2022. 1. 16. 15:14

알아야하는 개념 - CodingKeys란?

  • 인코딩과 디코딩할때 사용하는 key값

  • Codable 타입은 CodingKeys라는 nested type인 CodingKeys를 정의해야 하는데, 이 CodingKeys는 CodingKey타입을 준수
    • CodingKey에 case문으로 key값들을 정의하고 Person이라는 구조체를 json으로 변경할때 이 값을 내부적으로 이용
struct Person: Codable {
    enum CodingKeys: String, CodingKey {
        case name
        case age
    }
    
    var name: String
    var age: Int
}

KeyDocodingStrategy

  • json의 key값을 coding keys로 어떻게 decoding할 것인지 정의하는 프로퍼티

  • CodingKeys에 정의된 case와 동일하게 매핑시켜주는 역할
    • 아래 코드에서 json을 Person으로 매핑 방법? -> Person: Codable에 rawValue로 입력해주는 방법도 있지만, keyDecodingStrategy를 이용한 방법도 존재
      struct Person: Codable {
        var personname: String
        var personage: Int
      }​
      
      let json = """
      {
      "person-name": "jake",
      "person-age": 10
      }
      """
  • keyDecodingStrategy를 이용한 방법
    • JSONDecoder.KeyDecodingStrategy를 커스텀
      private let myKeyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = {
        .custom { codingPath in
          print(codingPath)
          let key = codingPath.last!.stringValue.split(separator: "-").joined()
          print("key = \(key)") // personname, personage
          return MyCodingKey(stringValue: key)!
        }
      }()​
    • JSONDecoder에 위에서 정의한 프로퍼티 keyDeocodingStrategy 사용
      private var myDecoder: JSONDecoder {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = myKeyDecodingStrategy
        return jsonDecoder
      }​
    • 위에서 정의한 decoder사용하여 decoding
      private func decoder(_ json: String) throws -> Person {
        do {
          let person = try self.myDecoder.decode(Person.self, from: Data(json.utf8))
          return person
        } catch {
          throw DecoderError.error
        }
      }​
      
      do {
        let person = try self.decoder(json)
        print(person) // Person(personname: "jake", personage: 10)
      } catch {
        print(error)
      }
  • keyDecodingStrategy 전체 코드
    import UIKit
    
    enum DecoderError: Error {
      case error
    }
    
    struct Person: Codable {
      var personname: String
      var personage: Int
    }
    
    class ViewController: UIViewController {
      override func viewDidLoad() {
        super.viewDidLoad()
        
        let json = """
        {
        "person-name": "jake",
        "person-age": 10
        }
        """
        
        do {
          let person = try self.decoder(json)
          print(person)
        } catch {
          print(error)
        }
      }
      
      private let myKeyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = {
        .custom { codingPath in
          print(codingPath)
          let key = codingPath.last!.stringValue.split(separator: "-").joined()
          print("key = \(key)") // personname, personage
          return MyCodingKey(stringValue: key)!
        }
      }()
      
      private var myDecoder: JSONDecoder {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.keyDecodingStrategy = myKeyDecodingStrategy
        return jsonDecoder
      }
      
      private func decoder(_ json: String) throws -> Person {
        do {
          let person = try self.myDecoder.decode(Person.self, from: Data(json.utf8))
          return person
        } catch {
          throw DecoderError.error
        }
      }
    }
    
    struct MyCodingKey: CodingKey {
      var stringValue: String
      var intValue: Int?
      
      init?(stringValue: String) {
        self.stringValue = stringValue
      }
      
      init?(intValue: Int) {
        self.intValue = intValue
        self.stringValue = String(intValue)
      }
    }​

DateDecodingStrategy

  • json으로 내려오는 date 정보를 formating할때 사용되는 값

  • 아래처럼 date 포멧이 여러개로 내려오는 경우, 파싱을 위해 사용
    let json = """
    {
        "date1": "2021-12-30 15:18:00",
        "date2": "2022-01-30T05:18:00",
        "date3": "2019-12-17"
    }
    """​
  • 일반적으로 json없이 date 문자열을 Date 객체로 인코딩하는 방법은 DateFormatter 객체를 사용
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // format: ISO8601DateFormatter
    
    let date = dateFormatter.date(from: "2013-07-21T19:32:00Z")
    print(date) // Optional(2013-07-21 19:32:00 +0000)​
  • json date를 파싱할때도 DateFormatter 객체가 사용되므로 프로퍼티로 선언
    private let myDateFormatter: DateFormatter = {
      let dateFormatter = DateFormatter()
      dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // format: ISO8601DateFormatter
      return dateFormatter
    }()​
  • JsonDecoder에서 위에서 선언한 dateForamtter 사용
    private var myDecoder: JSONDecoder {
      let decoder = JSONDecoder()
      decoder.dateDecodingStrategy = .custom { decoder in
        let container = try decoder.singleValueContainer()
        let dateString = try container.decode(String.self)
        guard let date = self.myDateFormatter.date(from: dateString) else {
          throw DecodingError.dataCorruptedError(in: container, debugDescription: "cannot decoding \(dateString)")
        }
        return date
      }
      return decoder
    }​
  • 사용
    let json = """
    {
      "date": "2013-07-21T19:32:00Z"
    }
    """
    let jsonData = json.data(using: .utf8)!
    let date = try! self.myDecoder.decode(MyDate.self, from: jsonData)
    print(date) // MyDate(date: 2013-07-21 19:32:00 +0000)​
  • dateDecodingStrategy 전체 코드
    import UIKit
    
    struct MyDate: Codable {
      var date: Date
    }
    
    class ViewController: UIViewController {
      
      private let myDateFormatter: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" // format: ISO8601DateFormatter
        return dateFormatter
      }()
      
      private var myDecoder: JSONDecoder {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .custom { decoder in
          let container = try decoder.singleValueContainer()
          let dateString = try container.decode(String.self)
          guard let date = self.myDateFormatter.date(from: dateString) else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "cannot decoding \(dateString)")
          }
          return date
        }
        return decoder
      }
      
      override func viewDidLoad() {
        super.viewDidLoad()
        
        let json = """
        {
          "date": "2013-07-21T19:32:00Z"
        }
        """
        let jsonData = json.data(using: .utf8)!
        let date = try! self.myDecoder.decode(MyDate.self, from: jsonData)
        print(date) // MyDate(date: 2013-07-21 19:32:00 +0000)
      }
    }​

date 디코딩 응용 - 여러 형태의 date 문자열 처리

  • 아래처럼 date 포멧이 여러개가 내려오는 경우 처리 방법
let json = """
{
  "date1": "2022-07-21T19:32:00Z",
  "date2": "2022-01-03"
}
"""
  • JSONDecoder에 extension으로 DateFormatter 배열 추가: for문을 돌면서 인수로 들어온 formatter로 디코딩되면 반환
    // https://stackoverflow.com/questions/44682626/swifts-jsondecoder-with-multiple-date-formats-in-a-json-string
    
    extension JSONDecoder {
      var dateDecodingStrategyFormatters: [DateFormatter]? {
        @available(*, unavailable, message: "This variable is meant to be set only")
        get { return nil }
        set {
          guard let formatters = newValue else { return }
          self.dateDecodingStrategy = .custom { decoder in
            
            let container = try decoder.singleValueContainer()
            let dateString = try container.decode(String.self)
            
            for formatter in formatters {
              if let date = formatter.date(from: dateString) {
                return date
              }
            }
            
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
          }
        }
      }
    }
  • DateFormatter에 extension으로 형식 정의
    extension DateFormatter {
      static let myISO8601DateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
        return dateFormatter
      }()
      
      static let myDateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        return dateFormatter
      }()
    }​
  • 사용
    let json = """
    {
      "date1": "2022-07-21T19:32:00Z",
      "date2": "2022-01-03"
    }
    """
    let jsonData = json.data(using: .utf8)!
    let dates = try! self.myJsonDecoder.decode(MyDate.self, from: jsonData)
    print(dates) // MyDate(date1: 2022-07-21 19:32:00 +0000, date2: 2022-01-02 15:00:00 +0000)​
  • 전체 코드
    import UIKit
    
    struct MyDate: Codable {
      var date1: Date
      var date2: Date
    }
    
    class ViewController: UIViewController {
      
      private let myJsonDecoder: JSONDecoder = {
        let jsonDecoder = JSONDecoder()
        jsonDecoder.dateDecodingStrategyFormatters = [
          DateFormatter.myISO8601DateFormatter,
          DateFormatter.myDateFormatter
        ]
        return jsonDecoder
      }()
      
      override func viewDidLoad() {
        super.viewDidLoad()
        
        let json = """
        {
          "date1": "2022-07-21T19:32:00Z",
          "date2": "2022-01-03"
        }
        """
        let jsonData = json.data(using: .utf8)!
        let dates = try! self.myJsonDecoder.decode(MyDate.self, from: jsonData)
        print(dates) // MyDate(date1: 2022-07-21 19:32:00 +0000, date2: 2022-01-02 15:00:00 +0000)
      }
    }
    
    extension JSONDecoder {
      var dateDecodingStrategyFormatters: [DateFormatter]? {
        @available(*, unavailable, message: "This variable is meant to be set only")
        get { return nil }
        set {
          guard let formatters = newValue else { return }
          self.dateDecodingStrategy = .custom { decoder in
            
            let container = try decoder.singleValueContainer()
            let dateString = try container.decode(String.self)
            
            for formatter in formatters {
              if let date = formatter.date(from: dateString) {
                return date
              }
            }
            
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "Cannot decode date string \(dateString)")
          }
        }
      }
    }
    
    extension DateFormatter {
      static let myISO8601DateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
        return dateFormatter
      }()
      
      static let myDateFormatter: DateFormatter = {
        var dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd"
        return dateFormatter
      }()
    }​

* 참고

https://stackoverflow.com/questions/44682626/swifts-jsondecoder-with-multiple-date-formats-in-a-json-string

https://developer.apple.com/documentation/swift/codingkey

https://developer.apple.com/documentation/foundation/jsondecoder/2949119-keydecodingstrategy

Comments