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 만들기
하단에 들어갈 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)
}
}
디자인을 하면 위와 같다.
전반적인 구성은 단순하지만, 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를 적용하고 실행하면, 다음과 같이 화면에 능력치가 표시된다.
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에 따라 이미지가 다르게 보여지도록 한다.
적용하고 실행하면, 다음과 같이 이미지가 달라지는걸 알 수 있다.
아무리 그래도 리자몽 색이 완전히 달라지는 건 좀…..
보완
현재 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"]
밖에 없기 때문
이렇게 순서가 바뀐것을 알 수 있다.
이떄 코드를 조금 더 간결하게 하고싶다면
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에 저장하여 오프라인에서도 보여줄 수 있도록 개선해본다.