포스트

Dex (2)

CoreData로 부터 Fetch

그전에 Controller에서

마지막 부분에

container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump MergePolicy를 추가해주자.

Docs 참고

Merge Policy설명우선순위사용 예시
NSMergeByPropertyObjectTrumpMergePolicy메모리(Context)의 변경값이 저장소(Store) 값을 덮어씀메모리 우선사용자가 입력한 최신 값을 유지하고 싶을 때
NSMergeByPropertyStoreTrumpMergePolicy저장소(Store)의 기존 값을 메모리(Context) 변경보다 유지저장소 우선서버 동기화 상태를 신뢰하고 충돌을 피하고 싶을 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Pokemon.id, ascending: true)],
        animation: .default)
    private var pokedex: FetchedResults<Pokemon>

    var body: some View {
        NavigationView {
            List {
                ForEach(pokedex) { pokemon in
                    NavigationLink {
                        Text(pokemon.name ?? "no name")
                    } label: {
                        Text(pokemon.name ?? "no name")
                    }
                }
                
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button("Add Item", systemImage: "plus") {
                        
                    }
                }
            }
        }
    }
}

이렇게 기본틀을 다듬어 준다.

Image

에러가 모두 해결되고 Controller에서 만든 초기값이 현재 Preview에 보이게 된다.

CodingKeys

뭐 CodingKeys 라고 적었지만 이미 Modeling 하면서 사용했던것이기에 크게 뭐 언급할만한건 없어보인다. (이때는 몰랐다…..)

이전글에서 비슷하게 한적이 있긴한데.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
struct FetchedPokemon: Decodable {
    let id: Int16
    let name: String
    let types: [String]
    let hp: Int16
    let attack: Int16
    let defense: Int16
    let specialAttack: Int16
    let specialDefense: Int16
    let speed: Int16
    let sprite: URL
    let shiny: URL
    
    enum CodingKeys: CodingKey {
        case id
        case name
        case types
        case stats
        case sprites
        
        enum TypeDictionaryKeys: CodingKey {
            case type
            
            enum TypeKeys: CodingKey {
                case name
            }
        }
        
        enum StatDictionaryKeys: CodingKey {
            case baseStat
        }
        
        enum SpriteKeys: String, CodingKey {
            case sprite = "frontDefault"
            case shiny = "frontShiny"
        }
    }
    init(from decoder: any Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    
    self.id = try container.decode(Int16.self, forKey: .id)
    self.name = try container.decode(String.self, forKey: .name)
    self.types = try container.decode([String].self, forKey: .types)
    self.hp = try container.decode(Int16.self, forKey: .hp)
    self.attack = try container.decode(Int16.self, forKey: .attack)
    self.defense = try container.decode(Int16.self, forKey: .defense)
    self.specialAttack = try container.decode(Int16.self, forKey: .specialAttack)
    self.specialDefense = try container.decode(Int16.self, forKey: .specialDefense)
    self.speed = try container.decode(Int16.self, forKey: .speed)
    self.sprite = try container.decode(URL.self, forKey: .sprite)
    self.shiny = try container.decode(URL.self, forKey: .shiny)
    }
}

여기서 눈여겨 봐야 할 것은

CondingKeys 라는 enum 안에 여러 enum을 또 세분화 한건데 이유는 json에 있다.

Image

해당 사진을 보면 이렇게 세부적으로 나뉘어져있는데

이것을 api를 통해 가져올것이므로 위와 같이 코드를 작성한 것.

Image

여기서 init구문 내 self를 모두 지워주자 이때, 한번에 지우는 방법이 있는데 Shift+Control을 누른 상태에서 이렇게 내리면 다중 선택 처럼 되어서 동시에 삭제가 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        id = try container.decode(Int16.self, forKey: .id)
        
        name = try container.decode(String.self, forKey: .name)
        
        var decodedTypes: [String] = []
        var typesContainer = try container.nestedUnkeyedContainer(forKey: .types)
        
        while !typesContainer.isAtEnd {
            // Decode types
            let typesDictionaryContainer = try typesContainer.nestedContainer(keyedBy: CodingKeys.TypeDictionaryKeys.self)
            let typeContainer = try typesDictionaryContainer.nestedContainer(keyedBy: CodingKeys.TypeDictionaryKeys.TypeKeys.self, forKey: .type)
            
            let type = try typeContainer.decode(String.self, forKey: .name)
            decodedTypes.append(type)
        }
        types = decodedTypes

        var decodedStats: [Int16] = []
        var statsContainer = try container.nestedUnkeyedContainer(forKey: .stats)
        while !statsContainer.isAtEnd {
            let statsDictionaryContainer = try statsContainer.nestedContainer(keyedBy: CodingKeys.StatDictionaryKeys.self)
            let stat = try statsDictionaryContainer.decode(Int16.self, forKey: .baseStat)
            decodedStats.append(stat)
        }
        
        hp = decodedStats[0]
        attack =  decodedStats[1]
        defense =  decodedStats[2]
        specialAttack =  decodedStats[3]
        specialDefense =  decodedStats[4]
        speed =  decodedStats[5]
        
        let spriteContainer = try container.nestedContainer(keyedBy: CodingKeys.SpriteKeys.self, forKey: .sprites)
        
        sprite = try spriteContainer.decode(URL.self, forKey: .sprite)
        shiny = try spriteContainer.decode(URL.self, forKey: .shiny)
    }

이렇게 코드를 바꿔주었따.

이부분은 생소한 부분이기에 조금 더 자세히 짚어보도록 한다.


🧬 FetchedPokemon 구조 요약

1️⃣ 왜 CodingKeys를 계층적으로 나누었는가?

중첩 JSON 구조에 정확히 접근하기 위해 각 계층마다 별도의 CodingKey enum이 필요하다.

항목JSON 구조 예시설명필요한 키 enum
types[ { "type": { "name": "..." } } ]배열 → 딕셔너리 → 내부 딕셔너리TypeDictionaryKeys.TypeKeys
stats[ { "base_stat": ..., "stat": { "name": "..." } } ]배열 → 딕셔너리 (기본값) → 내부 딕셔너리 (이름 등 추가 정보 포함)StatDictionaryKeys
sprites{ "sprites": { "front_default": "...", "front_shiny": "...", ... } }딕셔너리 → 여러 key → URL (Swift 속성과 JSON key 이름이 다름)SpriteKeys (with rawValue)

JSON 내부에 중첩된 배열이나 딕셔너리를 파싱하려면 Swift의 nested(Un)keyedContainer와 계층적인 CodingKey 설계가 필수적이다.


2️⃣ 왜 init(from:)을 직접 구현했는가?

자동 생성이 JSON 구조에 대응하지 못하기 때문.

  • 중첩 구조 (types, stats) → 배열 안에 딕셔너리 구조
  • 키 이름 불일치 (sprites) → "front_default" 등 JSON key가 Swift 변수명과 다름

3️⃣ 주요 흐름 정리

항목처리 방식
id, name상위 container에서 바로 디코딩
types배열 → 딕셔너리 → 내부 딕셔너리 → namewhile 루프
stats배열 → 딕셔너리 → base_stat만 추출 → while 루프
sprites1단계 nested container (sprites) + key 이름 매핑

4️⃣ 컨테이너 사용 요약

구조메서드예시
딕셔너리nestedContainersprites, type, stat
배열nestedUnkeyedContainertypes, stats
루프while !container.isAtEnd반복 디코딩
누적append()배열에 값 추가 (types, stats)

🧩 핵심 요약

  • 중첩 JSON 구조 → CodingKeys를 계층화하여 정확히 매핑
  • 자동 생성 init은 구조 대응 불가 → 수동 구현 필수
  • types, stats, sprites는 각기 다른 방식으로 접근 및 파싱 필요
  • nested(Un)keyedContainer, while 루프, rawValue 사용 이해 필수

📦 JSONDecoder.decode vs Decoder.container - 예제 기반 비교

✅ JSONDecoder.decode 방식

예제 JSON (단순 구조):

1
2
3
4
5
{
  "name": "Bulbasaur",
  "hp": 45,
  "attack": 49
}

적용 코드:

1
2
3
4
5
6
7
struct Pokemon: Decodable {
  let name: String
  let hp: Int
  let attack: Int
}

let result = try JSONDecoder().decode(Pokemon.self, from: data)

특징:

  • JSON 구조가 평면적(flat)일 때 매우 간단하게 사용 가능
  • 모델 구조와 JSON 구조가 1:1로 대응됨

✅ Decoder.container 방식

예제 JSON (중첩 구조):

1
2
3
4
5
6
7
8
9
{
  "pokemon": {
    "name": "Bulbasaur",
    "stats": {
      "hp": 45,
      "attack": 49
    }
  }
}

적용 코드:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct PokemonStats: Decodable {
  let name: String
  let hp: Int

  enum RootKeys: String, CodingKey {
    case pokemon
  }

  enum PokemonKeys: String, CodingKey {
    case name
    case stats
  }

  enum StatsKeys: String, CodingKey {
    case hp
  }

  init(from decoder: Decoder) throws {
    let root = try decoder.container(keyedBy: RootKeys.self)
    let pokemon = try root.nestedContainer(keyedBy: PokemonKeys.self, forKey: .pokemon)
    name = try pokemon.decode(String.self, forKey: .name)

    let stats = try pokemon.nestedContainer(keyedBy: StatsKeys.self, forKey: .stats)
    hp = try stats.decode(Int.self, forKey: .hp)
  }
}

특징:

  • 중첩된 구조에서 필요한 값만 파싱 가능
  • 전체 구조를 모델링하지 않아도 됨
  • JSON이 복잡하거나 일부 값만 추출할 때 유리

📌 요약 비교표

항목JSONDecoder.decodeDecoder.container
예제 JSON 구조평면적중첩 구조
처리 대상전체 구조필요한 값만 선택적으로 추출
사용 난이도간단하고 직관적정교하고 유연한 제어 가능
적합한 상황전체 구조가 단순하고 그대로 쓸 수 있을 때일부 값만 파싱하거나 구조가 복잡할 때

다음편에서는 실제 JSON 구조를 바탕으로 init 디코딩을 어떻게 하는지 자세히 본다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.