포스트

MapKit

MapView 파일을 하나 만들어준다.

그리고 MapKit을 import 해주자.

지도를 화면에 보여주는건 아주 간단하다

그냥 Map()을 적으면 된다.

1
2
3
4
5
struct MapView: View {
    var body: some View {
        Map()
    }
}

CleanShot 2024-09-10 at 00 09 09@2x

이렇게 바로 지도가 보이게 된다.

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는 지도나 카메라 뷰에서 카메라의 방향과 각도를 조절하는 데 사용되는 중요한 속성들이다. 특히 지도 애플리케이션에서 사용자가 지도를 보는 각도와 방향을 제어할 때 유용하다.

  1. Heading (헤딩)
    • 설명: 헤딩은 카메라가 바라보는 방향을 나타내는 값이다. 지도를 위에서 내려다볼 때, 기본 방향은 북쪽(0도)이다. 헤딩 값은 시계 방향으로 증가하며, 360도는 다시 북쪽을 가리킨다.
    • 범위: 0에서 360까지의 값으로, 0도는 북쪽, 90도는 동쪽, 180도는 남쪽, 270도는 서쪽을 가리킨다.
    • 예시:
    • 0도: 북쪽을 바라봄
    • 90도: 동쪽을 바라봄
    • 180도: 남쪽을 바라봄
    • 270도: 서쪽을 바라봄
  2. 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
    )))
}

CleanShot 2024-09-10 at 00 18 59@2x

참고하자!

이제 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)
    }
}

Sep-10-2024 00-21-05

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면 👎 표시가 나온다.

Sep-10-2024 00-28-22

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()
                }
            }
        }
    }
}

Sep-10-2024 00-30-33

이렇게 클릭하면 토글로 인해 값이 바뀌면서 이미지와 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)
                        }
                    }
                }
            }

Sep-10-2024 00-39-23

하지만 아직 넘어가지는 않는다.

왜냐하면 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에 했던 세팅 그대로 값을 가져왔다.

Sep-10-2024 00-42-33

이렇게 화면전환이 되는 것을 확인할 수 있다.

하지만 화면전환시 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에서 확인을 해보면

Sep-10-2024 00-53-23

이렇게 작동이 되고, 또한 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`를 추가해주었다.

CleanShot 2024-09-10 at 01 01 57@2x

namespace와 id는 SwiftUI에서 애니메이션 및 뷰 간 전환(transition) 효과를 정의할 때 사용되는 요소들이다. namespace는 뷰 간의 일관된 애니메이션을 관리하기 위해 사용되고, id는 전환 효과가 적용될 대상 뷰를 식별하기 위해 사용된다.

@Namespace

  • 설명: @Namespace는 SwiftUI에서 뷰 간의 애니메이션 전환을 일관되게 관리할 수 있도록 도와주는 속성이다. 서로 다른 뷰가 같은 namespace를 공유하게 되면 SwiftUI는 뷰 간의 상태 변화에 대해 자연스러운 전환을 적용할 수 있다.
  • 용도: 뷰 전환 시 matchedGeometryEffect 또는 matchedTransitionSource와 같은 기능을 사용할 때 @Namespace를 사용한다.

id

  • 설명: id는 특정 전환 효과나 애니메이션이 적용될 뷰를 식별하는 데 사용되는 고유한 식별자이다. 같은 namespace 내에서 id가 동일한 두 뷰는 전환 중에 서로 연결되어 자연스러운 애니메이션을 제공할 수 있다.
  • 용도: SwiftUI에서 matchedTransitionSource 및 matchedTransitionDestination와 같은 수식어를 사용하여 특정 id를 기반으로 뷰 간의 전환을 설정할 수 있다.

Sep-10-2024 01-03-11

이렇게 클릭시 화면전환이 바뀐걸 알 수 있다.

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을 추가하고 버튼을 만들어 준다.

Sep-10-2024 01-33-15

작동이 잘 되는것을 확인할 수 있다.

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