MapKit
MapView 파일을 하나 만들어준다.
그리고 MapKit을 import 해주자.
지도를 화면에 보여주는건 아주 간단하다
그냥 Map()
을 적으면 된다.
1
2
3
4
5
struct MapView: View {
var body: some View {
Map()
}
}
이렇게 바로 지도가 보이게 된다.
1. 설정한 지역을 보이게 구현하기
우리가 이전에 Place를 통해 여러 샘플의 좌표를 지정해두었다.
그것을 활용해서 지도에 보여지게 해보자.
우선 preview에 다음과 같이 적어보자
1
2
3
4
5
6
7
8
#Preview {
MapView(place: Place.previewPlaces[0], position: .camera(MapCamera(
centerCoordinate: Place.previewPlaces[0].location,
distance: 1000,
heading: 250,
pitch: 80
)))
}
heading과 pitch는 지도나 카메라 뷰에서 카메라의 방향과 각도를 조절하는 데 사용되는 중요한 속성들이다. 특히 지도 애플리케이션에서 사용자가 지도를 보는 각도와 방향을 제어할 때 유용하다.
- Heading (헤딩)
- 설명: 헤딩은 카메라가 바라보는 방향을 나타내는 값이다. 지도를 위에서 내려다볼 때, 기본 방향은 북쪽(0도)이다. 헤딩 값은 시계 방향으로 증가하며, 360도는 다시 북쪽을 가리킨다.
- 범위: 0에서 360까지의 값으로, 0도는 북쪽, 90도는 동쪽, 180도는 남쪽, 270도는 서쪽을 가리킨다.
- 예시:
- 0도: 북쪽을 바라봄
- 90도: 동쪽을 바라봄
- 180도: 남쪽을 바라봄
- 270도: 서쪽을 바라봄
- Pitch (피치)
- 설명: 피치는 카메라의 기울기를 나타내는 값이다. 피치는 지도 또는 3D 뷰를 수직 방향에서 얼마나 기울여서 볼지를 결정한다.
- 범위: 0에서 90까지의 값으로, 0도는 수직으로 아래를 내려다보는 것이고, 90도는 지면과 수평하게 보는 것이다.
- 예시:
- 0도: 지도를 수직으로 내려다보는 뷰 (탑다운 뷰)
- 45도: 지도를 약간 기울여서 보는 뷰 (일부 지형이 보이기 시작함)
- 80도: 거의 수평으로 지형과 건물 등을 볼 수 있는 뷰 (지상에서 보는 것과 유사한 뷰)
하지만 보이지 않는다?
왜냐하면 body 부분을 더 보완해야 하기 때문이다.
그전에, 현재 프리뷰쪽 코드에서 Place.previewPlaces[0]
이게 두번 반복이 된다.
코드를 좀 줄여보자.
1
2
3
4
5
6
7
8
9
10
#Preview {
@Previewable @State var place = Place.previewPlaces[0]
MapView(place: place, position: .camera(MapCamera(
centerCoordinate: place.location,
distance: 1000,
heading: 250,
pitch: 80
)))
}
참고하자!
이제 body부분을 보완하자.
간단하다. Map() 괄호 안에 위치를 넣어주면 된다.
1
2
3
4
5
6
7
8
9
struct MapView: View {
var place: Place
@State var position: MapCameraPosition
var body: some View {
Map(position: $position)
}
}
2. Annotation 추가하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct MapView: View {
var place: Place
@State var position: MapCameraPosition
var body: some View {
Map(position: $position) {
Annotation(place.intersted ? "Place of Interest" : "Not Interested", coordinate: place.location) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(.ultraThickMaterial)
.stroke(.secondary, lineWidth: 5)
Image(systemName: place.intersted ? "face.smiling" : "hand.thumbsdown")
}
}
}
}
}
어노테이션을 그냥 추가하면 되는데 이때 앞에는 title이 들어간다.
그래서 interested가 true / false에 따라 이름이 다르게 나오게 삼항연산자를 사용하였고, 그뒤에는 annotation pin이 들어갈 좌표를 설정해준다.
이후 Zstack을 사용하여 핀의 이미지를 구현하는데 true면 웃는 표시, false면 👎 표시가 나온다.
3. 핀 클릭시 값 변경하기
onTapGesture
를 통해서 토글을 해주면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct MapView: View {
var place: Place
@State var position: MapCameraPosition
var body: some View {
Map(position: $position) {
Annotation(place.intersted ? "Place of Interest" : "Not Interested", coordinate: place.location) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(.ultraThickMaterial)
.stroke(.secondary, lineWidth: 5)
Image(systemName: place.intersted ? "face.smiling" : "hand.thumbsdown")
.padding(5)
}
.onTapGesture {
place.intersted.toggle()
}
}
}
}
}
이렇게 클릭하면 토글로 인해 값이 바뀌면서 이미지와 title도 같이 바뀌게 된다.
3. PlaceList와 연동하기
다시 PlaceList로 돌아와서 NavigationLink를 추가해준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NavigationStack {
List ((try? places.filter(predicate)) ?? places) { place in
NavigationLink(value: place) {
HStack {
place.image
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: 7))
.frame(width: 100, height: 100)
Text(place.name)
Spacer()
if place.intersted {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.padding(.trailing)
}
}
}
}
하지만 아직 넘어가지는 않는다.
왜냐하면 Destination을 설정하지 않았기 때문
animation 밑에 destination을 설정해주자.
1
2
3
4
5
6
7
8
9
10
11
.navigationTitle("Places")
.searchable(text: $searchText, prompt: "Find a Place")
.animation(.default, value: searchText)
.navigationDestination(for: Place.self) { place in
MapView(place: place, position: .camera(MapCamera(
centerCoordinate: place.location,
distance: 1000,
heading: 250,
pitch: 80
)))
}
카메라는 아까 preview에 했던 세팅 그대로 값을 가져왔다.
이렇게 화면전환이 되는 것을 확인할 수 있다.
하지만 화면전환시 Navigation Bar 영역이 남아있어 좋아보이지 않는다.
MapView로 가서
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct MapView: View {
var place: Place
@State var position: MapCameraPosition
var body: some View {
Map(position: $position) {
Annotation(place.intersted ? "Place of Interest" : "Not Interested", coordinate: place.location) {
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(.ultraThickMaterial)
.stroke(.secondary, lineWidth: 5)
Image(systemName: place.intersted ? "face.smiling" : "hand.thumbsdown")
.padding(5)
}
.onTapGesture {
place.intersted.toggle()
}
}
}
.toolbarBackground(.automatic)
}
}
toolbarBackground에 대해 automatically로 작동하게 설정을 한다.
여기 Preview에서는 변화를 감지하지 못한다.
다시 PlaceList에서 확인을 해보면
이렇게 작동이 되고, 또한 interested도 반영이 되는걸 확인할 수 있다.
Swift data의 장점이 여기서 나온다.
Swift 데이터를 사용하고 실제 데이터베이스에 데이터를 저장하면 다음 중 하나를 변경할 수 있다.
→ 값이 바뀌면 모든 곳에서 바뀐다! → 동일한 값이고 동일한 속성이기 때문에 데이터베이스에서 해당 값을 참조한다.
4. 부모, 자식뷰를 활용하여 보완하기
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
var body: some View {
NavigationStack {
List ((try? places.filter(predicate)) ?? places) { place in
NavigationLink(value: place) {
HStack {
place.image
.resizable()
.scaledToFit()
.clipShape(.rect(cornerRadius: 7))
.frame(width: 100, height: 100)
Text(place.name)
Spacer()
if place.intersted {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
.padding(.trailing)
}
}
}
.matchedTransitionSource(id: 1, in: namespace) // added
}
.navigationTitle("Places")
.searchable(text: $searchText, prompt: "Find a Place")
.animation(.default, value: searchText)
.navigationDestination(for: Place.self) { place in
MapView(place: place, position: .camera(MapCamera(
centerCoordinate: place.location,
distance: 1000,
heading: 250,
pitch: 80
)))
.navigationTransition(.zoom(sourceID: 1, in: namespace)) // added
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button("Show Images", systemImage: "photo") {
showImages.toggle()
}
.sheet(isPresented: $showImages) {
Scrolling()
}
}
ToolbarItem(placement: .topBarLeading) {
Button("filter", systemImage: filterByInterested ? "star.fill" : "star") {
withAnimation {
filterByInterested.toggle()
}
}
.tint(filterByInterested ? .yellow : .blue)
}
}
}
}
그리고 ` @Namespace var namespace`를 추가해주었다.
namespace와 id는 SwiftUI에서 애니메이션 및 뷰 간 전환(transition) 효과를 정의할 때 사용되는 요소들이다. namespace는 뷰 간의 일관된 애니메이션을 관리하기 위해 사용되고, id는 전환 효과가 적용될 대상 뷰를 식별하기 위해 사용된다.
@Namespace
- 설명: @Namespace는 SwiftUI에서 뷰 간의 애니메이션 전환을 일관되게 관리할 수 있도록 도와주는 속성이다. 서로 다른 뷰가 같은 namespace를 공유하게 되면 SwiftUI는 뷰 간의 상태 변화에 대해 자연스러운 전환을 적용할 수 있다.
- 용도: 뷰 전환 시 matchedGeometryEffect 또는 matchedTransitionSource와 같은 기능을 사용할 때 @Namespace를 사용한다.
id
- 설명: id는 특정 전환 효과나 애니메이션이 적용될 뷰를 식별하는 데 사용되는 고유한 식별자이다. 같은 namespace 내에서 id가 동일한 두 뷰는 전환 중에 서로 연결되어 자연스러운 애니메이션을 제공할 수 있다.
- 용도: SwiftUI에서 matchedTransitionSource 및 matchedTransitionDestination와 같은 수식어를 사용하여 특정 id를 기반으로 뷰 간의 전환을 설정할 수 있다.
이렇게 클릭시 화면전환이 바뀐걸 알 수 있다.
5. SwipeAction 추가하기
1
2
3
4
5
6
7
.matchedTransitionSource(id: 1, in: namespace)
.swipeActions(edge: .leading) {
Button(place.interested ? "Interested" : "Not Interested", systemImage: "star") {
place.interested.toggle()
}
}
.tint(place.interested ? .yellow : .gray)
이렇게 밑에 swipeAction을 추가하고 버튼을 만들어 준다.
작동이 잘 되는것을 확인할 수 있다.