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") {
}
}
}
}
}
}
이렇게 기본틀을 다듬어 준다.
에러가 모두 해결되고 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에 있다.
해당 사진을 보면 이렇게 세부적으로 나뉘어져있는데
이것을 api를 통해 가져올것이므로 위와 같이 코드를 작성한 것.
여기서 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 | 배열 → 딕셔너리 → 내부 딕셔너리 → name → while 루프 |
stats | 배열 → 딕셔너리 → base_stat 만 추출 → while 루프 |
sprites | 1단계 nested container (sprites ) + key 이름 매핑 |
4️⃣ 컨테이너 사용 요약
구조 | 메서드 | 예시 |
---|---|---|
딕셔너리 | nestedContainer | sprites , type , stat |
배열 | nestedUnkeyedContainer | types , 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.decode | Decoder.container |
---|---|---|
예제 JSON 구조 | 평면적 | 중첩 구조 |
처리 대상 | 전체 구조 | 필요한 값만 선택적으로 추출 |
사용 난이도 | 간단하고 직관적 | 정교하고 유연한 제어 가능 |
적합한 상황 | 전체 구조가 단순하고 그대로 쓸 수 있을 때 | 일부 값만 파싱하거나 구조가 복잡할 때 |
다음편에서는 실제 JSON 구조를 바탕으로 init 디코딩을 어떻게 하는지 자세히 본다.