티스토리 뷰

반응형

 

안녕하세요 :) Zedd입니다.

꼭 Swift가 릴리즈되면 변경사항을 공부 & 정리했었는데...

요번 Swift 5.5의 변경사항이 은근히 많다보니..

ㅎㅎ...ㅠㅠㅠ 정리를 못해서 아쉬웠는데, 그냥 하나씩 공부해보는 것도 나쁘지 않을 것 같아요 💪

오늘은 얼마전에 처음 봤던! 

Codable synthesis for enums with associated values에 대해서 공부해보려고 합니다. 

 

# Swift 5.5 이전

enum과 Codable을 함께 쓰기위해서는 

enum Status: String, Codable {

  case normal = "NORMAL"
  case rejected = "REJECTED"
  case deleted = "DELETED"
}

뭐 이런식으로 만들고는 했습니다. 

컴파일러가 enum을 rawValue로 Encoding / Decoding 하기 위한 코드를 자동으로 합성(automatically synthesize)하기 때문에 

여기서 추가적인 코드가 없어도 Encoding / Decoding에 문제가 없었죠.

예를들어

struct Response: Codable {
    
    let status: Status
    
    enum Status: String, Codable {

      case normal = "NORMAL"
      case rejected = "REJECTED"
      case deleted = "DELETED"
    }
}

let jsonString =
"""
    {"status": "NORMAL"}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.status) // normal

이런 Decoding을 문제없이 할 수 있었다는 겁니다. 

하지만

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
}

이렇게 associated Value가 있는 case가 있는 enum이 Codable을 준수하게 되면 

이렇게 에러가나죠!

에러가 나는 이유는 associated values가 있는 enum에 대해서 위에서 말한 automatically synthesize(자동 합성)을 지원하지 않기 때문입니다. 

그래서 추가로 코드를 작성해줘야했습니다. 

 

# Codable synthesis for enums with associated values

Swift 5.5에서는 associated values가 있는 enum에 대해 auto-synthesize(자동 합성)을 지원합니다.🥳 

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
}

위 코드가 컴파일에러를 일으키지않습니다.

 

[Encoding]

{
  "load": {
    "key": "zedd"
  }
}
{
  "store": {
    "key": "zedd",
    "value": 42
  }
}

 

[Decoding]

struct Response: Codable {
    
    let command: Command
    
    enum Command: Codable {

      case load(key: String)
      case store(key: String, value: Int)
    }
}

let jsonString =
"""
{
    "command": {
        "load": {
            "key": "zedd"
        }
    }
}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.command) // load(key: "zedd")

이런 것들이 가능해집니다.

 

컴파일러가 해주는 자동합성에 대해서 살짝 더 보겠습니다. 

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
}

이 enum을 딱 만들면 컴파일러가 자동으로 만들어주는 코드들이 있는데요. 

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
  case load
  case store
}

// contains keys for all associated values of `case load`
enum LoadCodingKeys: CodingKey {
  case key
}

// contains keys for all associated values of `case store`
enum StoreCodingKeys: CodingKey {
  case key
  case value
}

public func encode(to encoder: Encoder) throws {
  var container = encoder.container(keyedBy: CodingKeys.self)
  switch self {
  case let .load(key):
    var nestedContainer = container.nestedContainer(keyedBy: LoadCodingKeys.self, forKey: .load)
    try nestedContainer.encode(key, forKey: .key)
  case let .store(key, value):
    var nestedContainer = container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
    try nestedContainer.encode(key, forKey: .key)
    try nestedContainer.encode(value, forKey: .value)
  }
}


public init(from decoder: Decoder) throws {
  let container = try decoder.container(keyedBy: CodingKeys.self)
  if container.allKeys.count != 1 {
    let context = DecodingError.Context(
      codingPath: container.codingPath,
      debugDescription: "Invalid number of keys found, expected one.")
    throw DecodingError.typeMismatch(Command.self, context)
  }

  switch container.allKeys.first.unsafelyUnwrapped {
  case .load:
    let nestedContainer = try container.nestedContainer(keyedBy: LoadCodingKeys.self, forKey: .load)
    self = .load(
      key: try nestedContainer.decode(String.self, forKey: .key))
  case .store:
    let nestedContainer = try container.nestedContainer(keyedBy: StoreCodingKeys.self, forKey: .store)
    self = .store(
      key: try nestedContainer.decode(String.self, forKey: .key),
      value: try nestedContainer.decode(Int.self, forKey: .value))
  }
}

무려 이만큼의 코드를 컴파일러가 자동으로 생성해줍니다. (물론 내가 볼 수 있는건 아님)

주의깊게 보면 좋을 부분은

// contains keys for all cases of the enum
enum CodingKeys: CodingKey {
  case load
  case store
}

// contains keys for all associated values of `case load`
enum LoadCodingKeys: CodingKey {
  case key
}

// contains keys for all associated values of `case store`
enum StoreCodingKeys: CodingKey {
  case key
  case value
}

바로 요부분인데요.

LoadCodingKeysStoreCodingKeys가 만들어진다는 겁니다.

(이름이 저거다!라는게 아니라 case의 이름을 prefix로 붙힌 CodingKeys enum이 만들어진다는 뜻)

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
}

요걸 Encoding하면 

{
  "load": {
    "key": "zedd"
  }
}
{
  "store": {
    "key": "zedd",
    "value": 42
  }
}

대충 이런 json이 만들어졌잖아요?!

"key"와 "value"는 내 associated values의 레이블 이름이죠.

근데 내가 이걸 바꾸고싶어!

그러면 

// contains keys for all associated values of `case load`
enum LoadCodingKeys: CodingKey {
  case key
}

// contains keys for all associated values of `case store`
enum StoreCodingKeys: CodingKey {
  case key
  case value
}

이걸 내가 정의해주면 됩니다.

아 load 케이스일때는 "key"라는 associated value의 레이블이 "zedd"로 됐으면 좋겠어! 

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
  
  enum LoadCodingKeys: String, CodingKey {
    case key = "zedd"
  }
}

그러면 위와 같이 LoadCondingKeys를 정의해주면 됩니다. 

 

[Encoding]

let command = Command.load(key: "some key")
let commandData = try JSONEncoder().encode(command)
print(String(data: commandData, encoding: .utf8))

/*
{
  "load": {
    "zedd": "some key"
  }
}
*/

 

[Decoding]

let jsonString =
"""
{
    "command": {
        "load": {
            "zedd": "some key"
        }
    }
}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.command) // load("some key")

이때는 반드시 "zedd"라는 key로 와야겠죠?! 

 

 

# Q&A

[1. associated value의 레이블이 없을 때]

🤔 : 음...근데

enum Command: Codable {

  case load(key: String)
  case store(key: String, value: Int)
}

이렇게도 되지만,

enum Command: Codable {

  case load(String)
  case store(key: String, Int)
}

이렇게 associated value에 레이블이 안붙을수도 있잖아! 이럴 땐 어떻게 돼?

 

[Encoding]

{
  "load": {
    "_0": "MyKey"
  }
}
{
  "store": {
    "key": "MyKey",
    "_1": 42
  }
}

Encoding은 이런식으로 일어나게 되는데요.

_0, _1 이런것들이 보입니다.

associated value에 레이블이 없을 경우 key는 _$N(0부터 시작) 포맷으로 생성된다고 합니다!

 

[Decoding]

enum Command: Codable {

  case load(String)
  case store(key: String, Int)
}

아래와 같은 json을 Decoding하면 

let jsonString =
"""
{
    "command": {
        "load": {
            "key": "zedd"
        }
    }
}
"""

Decoding Error가 나게 됩니다. 

▿ DecodingError
  ▿ keyNotFound : 2 elements
    - .0 : DeleteCodingKeys(stringValue: "_0", intValue: nil)
    ▿ .1 : Context
      ▿ codingPath : 1 element
        - 0 : CodingKeys(stringValue: "command", intValue: nil)
      - debugDescription : "No value associated with key DeleteCodingKeys(stringValue: \"_0\", intValue: nil) (\"_0\")."
      - underlyingError : nil

associated value에 레이블이 없을 경우 key가 _$N(0부터 시작)로 생성되었는데, "key"가 key이기 때문...

그래서 

let jsonString =
"""
{
    "command": {
        "load": {
            "_0": "zedd"
        }
    }
}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.command) // load("zedd")

이런식으로 _0으로 지정하고 Decoding하면 잘됩니다.

 

그렇다면 위에서 배운 것을 응용해봅시다.

enum Command: Codable {

  case load(String)
  case store(key: String, Int)
}

이렇게 associated value의 레이블은 없지만??

enum Command: Codable {

  case load(String)
  case store(key: String, Int)
  
  enum LoadCodingKeys: String, CodingKey {
        case _0 = "zedd"
   }
}

이렇게 LoadCodingKeys를 지정해주면??

 

[Encoding]

{
  "load": {
    "zedd": "some key"
  }
}

[Decoding]

let jsonString =
"""
{
    "command": {
        "load": {
            "zedd": "some key"
        }
    }
}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.command) // load("some key")

이렇게 됩니다!

 

[2. associated value가 없는 case]

🤔 : 음..

enum Command: Codable {

  case load(String)
  case store(key: String, Int)
  case dumpToDisk ✅
}

dumpToDisk같이 associated value가 없는 case는 어떻게 돼?

 

[Encoding]

{
  "dumpToDisk": {}
}

[Decoding]

let jsonString =
"""
{
    "command": {
        "dumpToDisk": {} // 반드시 빈 객체가 들어가야함 
    }
}
"""
let data = jsonString.data(using: .utf8)!
let object = try JSONDecoder().decode(Response.self, from: data)
print(object.command) // dumpToDisk

 

# Unsupported cases

이 변경사항은 너무 좋지만, 지원하지 않는 케이스가 있습니다.

바로 

enum Command: Codable {
    
    case load(key: String)
    case load(index: Int)
}

이런 상황!

- 지원할 수 있는 명확한 방법이 없음

- 제한이 없어도 너무 없이 모델을 발전시킬 수 있기 때문에

지원을 안하는 것으로 결정이 되었다고 합니다! 

자세한 사항은 Unsupported cases 참고!

 

 

[참고]

https://github.com/apple/swift-evolution/blob/main/proposals/0295-codable-synthesis-for-enums-with-associated-values.md#codable-synthesis-for-enums-with-associated-values

반응형

'Swift' 카테고리의 다른 글

Literal  (0) 2022.01.23
Swift ) prefix / suffix  (1) 2021.12.01
WWDC 21 ) What‘s new in Swift  (3) 2021.06.11
Swift 5.4 Released!  (3) 2021.04.28
Introducing Swift Collections  (0) 2021.04.13