포스트

Dex (7)

능력치 데이터를 다루기 위한 모델 확장

지난 글에서는 포켓몬 타입에 따라 배경 이미지를 지정하는 속성을 확장했다.
이번에는 능력치 데이터를 더 쉽게 다룰 수 있도록,
typeColor, stats, highestStat 등의 속성을 Pokemon 모델에 추가한다.

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
extension Pokemon {
    // 생략
    
    var typeColor: Color {
        Color(types![0].capitalized)
    }
    
    var stats: [Stat] {
        [
            Stat(id: 1, name: "HP", value: hp),
            Stat(id: 2, name: "Attack", value: attack),
            Stat(id: 3, name: "Defense", value: defense),
            Stat(id: 4, name: "Special Attack", value: specialAttack),
            Stat(id: 5, name: "Special Defense", value: specialDefense),
            Stat(id: 6, name: "Speed", value: speed)
        ]
    }
    
    var highestStat: Stat {
        stats.max { $0.value < $1.value }!
    }
}


struct Stat: Identifiable {
    let id: Int
    let name: String
    let value: Int16
}

이렇게

typeColor, stats, highestStat 속성을 Pokemon 모델에 추가해주었다.

이렇게 확장한 속성들은 이후 능력치 화면을 구성하는 데 활용될 것이다.

StatsView 만들기

Image

하단에 들어갈 Stat 관련 UI를 위해 새롭게 View를 만들어본다.

이번엔 Charts를 사용할 예정이다.

Charts의 경우 이전에 사용했던 적이 있다.

이전글 참고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct StatsView: View {
    var pokemon: Pokemon
    
    var body: some View {
        Chart(pokemon.stats) { stat in
            BarMark(
                x: .value("Value", stat.value),
                y: .value("Stat", stat.name)
            )
            .annotation(position: .trailing) {
                Text("\(stat.value)")
                    .font(.subheadline)
                    .foregroundStyle(.secondary)
                    .padding(.top, -5)
            }
        }
        .frame(height: 200)
        .padding([.horizontal, .bottom])
        .foregroundStyle(pokemon.typeColor)
        .chartXScale(domain: 0...pokemon.highestStat.value + 10)
    }
}

Image

디자인을 하면 위와 같다.

전반적인 구성은 단순하지만, chartXScale을 통해 최고값보다 약간 여유를 둔 것은 수치 막대가 너무 끝까지 닿지 않도록 하는 작은 UI 배려이다.

ContentView에 적용을 해보면

1
2
3
4
5
6
7
8
9
HStack {
     // 생략
}
.padding()

Text("Stats")
    .font(.title)
    .padding(.bottom, -7)
StatsView(pokemon: pokemon)

이렇게 구성한 StatsView를 적용하고 실행하면, 다음과 같이 화면에 능력치가 표시된다.

Image

Chart의 경우는 링크를 걸어둔 이전글에서 자세히 다뤘으니 다시 리마인드하면 좋을 것 같다.

Shiny Sprite 전환 기능 구현

DetailView에서 ToolBar를 하나 만들어 준다.

1
2
3
4
5
6
7
8
9
10
11
.navigationTitle(pokemon.name!.capitalized)
.toolbar {
    ToolbarItem(placement: .topBarTrailing) {
        Button {
            showShiny.toggle()
        } label: {
            Image(systemName: showShiny ? "wand.and.stars" : "wand.and.stars.inverse")
                .tint(showShiny ? .yellow : .primary)
        }
    }
}

이렇게 ToolBar를 추가해준 뒤,

1
AsyncImage(url: showShiny ? pokemon.shiny : pokemon.sprite) // modified

삼항연산자에 따라 showShiny가 토글되어서 true / false에 따라 이미지가 다르게 보여지도록 한다.

적용하고 실행하면, 다음과 같이 이미지가 달라지는걸 알 수 있다.

Image

아무리 그래도 리자몽 색이 완전히 달라지는 건 좀…..

보완

Image

현재 Normal, Flying 이렇게 같이 있는 포켓몬의 경우 Flying이 실제로 더 주된 타입(major type)처럼 느껴지기에 조금 수정을 해보려 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var decodedTypes: [String] = []
var typesContainer = try container.nestedUnkeyedContainer(forKey: .types)
        
while !typesContainer.isAtEnd {
        // 생략
}

// Pidget: ["normal", "flying"]
if decodedTypes.count == 2 && decodedTypes[0] == "normal" {
    let tempType = decodedTypes[0] // tempType: "normal"
    decodedTypes[0] = decodedTypes[1] // decodedTypes: ["flying", "flying"]
    decodedTypes[1] = tempType // ["flying", "normal"]
}

if를 통해 type이 2개이고, 배열의 첫번째 element가 “normal” 인 경우, 첫번째, 두번째의 element 순서를 바꾸도록 했다.

if decodedTypes.count == 2 && decodedTypes[0] == "normal" 이렇게 조건을 단정 지은 이유는

normal이면서 type.count가 2인경우는 ["normal", "flying"] 밖에 없기 때문

ImageImage

이렇게 순서가 바뀐것을 알 수 있다.

이떄 코드를 조금 더 간결하게 하고싶다면

1
2
3
4
5
6
7
8
9
10
// before
if decodedTypes.count == 2 && decodedTypes[0] == "normal" {
    let tempType = decodedTypes[0] // tempType: "normal"
    decodedTypes[0] = decodedTypes[1] // decodedTypes: ["flying", "flying"]
    decodedTypes[1] = tempType // ["flying", "normal"]
}
// after
if decodedTypes.count == 2 && decodedTypes[0] == "normal" {
    decodedTypes.swapAt(0, 1)
}

이렇게 바꿔주면 된다. index가 0인 element와, index가 1인 element의 위치를 서로 스왑하라는 것.

다음 글에서는 현재 URL 기반으로만 불러오고 있는 포켓몬 스프라이트 이미지를 Core Data에 저장하여 오프라인에서도 보여줄 수 있도록 개선해본다.

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