본문 바로가기

iOS+

MapKit

개요

이 글은 MapKit에 대한 정리 글이다

다음과 같은 개념들에 대해 정리한다

 

  • CoreLocation
  • Map View
    • Mark, Annotation, UserAnnotation
    • MapCameraPosition
    • MKMapItem
    • .onMapCameraChange
  • LocalSearch와 LookAroundPreview
    • MKLocalSearch 
    • LookAroundPreview

 

CoreLocation

MapKit에 대해 알아보기에 앞서 우선 CoreLocation에 대해 정리한다

CoreLocation은 iOS 앱에서 위치 관련 기능을 구현하는 데 사용되는 Apple의 프레임워크다

 

  • CLLocationManager - CoreLocation의 핵심 클래스로, 위치 서비스를 관리한다(권한 요청)
  • CLLocationCoordinate2D - 위도와 경도를 포함하는 구조체
  • CLRegion - 지리적 영역을 정의하는 클래스

 

Coordinate는 좌표이고, Region은 지역범위를 표현한다고 생각하면 된다

이러한 관점에서 보면 마찬가지로 MapKit의 MKCoordinateRegion은 CLLocationCoordinate2D과 MKCoordinateSpan로 이뤄진 것을 알 수 있다(점과 범위)

 

참고로 CLLocationManager로 위치정보를 얻으려면, Info에 관련 내용을 등록하고 권한 요청을 해야 한다

 

  • Info
    • "Privacy - Location When In Use Usage Description" : 앱이 포그라운드에서 실행 중일 때만 위치 정보에 접근
    • "Privacy - Location Always and When In Use Usage Description" : 앱이 백그라운드에서 실행되는 동안 위치 정보에 접근

 

Map View

MapKitMap 뷰를 제공해 준다

Map() { }에서 클로저로 MarkerAnnotation 같은 뷰를 전달받아서 Map 위에 나타낸다

 

또한, Map 뷰에 주로 사용되는 인자로는 position과 selection이 있다

  • Map(position:) - MapCameraPosition을 Binding 하여 넘긴다
  • Map(selection:) - MKMapItem?을 Binding 하여 넘긴다

 

Marker와 Annotation 그리고 UserAnnotation

이 뷰들은 클로저로 전달되며, CLLocationCoordinate2D나 현재 위치 정보를 통해 Map 뷰 위에 표시한다

Marker(_:systemImage:coordniate)

Map에서 표시되는 요소들은 CLLocationCooridnate2D로 위치정보가 필요하다

다음 예시에서 볼 수 있듯이 Map 뷰에 클로저로 Marker 뷰를 전달하면, Map 위에 나타난다(Image 적용도 가능하다)

 

extension CLLocationCoordinate2D {
    static let hongikUniv = CLLocationCoordinate2D(latitude: 37.5563, longitude: 126.9231)
    static let gbgPalace = CLLocationCoordinate2D(latitude: 37.5775, longitude: 126.9771)
    static let seoulLand = CLLocationCoordinate2D(latitude: 37.4328, longitude: 127.0185)
}

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    
    var body: some View {
        Map(position: $position){
            Marker("홍대입구역", systemImage: "star.fill" , coordinate: .hongikUniv)
            Marker("경복궁", coordinate: .gbgPalace)
            Marker("서울랜드", coordinate: .seoulLand)
        }
    }
}

 

홍대입구역, 경복궁, 서울랜드가 표시된다

 

Annotation(_:coordinate:content:)

Marker에서 SwiftUI 뷰로 직접 표시하려면 Annotation 뷰를 이용하면 된다

마찬가지로 CLLocationCooridnate2D로 위치정보가 필요하지만, 추가적으로 표시에 사용될 SwiftUI 뷰인 content도 전달해야 한다

 

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    
    var body: some View {
        Map(position: $position){
            Annotation("서울랜드", coordinate: .seoulLand) {
                Circle()
                    .fill(
                        LinearGradient(gradient: .init(colors: [.red, .orange, .yellow, .green, .blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)
                        )
                    .frame(width: 30)
            }
        }
    }
}

이전과 달리 직접만든 Circle 뷰로 나타난다

 

UserAnnotation()

Map 뷰에서 사용하는 뷰로 현재 위치를 표시하는데, 위치권한 허가가 되어있는 상태여야 나타난다

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    
    var body: some View {
        Map(position: $position){
            UserAnnotation()
        }
    }
}

파란색 동그라미 뷰다

 

MapCameraPosition

MapCameraPositionMap(position:)에서 position 인자로 사용된다

현재 Map 뷰에서 나타낼 위치를 설정하는 요소로 생각하면 되는데, 이때 해당 타입으로 사용될 수 있는 요소들은 다음과 같다

 

  • .automatic
  • .region(MKCoordinateRegion)
  • .item(MKMapItem)
  • .userLocation(fallback:)

 

.automatic

지도에 표시된 모든 MarkerAnnotation를 포함하도록 카메라 위치를 자동으로 조정한다

이전 예시에서, .automatic을 이용하여 3개의 Marker가 전부 보이도록 설정했다

 

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    
    var body: some View {
        Map(position: $position){
		/// Map Content

3개의 Marker가 전부 보이는 .automatic

 

.region(MKCoordinateRegion)

.region(MKCoordinateRegion)을 사용하면 지도의 카메라 위치를 특정 지역으로 설정할 수 있다

 

@State private var position: MapCameraPosition = .region(MKCoordinateRegion(
    center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
    span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
))

Map(position: $position) {
    /// Map content
}

 

.item(MKMapItem)

MKLocalSearch을 이용한 검색 결과는 MKMapItem인데, 이 검색결과를 바로 이용하는데 사용된다

검색결과를 Marker로 나타내고, 해당 Marker 뷰를 선택해서 selection으로 설정하게 되면 이를 바탕으로 position이 업데이트된다

 

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    @State private var searchResults: [MKMapItem] = []
    @State private var selection: MKMapItem? = nil
    
    var body: some View {
        Map(position: $position, selection: $selection) {
            ForEach(searchResults, id: \\.self) { place in
                Marker(place.name ?? "", systemImage: "pc", coordinate: place.placemark.coordinate)
            }
        }
        .onChange(of: selection) {
            if let selection = selection {
                position = .item(selection)
            }
        }
    }
}

 

.userLocation(fallback:)

현재 위치정보를 이용하여, 이곳이 중심이 되도록 카메라 위치를 설정하는데 사용된다

만약 위치 권한이 있는 상태라면 .userLocation을 통해 쉽게 위치정보를 이용할 수 있다

아래의 예시에선 Button을 누르면, Map 상에 현재 위치로 카메라를 설정한다

 

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
    
    var body: some View {
		ZStack(alignment: .bottom) {
			Map(position: $position){
				UserAnnotation()
			}    
			Button(action: {
				position = .userLocation(fallback: .automatic)
			}) {
				Image(systemName: "location.fill")
                	.font(.title2)
			}
		}
    }
}

 

MKMapItem

MKMapItem은 MKLocalSearch을 이용한 검색 결과

Map(selection:)에서 인자로도 사용되며, 이때 Map 뷰 위에 존재하는 Marker 뷰가 터치되면 확대되는 애니메이션이 적용된다(2)

 

MKMapItem은 다음과 같은 요소들을 포함한다

  • .placemark(MKPlaceMark)
  • .name
  • .phoneNumber
  • .url

 

개인적으로 MKMapItem은 MapKit에서 가장 다양하게 사용되는 요소인 것 같은데 나름 정리해 보면 다음과 같다

 

  1. MapCameraPosition에서 .item(MKMapItem)을 통해 이용할 수도 있다
  2. Map(selection:)에 MKMapItem을 바인딩해서 터치 시, 연동시킬 수도 있다
  3. MKMapItem.placemark.location?.coordinate를 Marker나 Annotation의 coordinate 인자에 전달하여 검색결과를 Map 뷰에 표시할 수 있다
  4. LookAroundPreview를 이용하면 검색결과인 MKMapItem을 바탕으로 주변을 볼 수 있는 뷰를 사용할 수 있다

 

.onMapCameraChange

.onMapCameraChange 수정자는 Map 뷰에서 현재 보고있는 위치가 업데이트 될 때마다 클로저를 수행한다

아래코드는 현재 보고있는 위치를 MKCoordinateRegion에 업데이트하는 예시다

 

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic
	@State private var visibleRegion: MKCoordinateRegion?

    var body: some View {
        Map(position: $position)
            .onMapCameraChange { context in
                  visibleRegion = context.region
        	}
    }
}

이는 나중에 MKLocalSearch.Request()의 .region에 전달하여 검색에 이용될 수도 있다

 

let searchRequest = MKLocalSearch.Request()
searchRequest.region = visibleRegion

 

LocalSearch와 LookAroundPreview

MKLocalSearch

MapKit에서 제공하는 핵심 기능 중 하나인 MKLocalSearch는 장소에 대한 정보를 얻는데 사용된다고 볼 수 있다

 

몇 가지 옵션을 지정하여 Reqeust를 만들어서 검색에 이용하고, Response를 통해 검색 결과를 얻는다

일단 검색과정을 나눠보면 다음과 같다

 

  1. MKLocalSearch Request 생성
    • .naturalLanguageQuery - 검색어
    • .region - 검색이 수행될 관련 지역
    • .resultTypes - 다음과 같은 옵션이 존재한다(배열로 여러 개 설정할 수도 있다)
      • .pointOfInterest - 관심 지점을 검색 결과에 포함
      • .address - 주소를 검색 결과에 포함
      • .business - 비즈니스 정보를 검색 결과에 포함
      • .phoneNumber - 전화번호를 검색 결과에 포함
      • .physicalFeature - 물리적 특징을 검색 결과에 포함
  2. Reqeust를 바탕으로 검색 수행
  3. 검색 결과인 Response에서 [MKMapItem]인 .mapItems를 이용
struct ContentView: View {
    @Binding var searchResults: [MKMapItem]
    
    /// ...

    func search(for query: String) {
		/// Request 생성
        let request = MKLocalSearch.Request()
        request.naturalLanguageQuery = query
        request.region = MKCoordinateRegion(center: .seoulLand, span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
		request.resultTypes = .pointOfInterest

        Task {
		    /// Request를 바탕으로 검색 수행
            let search = MKLocalSearch(request: request)
            let response = try? await search.start()
            /// 검색 결과에서 [MKMapItem] 추출
            searchResults = response?.mapItems ?? []
        }
    }
}

 

LookAroundPreview

MapKit에서 제공하는 핵심 기능 중 하나인 LookAroundPreview는 주변을 디테일하게 탐색해 볼 수 있는 뷰를 제공해 준다

다음과 같은 과정을 거쳐 MKLookAroundScene을 얻어 LookAroundPreview(scene:)를 이용할 수 있다

 

  1. MKLookAroundSceneRequest 생성(검색결과인 MKMapItem을 이용)
  2. LookAroundPreview(initialScene:)에 요청 결과인 MKLookAroundScene를 전달
struct ContentView: View {
    var mapItem: MKMapItem

    @State private var lookAroundScene: MKLookAroundScene?

    var body: some View {
            LookAroundPreview(scene: $lookAroundScene)
                .frame(height: 200)
                .cornerRadius(10)
                .onAppear {
                        updateLookAroundScene(for: mapItem)
                }
    }

    func updateLookAroundScene(for mapItem: MKMapItem) {		    
        Task {
            let request = MKLookAroundSceneRequest(mapItem: mapItem)
            do {
                lookAroundScene = try await request.scene
                if lookAroundScene == nil {
                    print("LookAround scene is not available for this location")
                }
            } catch {
                print("Error loading LookAround scene: \\(error)")
            }
        }
    }
}

한국은 해당 기능을 지원하지 않는건지 내가 찾아 본 지역들에서 전부 기능을 제공하지 않았다

 

프로젝트

지금까지 공부한 내용을 복습해 볼 겸 간단한 프로젝트를 만들어보았다

해당 앱은 현재 위치를 바탕으로 주변 PC방을 맵에 표시하거나, 맵에서 현재 보고 있는 영역에서 주변 PC방을 맵에 표시해 준다

 

마무리

핵심내용들을 다시 정리해 보면 다음과 같다

 

우선 MapKitMap 뷰를 제공한다

이때 클로저로 MarkerAnnotation 뷰를 사용할 수 있고, 이들은 CLLocationCooridnate2D를 통해 위치를 표시하게 된다

 

Map(selection:)을 이용하면 Map 뷰에 존재하는 Marker를 터치하면 선택된 것으로 설정할 수 있다

 

Map(position:)은 Map 뷰의 어떤 위치를 어떻게 보일 지를 설정하는데

.automatic, .item(MKMapItem), .userLocation(fallback:) 등을 통해 쉽게 요소들을 나타낼 수 있다

 

이뿐만 아니라 MapKitMKLocalSearchLookAroundPreview 같은 기능들도 제공해 준다

이때 MKLocalSearch의 결과 타입은 MKMapItem이다

 

 

이전에 MapKit을 사용한 프로젝트를 한 적이 있는데, 이번에 MapCameraPosition을 자세히 다뤄보면서 기존 코드를 엄청나게 개선할 수 있을 것 같다는 생각이 든다

기존에는 .automatic으로 모든 지점들이 나오게 할 수 있는 것을 몰라서, 모든 지점의 좌표를 통해 전부 나타낼 수 있는 Region을 직접 계산하여 복잡하게 구현했었다.. 코드를 간결하게 업데이트 할 수 있을 것 같다

'iOS+' 카테고리의 다른 글

UserNotifications  (0) 2024.08.11
EventKit  (0) 2024.08.03
AVFoundation  (0) 2024.07.14
App Intents  (0) 2024.07.06
상반기 후기와 후반기 계획  (0) 2024.06.29