[Swift] Codable (2)

2022. 9. 14. 18:40SWIFT

반응형

저번 Codable (1) 글에 이어서 이번에는 좀 더 심화된 사용법에 대해 알아보려고 합니다.

 

[Swift] Codable (1)

블로그에 너무 소홀했는데, 오랜만에 정신을 차리고 다시 포스팅을 진행해보려고 합니다 🥲 오늘은 그동안 스터디를 진행하면서 이 부분은 꼭 따로 공부해서 써봐야지 했던 부분인 Codable에 관

dongminyoon.tistory.com

평소에는 이렇게까지 사용할 수 없을 수 있는데, 분명 알아두면 언젠가는 꼭 쓸일이 있을 것 같아요 🙃

 

Container란?

Container는 우리가 디코딩 & 인코딩을 하기 위한 Context라고 생각하면 될 것 같습니다. 각 기능을 위한 데이터의 구조(?) 정도로 저는 크게 이해했는데, 여기서 저희가 사용할 수 있게 크게 3가지 흐름으로 구분됩니다.

  • Keyed Container : CondingKey을 Key로 사용하는 Dictionary 같이 사용할 수 있는 Container
  • Unkeyed Container : 여러 값들을 가지지만 Key가 없는 Container 대표적으로 Array가 있음
  • Single Value Container : 하나의 primitive type(Int, Bool, String)등의 single value를 위한 Container
  • Nested Container : Keyed Container와 같이 내부에 Container를 한번 더 감싸는 Container

 

이렇게 대표적으로 구성되고 Nested container는 똑같은 역할에서 하나 더 Depth가 깊어지기 때문에, 따로 다루지는 않았습니다. 구조는 이렇게 되는데 이제 이걸 어떻게 사용하는지 각각 예시로 알아보아야겠죠?

 

Keyed Container

이전 글에서 Keyed Container는 간단히 사용하는 법을 알아보았기 때문에, 이번에는 좀 더 특별한 상황에 사용할 수 있는 방법을 알아볼게요. 

 

우리가 지금까지 디코딩을 진행했던 모델들은 Key : Value로 Key 값이 확실한 상황이었잖아요..? 만약 여기서 Key 값이 유동적으로 변한다면 어떻게 디코딩을 할 수 있을까요. 해당 방법에 대해 알아보겠습니다.

struct AnyCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init?(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
    
    init?(stringValue: String) {
        self.stringValue = stringValue
    }
}

CodingKey를 채택하고 구현하는 Struct인 AnyCodingKey를 하나 선언해줍니다.

간단히 String, Int가 Key로 들어왔을 때, 해당 값을 Key로하는 CodingKey를 초기화할 수 있게 사용합니다.

다음으로는 이 Struct를 활용해서 바로 디코딩을 해보겠습니다

struct PracData: Codable {
    
    var values = [String: Any]()
    
    func encode(to encoder: Encoder) throws {
        let error = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Not Supported")
        throw EncodingError.invalidValue(encoder.codingPath, error)
    }
    
    init(from decoder: Decoder) throws {
        // 1
        let container = try decoder.container(keyedBy: AnyCodingKey.self)
        
        var values = [String: Any]()
        
        // 2
        container.allKeys.forEach { key in
            // 3
            if let intValue = try? container.decodeIfPresent(Int.self, forKey: key) {
                values[key.stringValue] = intValue
            } else if let stringValue = try? container.decodeIfPresent(String.self, forKey: key) {
                values[key.stringValue] = stringValue
            } else if let boolValue = try? container.decodeIfPresent(Bool.self, forKey: key) {
                values[key.stringValue] = boolValue
            }
        }
        self.values = values
    }
    
}
  1. 보통 enum으로 Key들의 집합을 넘겨줬었는데, 현재는 해당 모델의 Key 값이 어떻게 내려올지 모르기 떄문에 이전에 구현했던 AnyCodingKey를 넘겨줍니다.
  2. Container의 모든 Key들을 iteration 합니다.
  3. Key에 어떤 타입이 들어있을 줄 모르니 필요한 타입들로 디코딩합니다.

이렇게 CodingKey를 채택하는 AnyCodingKey를 하나 구현함으로서 어떤 Key 값이 있을줄 모르는 객체를 디코딩 할 수 있는데요.

 

지금은 예시를 보여주기 위해 Dictionary를 이용해서 받고있지만 사용할 때는 저희가 하나의 Struct를 따로 구현해서 프로퍼티로 디코딩한 값을 넣어주는 방법도 사용할 수 있습니다.

 

AnyCodingKey 구현체를 이용하면 자주는 아니지만 종종 유용하게 사용할 수 있을 것 같네요 🙃

 

Single Value Container

하나의 Primitive Type을 위한 Container라고 정의했었죠.

Key : Value의 형태가 아닌 Primitive Type 하나만 들어왔을 때, 사용할 수 있습니다.

struct PracData: Codable {
    
    let stringValue: String?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        self.stringValue = try container.decode(String.self)
    }
    
}

let stringJSON = "\"ABC\""
if let stringData = stringJSON.data(using: .utf8) {
    let decodedData = JSONDecoder().decode(PracData.self, from: stringData)
    print(decodedData.stringValue) // "ABC"
}

이렇게 Key가 없이 Primitive Type만 들어오는 경우 활용할 수 있습니다.

근데 Unkeyed Container도 있었죠..? 비슷하다고 생각할 수도 있는데 조금 다릅니다. 

다음 예시에서 알아볼게요.

 

Unkeyed Container

보통 Key가 없는 Container로 활용되는데 대표적으로 Set, Array와 같은 타입들에 활용됩니다.

struct PracData: Codable {
    
    var arrayValue = [Any]()
    
    func encode(to encoder: Encoder) throws {
        let error = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Not Supported")
        throw EncodingError.invalidValue(encoder.codingPath, error)
    }
    
    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()
        
        var values = [Any]()
        while container.isAtEnd == false {
            if let stringValue = try? container.decodeIfPresent(String.self) {
                values.append(stringValue)
            } else if let boolValue = try? container.decodeIfPresent(Bool.self) {
                values.append(boolValue)
            } else if let intValue = try? container.decodeIfPresent(Int.self) {
                values.append(intValue)
            }
        }
        self.arrayValue = values
    }
    
}

let arrayJSON = """
[1, true, 20, 50]
"""

if let arrayData = arrayJSON.data(using: .utf8) {
    let decodedData = try? JSONDecoder().decode(PracData.self, from: arrayData)
    print(decodedData?.arrayValue)  // [1, true, 20, 50]
}

이렇게 Single Value Container와 비슷할수도있지만 Key가 없고 Array, Set과 같은 타입으로 넘어왔을 때, isAtEnd 프로퍼티로 확인하면서 마지막 요소까지 디코딩이 가능합니다.

 

또한 count, currentIndex와 같은 프로퍼티들로 Container에 몇개의 요소가 있고 현재 디코딩이 필요한 부분은 어딘지도 확인이 가능합니다. 자세한건 애플 문서에 나와있습니다

 

Nested Container

해당 Container는 Keyed Container와 동일하지만 내부에 포함되어 있는 Container를 사용할 수 있습니다.

struct PracData: Codable {
    
    let name: String
    let latitude: String
    let longitude: String
    
    func encode(to encoder: Encoder) throws {
        let error = EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Not Supported")
        throw EncodingError.invalidValue(encoder.codingPath, error)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String.self, forKey: .name)
    
        // 1
        let locationContainer = try container.nestedContainer(keyedBy: LocationKeys.self, forKey: .location)
         
        // 2
        self.latitude = try locationContainer.decode(String.self, forKey: .latitude)
        self.longitude = try locationContainer.decode(String.self, forKey: .longitude)
    }
    
    enum CodingKeys: String, CodingKey {
        case name
        case location
    }
    
    enum LocationKeys: String, CodingKey {
        case latitude = "위도"
        case longitude = "경도"
    }

}

let nestedJSON = """
{
    "name": "DongDong",
    "location": {
        "위도": "37.29782535558624",
        "경도": "127.06937480940317"
    }
}
"""

if let nestedData = nestedJSON.data(using: .utf8) {
    let decodedData = try? JSONDecoder().decode(PracData.self, from: nestedData)
    print(decodedData)
}
  1. .location으로 된 키 값의 Container를 새롭게 정의한 LocationKeys로 받아옵니다.
  2. 해당 Container에서 각 값들을 디코딩해서 현재 정의된 객체의 프로퍼티로 넣어줍니다.

물론 또 Location을 위한 객체를 정의해서도 가능하겠지만 이렇게 Nested Container를 이용해서도 디코딩이 가능합니다.

혹시 사용할 일이 있으면 이 방법으로 사용해도 유용할 것 같네요 🙃

 

오늘은 저번 포스팅에 이어 Codable 심화과정(?)에 대해 알아봤는데 잘 사용하면 유용할 것 같네요.

혹시 잘못된 내용이나 궁금한 내용이 있으면 댓글달아주세요 🙇‍♂️

 

반응형

'SWIFT' 카테고리의 다른 글

[Swift] Codable (1)  (0) 2022.04.24
[SWIFT] ArraySlice  (2) 2022.02.25
[SWIFT] String Compare (앱 버전 비교)  (0) 2021.10.10
[SWIFT] KVO (Key - Value - Observing)  (0) 2021.09.26
[SWIFT] KVC (Key - Value - Coding)  (0) 2021.09.20