티스토리 뷰
안녕하세요 :) Zedd입니다.
꼭 Swift가 릴리즈되면 변경사항을 공부 & 정리했었는데...
요번 Swift 5.5의 변경사항이 은근히 많다보니..
- SE-0291 Package Collections
- SE-0293 Extend Property Wrappers to Function and Closure Parameters
- SE-0295 Codable synthesis for enums with associated values
- SE-0296 Async/await
- SE-0297 Concurrency Interoperability with Objective-C
- SE-0298 Async/Await: Sequences
- SE-0299 Extending Static Member Lookup in Generic Contexts
- SE-0300 Continuations for interfacing async tasks with synchronous code
- SE-0304 Structured concurrency
- SE-0306 Actors
- SE-0307 Allow interchangeable use of CGFloat and Double types
- SE-0308 if for postfix member expressions
- SE-0310 Effectful Read-only Properties
- SE-0311 Task Local Values
- SE-0313 Improved control over actor isolation
- SE-0314 AsyncStream and AsyncThrowingStream
- SE-0316 Global actors
- SE-0317 async let bindings
- SE-0319 Conform Never to Identifiable
ㅎㅎ...ㅠㅠㅠ 정리를 못해서 아쉬웠는데, 그냥 하나씩 공부해보는 것도 나쁘지 않을 것 같아요 💪
오늘은 얼마전에 처음 봤던!
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
}
바로 요부분인데요.
LoadCodingKeys와 StoreCodingKeys가 만들어진다는 겁니다.
(이름이 저거다!라는게 아니라 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 참고!
[참고]
'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 |
- fastlane
- Combine
- UIBezierPath
- swift tutorial
- FLUTTER
- swift delegate
- IOS
- Swift
- SwiftUI
- 스위프트 문법
- WWDC
- swift 공부
- 피아노
- actor
- ios 13
- 제이슨 파싱
- Xcode
- WKWebView
- np-complete
- 스위프트
- swift3
- swift sort
- Accessibility
- iOS delegate
- WidgetKit
- github
- 회고
- np-hard
- Git
- swift array
- Total
- Today
- Yesterday