Dex (6)
PokemonDetailView
이제는 DetailView를 만들어 본다.
디자인은 이렇게 할 예정
Preview용 SampleData 생성
그전에 샘플 데이터를 먼저 만들어줄것이다.
1
2
3
4
5
6
7
8
static var previewPokemon: Pokemon {
let context = PersistenceController.preview.container.viewContext
}
@MainActor
static let preview: PersistenceController = {
// 생략
}
이렇게 코드를 작성하자마자 발생하는 에러
@MainActor
로 선언된 preview는 MainActor context 내에서만 접근 가능하다. 즉, 문제의 원인은 @MainActor로 선언된 static 프로퍼티나 메서드는 명시적으로 MainActor 컨텍스트 내에서만 접근할 수 있는데, 그걸 MainActor 외부에서 사용했기 때문, 그렇기에 @MainActor
를 지워주면 된다.
그리고 코드를 완성시키자.
1
2
3
4
5
6
7
8
9
static var previewPokemon: Pokemon {
let context = PersistenceController.preview.container.viewContext
let fetchRequest: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
fetchRequest.fetchLimit = 1
let results = try! context.fetch(fetchRequest)
return results.first!
}
Environmentobject 사용한 pokemon 데이터 공유
이전에 이것을 사용해서 코드를 작성해본 경험이 있기에 어렵지는 않다.
이전글은 여기에
1
2
3
4
5
6
// ContentView
.navigationDestination(for: Pokemon.self) { pokemon in
PokemonDetailView() // Changed
.environmentObject(pokemon)
}
이때 넘겨주는 포켓몬 인스턴스는 뷰 계층 전체에서 공유 가능한 전역 상태처럼 작동하므로, 해당 뷰와 그 하위 뷰에서 @EnvironmentObject
로 접근 가능하다.
UIKit 시절이라면 아마 didSelectRowAt
에서 indexPath를 기준으로 데이터를 꺼내고,
NavigationController.pushViewController
나 present
를 통해 화면을 전환하면서 해당 데이터를 전달했을 것이다.
SwiftUI에서는 이 과정이 훨씬 간단하다. environmentObject
를 통해 데이터를 넘기고, NavigationStack
을 사용해 “어떤 데이터가 선택되면 어떤 화면으로 이동할지”를 코드로 미리 구성해둘 수 있다. UIKit처럼 화면 전환을 직접 명령하지 않아도 된다.
다시 본론으로 돌아와서 EnvironmentObject를 전달 받는 DetailView에서는
1
2
3
4
5
6
7
8
9
10
11
12
struct PokemonDetailView: View {
// 생략
@EnvironmentObject private var pokemon: Pokemon
// 생략
}
#Preview {
NavigationStack {
PokemonDetailView()
.environmentObject(PersistenceController.previewPokemon)
}
}
이런식으로 만들어주자.
여기서 Preview는 NavigationStack
으로 한번 감싸준 이유는 NavigationLink를 통해 넘어오기에 아래 디자인 쪽에 navigationTitle
Modifier를 사용했는데, preview에서는 보이지 않기에 NavigationStack을 사용.
그리고 environmentObject
의 경우엔 위에서 만든 샘플을 적용
UI Design
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
54
55
56
57
58
59
60
61
62
63
64
struct PokemonDetailView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject private var pokemon: Pokemon
@State private var showShiny = false
var body: some View {
ScrollView {
ZStack {
Image(.normalgrasselectricpoisonfairy)
.resizable()
.scaledToFit()
.shadow(color: .black, radius: 6)
AsyncImage(url: pokemon.sprite) { image in
image
.interpolation(.none)
.resizable()
.scaledToFit()
.padding(.top, 50)
.shadow(color: .black, radius: 6)
} placeholder: {
ProgressView()
}
}
HStack {
ForEach(pokemon.types!, id: \.self) { type in
Text(type.capitalized)
.font(.title2)
.fontWeight(.semibold)
.foregroundStyle(.black)
.shadow(color: .white, radius: 1)
.padding(.vertical, 7)
.padding(.horizontal)
.background(Color(type.capitalized))
.clipShape(.capsule)
}
Spacer()
Button {
pokemon.favorite.toggle()
do {
try viewContext.save()
} catch {
print(error)
}
} label: {
Image(systemName: pokemon.favorite ? "star.fill" : "star")
.font(.largeTitle)
.tint(.yellow)
}
}
.padding()
}
.navigationTitle(pokemon.name!.capitalized)
}
}
interpolation
말고는 크게 언급할만한 내용은 없어보인다.
interpolation(.none)은 확대 시 흐릿하게 보이지 않도록 픽셀을 그대로 보여주는 옵션으로, 포켓몬 스프라이트처럼 도트 기반 이미지에 적합하다.
현재까지 완성된 UI는 다음과 같다.
Pokemon Extension
Core Data 모델(Pokemon)에는 저장 속성만 정의할 수 있기 때문에, 계산된 속성이나 UI를 위한 로직은 따로 확장해서 구현해주어야 한다. Swift의 extension 기능을 이용하면 기존 모델에 새로운 계산 프로퍼티를 추가할 수 있어, View 쪽에서 더 간결하고 직관적인 방식으로 접근이 가능하다.
예를 들어, 포켓몬의 주 타입(types[0])에 따라 배경 이미지를 다르게 보여주고 싶을 때 아래와 같이 구현할 수 있다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
extension Pokemon {
var background: ImageResource {
switch types![0] {
case "rock", "ground", "steel", "fighting", "ghost", "dark", "psychic":
.rockgroundsteelfightingghostdarkpsychic
case "fire", "dragon":
.firedragon
case "flying", "bug":
.flyingbug
case "ice":
.ice
case "water":
.water
default:
.normalgrasselectricpoisonfairy
}
}
}
이렇게 extension을 통해 뷰에서 .background만 호출하면 간단히 배경 이미지를 불러올 수 있게 된다.
1
2
3
4
var body: some View {
ScrollView {
ZStack {
Image(pokemon.background) // changed
그리고 실행을 해보면, 각 type에 맞게 배경이 다르게 표시되는걸 알 수 있다.
이번 글에서는 포켓몬의 타입에 따라 배경 이미지가 바뀌도록 background
속성을 확장했다.
다음 글에서는 포켓몬 능력치 시각화를 위해 typeColor
, stats
, highestStat
속성을 확장하고
이를 활용한 Stat Chart UI를 Detail 화면에 구현해본다.