MapKit을 사용하여 지도를 불러와 원하는 기능을 구현할 수 있습니다
간단하게 지도 사용해보기
MapKit 을 사용하기 위해서 가장 먼저 MapKit 을 불러와야 합니다.
import SwiftUI
import MapKit
지도를 불러오는 방법은 간단합니다. 다음과 같이 Map 만 호출하면 됩니다.
- Map 내부에는 Marker(마커) 또는 Annotation(주석) 을 꾸밀 수 있으며 coordinate 는 경도와 위도로 초기화하여 사용됩니다.
struct ContentView: View {
var body: some View {
Map {
Marker("San Francisco City Hall", coordinate: cityHallLocation)
.tint(.orange)
Marker("San Francisco Public Library", coordinate: publicLibraryLocation)
.tint(.blue)
Annotation("Diller Civic Center Playground", coordinate: playgroundLocation) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(Color.yellow)
Text("🛝")
.padding(5)
}
}
}
.mapControlVisibility(.hidden)
}
}
나의 위치 파악하기
내 위치 정보 불러오기 위한 기본 설정
나의 현재 위치를 파악하기 위해서는 위치 권한을 얻은 후 LocationManager 를 사용해야 합니다.
CLLocationManager 객체는 앱의 위치 관련 동작을 관리하는 중앙 관리자입니다.
CLLoactionManager 를 사용하여 위치 서비스를 구성하고 시작 및 멈출 수 있습니다.
* 유저의 현재 위치를 파악할 수 있다.
* 주변 지역에 대한 정보 또는 그 지역에 누가 왔고 떠났는지를 모니터링할 수 있다.
* 주변에 있는 블루투스 비콘을 알아낼 수 있다.
1개 또는 여러개의 CLLoactionManager를 만들어서 위치 데이터를 활용할 수 있습니다.
location-manager 객체를 만들면 Core Location이 위치가 변화를 감지했을때 어떻게 얼마나 자주 보고를 할 것인지를 설정해야합니다. 특히, 앱의 요구를 반영하는 값으로 distanceFilter와 desiredAccuracy 를 구성합니다.
distanceFilter: 사용자가 (최소 몇미터) 움직였을때 업데이트를 시켜야하는지
desiredAccuracy: 사용자가 값을 받고싶은 위치의 정확도
CLLoactionManager 객체는 모든 위치와 관련된 정보들을 CLLocationManagerDelegate 프로토콜을 준수하는 delegate 객체에 전달합니다.
location manager가 초기화를 마친 후 시스템이 앱의 권한 상태를 delegate의 locationManagerDidChangeAuthorization(_:) 메소드에 보고하기 때문에 location manager를 구성할 때 즉시 delegate를 할당해야 합니다.
CLLocationManager를 사용하는 스레드는 활성 상태인 RunLoop를 갖추어야 하며, 메인 스레드에서 작업하는 것이 일반적입니다.
코드를 보기 전에 우선 Info.plist 를 먼저 설정하여 사용자로부터 위치 권한을 요청합니다.
Location When In Use Usage Description: 앱을 실행 중일 때 위치 서비스를 사용할 권한을 요청합니다.
Location Always Usage Description: 앱이 백그라운드에서도 위치 서비스를 사용할 권한을 요청합니다.
이후 LocationManager 를 통해 위치 권한 여부를 확인한 후 권한이 없다면 요청합니다.
실수할 수 있는 점이 아래처럼 init() 으로 직접 권한을 요청하는 것이 아닌 Delegate 메서드를 통해 권한을 요청해야 합니다.
❌안되는 코드
@Observable
@MainActor class LocationManager {
let manager: CLLocationManager // 이게 진짜 LoactionManager이고 위치 권한에 대한 것들을 담당한다.
init() {
self.manager = CLLocationManager()
if self.manager.authorizationStatus == .notDetermined {
// 만약 권한이 없다면
self.manager.requestWhenInUseAuthorization()
}
}
}
⭕️ 동작하는 코드
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
switch manager.authorizationStatus {
case .notDetermined:
print("디버깅: notDetermined")
manager.desiredAccuracy = kCLLocationAccuracyBest
self.manager.requestWhenInUseAuthorization()
case .denied:
print("디버깅: denied")
case .restricted:
print("디버깅: restricted")
case .authorizedWhenInUse:
print("디버깅: authorizedWhenInUse")
manager.startUpdatingLocation()
default:
print("디버깅: default")
}
}
하지만 여기까지만 해서 다음과 같이 권한 요청이 나타나지 않는다면 다음과 같은 방법들을 시도해볼 수 있겠습니다
- info.plist 에 NSLocationWhenInUseUsageDescription 를 직접 넣기
info.plist 에 NSLocationAlwaysUsageDescription 를 직접 넣기
UserAnnotation 으로 내 위치 지도에 표시하기
다음과 같이 지도에 표시하기 위해서는 UserAnnotation() 을 사용해야 합니다.
Map() {
UserAnnotation()
}
내 위치로 카메라 이동
내 위치로 지도 뷰 이동시키기 위해서는 MapUserLocationButton 을 사용할 수 있겠습니다. 이 버튼은 MapKit 에서 기본적으로 제공하는 버튼으로 다음과 같이 사용될 수 있습니다.
아래 코드에서는 @Namespace 프로퍼티를 사용하여 Map의 scope 를 설정하였지만 이것은 Map 의 클로저 외부에서 버튼을 구현하고 싶을 때 Namespace 를 사용합니다.
@Namespace 는 애니메이션이나 뷰 간 상태 공유를 위해 사용되며 뷰들이 같은 @Namespace 를 공유할 때, 상태가 일관되게 유지하며, 뷰 간의 전환이나 상호작용을 보다 쉽게 관리할 수 있습니다.
즉 MapUserLocationButton 은 Map 의 mapControl 내부에서 동작하면 Map과 상호작용이 가능하지만
만약 Map 외부에서 따로 동작을 하기 위해서는 MapUserLocationButton으로 사용자의 위치로 지도를 이동시키려 할 때,
Map 과 버튼이 같은 @Namespace 내에 있어야 올바르게 상호작용할 수 있습니다.
struct LocationButtonTestView: View {
@Namespace var mapScope
var body: some View {
VStack {
Map(scope: mapScope)
MapUserLocationButton(scope: mapScope)
}
.mapScope(mapScope)
}
}
🔴 mapScope 를 적용하지 않으면 생기는 문제
이때 mapScope 를 적용하지 않는다면 다음과 같이 UI 적으로도 기능적으로도 제대로 동작하지 않는 것을 확인할 수 있습니다.
만약 mapControls 내부에 구현을 하게 된다면 다음과 같이 Namespace 를 사용하지 않고 구현됩니다.
🔴 중요한 점은 아래처럼 mapControls 내부에 적용하게 된다면 우측 상단에 위치가 고정됩니다.
Map()
.mapControls {
MapUserLocationButton()
}
변경되는 위치 불러오기
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
이 메서드는 사용자의 위치가 변경되었을 때 호출되는 콜백 함수입니다. 변경된 위치를 locations 에 배열로 담고 있으며 가장 최신 변경된 위치는 locations 배열의 마지막 원소에 해당합니다.
위 콜백함수는 다음과 같이 사용될 수 있습니다.
MKCoordinateRegion 이란 동그란 지역을 나타냅니다.
파라미터로는 center 와 span 이 존재하며 center 는 원의 중앙 기준 span은 확대 정도를 의미합니다.
var region: MKCoordinateRegion = MKCoordinateRegion()
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
locations.last.map {
region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
}
}
변경되는 위치 UI 에 적용하기
내 위치가 변경될 때마다 나의 위치에 맞게 지도가 움직일 수 있도록 다음과 같이 만들 수 있습니다.
Map(position: $position, scope: mapScope)
...
.onChange(of: locationManager.region, { oldValue, newValue in
position = .region(newValue)
})
탭한 부분 위치 가져오기
지도에서 내가 탭한 부분의 위치를 가져오기 위해서는 MapReader를 사용해야 합니다.
MapReader의 MapProxy 인스턴스를 사용하여 탭한 특정 부분의 경도와 위도를 convert 메서드로 변환할 수 있습니다.
MapReader { reader in
Map()
.onTapGesture(perform: { screenCoord in
if let pinLocation = reader.convert(screenCoord, from: .local) {
print(pinLocation)
}
})
}
탭한 부분 핀으로 만들기
위에서 탭한 부분의 위치를 가져왔으면 해당 위치를 저장하여 해당 위치를 핀으로 만들 수 있습니다.
핀은 Annotation 을 사용하여 만들 수 있습니다, Annotation은 Map 의 클로저 내부에 들어가 생성됩니다.
init(coordinate: CLLocationCoordinate2D, anchor: UnitPoint, content: () -> Content, label: () -> Label)
: coordinates(좌표), anchor(Content의 위치 설정), content, label 파라미터들을 사용하여 만들 수 있습니다.
@State private var tapPosition: CLLocationCoordinate2D?
MapReader { reader in
Map(position: $position, scope: mapScope) {
if let position = tapPosition {
Annotation(coordinate: position) {
Text("📍")
} label: { }
}
}
.onTapGesture(perform: { screenCoord in
if let pinLocation = reader.convert(screenCoord, from: .local) {
print(pinLocation)
tapPosition = pinLocation
}
})
}
Marker 마커 만들기
Maker란 pin과는 다릅니다. pin은 사용자가 정의하는 위치 표시 느낌이지만 Marker는 MKMapItem 을 사용하여 장소를 표현합니다. MKMapItem 을 사용하기 때문에 해당 장소에 대한 정보를 바탕으로 아이콘과 색상으로 만들어진 마커를 생성합니다.
Marker는 MKMapItem을 사용하지 않는다고 해도 직접 커스텀화 가능합니다.
Marker("Custom Marker", coordinate: CLLocationCoordinate2D(latitude: 37, longitude: 127))
Marker("Custom Marker2", systemImage: "car.fill", coordinate: .init(latitude: 37.0001, longitude: 127.0001))
//Marker("Custom Marker3", Image: "image", coordinate: .init(latitude: 37.0001, longitude: 127.0001))
monogram으로 다음과 같이 꾸밀 수도 있습니다.
Marker("Monogram1", monogram: Text("M1"), coordinate: .init(latitude: 37, longitude: 127))
.tint(.blue)
Marker("Monogram2", monogram: Text("M2"), coordinate: .init(latitude: 37.00001, longitude: 127.00004))
전화걸기
전화를 할 수 있는 기능은 다음 코드와 같이 구현됩니다.
func makeCall(phone: String) {
if let url = URL(string: "tel://\(phone)") {
if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url) // 만약에 가능한 전화번호면 전화를 엽니다
} else {
print("Device can't make phone calls")
}
}
}
🟢 버튼으로 만들어 전화걸기
Button(action: {
if let phone = mapItem.phoneNumber {
let numericPhoneNumber = phone.components(separatedBy: CharacterSet.decimalDigits.inverted).joined() // 숫자만 표기하도록 설정
makeCall(phone: numericPhoneNumber)
}
}, label: {
HStack {
Image(systemName: "phone.fill")
Text("Call")
}
}).buttonStyle(.bordered)
MKMapItem 이란?
🟢 MKMapItem 은 장소를 표현하기 위한 위치 정보를 포함한 인스턴스
지도에 특정한 지점뿐만 아니라 자신의 위치 또한 MKMapItem 으로 표현할 수 있겠습니다.
지도에서 특정 위치에 정보를 나타내거나 특정 위치를 저장하고 싶을 때에는 openMaps(with: launchOptions:) 메서드를 호출합니다.
openMaps 메서드는 지도 앱을 실행시켜 해당 위치를 사용자가 사용자가 지정한 정보와 함께 보여줍니다.
🟢 MKMapItem 을 사용하여 길찾기도 구현 가능
MKMapItem 은 길찾기 앱을 구현할 때에도 마찬가지로 사용되며 2개의 MKMapItem 객체를 사용하여 출발지과 목적지를 지정할 수 있습니다. 이렇게 MKMapItem 을 사용한다면 길찾기를 구현할 수도 있습니다.
🔵 MKMapItem 은 다음과 같은 데이터들을 포함하고 있습니다.
forCurrentLocation(): 현재 위치를 MKMapItem 로 받을 수 있습니다.
let item = MKMapItem.forCurrentLocation() // 현재 위치를 반환하는 싱글톤 MapItem 객체를 생성합니다
placemark: 위치 정보를 담고 있는 MKPlacemark 객체를 반환합니다.
let latitude = mapItem.placemark.coordinate.latitude
let longitude = mapItem.placemark.coordinate.longitude
pointOfInterestCategory: 해당 위치에 카테고리를 담고 있습니다.
let category = mapItem.pointOfInterestCategory // 레스토랑, 주유소, 공원 등
그외에도 다음과 같은 속성값들이 존재합니다.
var isCurrentLocation (현재 위치와 동일한지) -> Bool
var name: 해당 장소와 연관된 이름
var phoneNumber: 연락처, 전화번호
var url: 해당 장소와 연관된 URL
var timeZone: 장소의 시간대 정보
지도 스타일 설정하기
지도에는 3가지 종류의 스타일이 존재합니다.
- standard
- hybrid
- imagery
스타일에 사용되는 파라미터들
- elevation: 사물을 2D로 만들지 좀 더 현실적으로 만들지 결정할 수 있는 파라미터 입니다. (automatic, flat, realitic)
- pintsOfInterest: 지도 위에 표시할 관심 지점 카테고리를 정의할 수 있습니다.
- showsTraffic: 교통상황을 보여줄지를 결정하는 Bool 타입 옵션입니다
.standard(elevation: .automatic, pointsOfInterest: .some([.restaurant, .hospital]), showsTraffic: True)
Map()
.mapStyle(.hybrid(elevation: .realistic))
장소 검색하기
장소를 검색하기 위해서는 MKLocalSearch 클래스를 사용해야 합니다.
MKLocalSearch 클래스를 사용하면 단일 검색 요청을 실행할 수 있으며 검색을 통해 주소를 얻으 수 있습니다. 검색이 완료되면 클래스 객체는 completion handler를 통해 검색 결과를 전달합니다.
MKLocationSearch 클래스는 MKLocalSearch.Request를 통해 검색을 하기 때문에 해당 Request를 만들어야 합니다.
MLLocalSearch.Request 만들기
naturalLanguageQuery: 검색하고자 하는 String 검색어
resultTypes: 검색 결과의 유형을 결정할 수 있으며 address(주소) 와 pointOfInterest(관심지점) 가 존재합니다.
region: 어떤 지역을 기준으로 검색을 할 것인지 설정할 수 있습니다.
- center: 예를 들어 주차장 인근의 놀이터를 찾고 싶을 때 center에 값이 들어갑니다.
- span: 어떤 지역의 범위를 설정합니다.
만든 Request로 검색하기
Request를 만든 후 검색을 하기 위해서는 간단하게 start() 메서드를 사용하면 됩니다.
- 만일 검색을 취소하고 싶으면 cancle() 메서드를 호출하면 됩니다.
@Binding private var searchResults: [MKMapItem]
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: tapPosition!, span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
검색어 자동완성
검색창에서 연관 검색어 보이게하기 위해서는 MKLocalSearchCompleter를 을 사용해야 합니다.
MKLocalSearchCompleter 는 일부 String 으로 사용자가 원하는 특정 장소를 검색하는데 도와주는데 특정 장소들은 MKLocalSearchCompletion 값으로 반환됩니다.
인스턴스와 delegate 설정하기
가장 먼저 자동완성을 지원하는 completer 의 인스턴스를 만들어야 합니다.
이후 클래스를 delegate 로 만들어 completer 의 변화를 관찰할 수 있도록 합니다.
@Published var searchCompletions: [MKLocalSearchCompletion] = []
private var searchCompleter: MKLocalSearchCompleter
override init() {
self.searchCompleter = MKLocalSearchCompleter()
super.init()
self.searchCompleter.delegate = self
}
검색할 단어 적용하기
검색할 단어를 적용하기 위해서는 completer 에 원하는 특정 조건을 넣어 값을 가져옵니다.
아래에서는 String 을 조건으로 설정하였지만 그 외에도 아래와 같이 여러 타입을 조건문으로 사용할 수 있습니다.
var addressFilter: MKAddressFilter?
- 특정 국가나 지역을 제외하거나 포함하는 등의 주소 필터링을 설정할 때 사용
- 자동완성 결과를 받을 String 의 검색어
searchCompleter.queryFragment = "Starbucks"
var region: MKCoordinateRegion
- 검색할 지역의 지리적 범위를 설정하는 속성
searchCompleter.region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))
var regionPriority: MKLocalSearchRegionPriority
- 설정된 지역의 중요도를 지정하는 속성으로 지역의 priority 를 높이면 지역 내 결과가 우선 표시
• default: 기본 우선순위
• max: 해당 지역 범위를 강력하게 우선
searchCompleter.regionPriority = .max
var resultTypes: MKLocalSearchCompleter.ResultType
- 검색 결과에 포함할 항목 유형을 정의하는 속성으로 특정한 유형의 결과만 검색하고자 할 때 사용.
• address: 주소 관련 검색 결과
• pointOfInterest: 명소 관련 검색 결과
• query: 기존 검색 쿼리에서의 자동완성 결과
searchCompleter.resultTypes = [.address, .pointOfInterest]
var pointOfInterestFilter: MKPointOfInterestFilter?
- 특정 명소 카테고리를 포함하거나 제외하는 필터를 설정하는 속성
let filter = MKPointOfInterestFilter(including: [.cafe, .park])
searchCompleter.pointOfInterestFilter = filter
$text
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.sink(receiveValue: { [weak self] query in
self?.searchCompleter.queryFragment = query
})
.store(in: &searchCancellable)
자동완성된 값 불러오기
자동완성된 값은 delegate 콜백 함수를 통해 받아올 수 있습니다.
extension SearchViewModel: MKLocalSearchCompleterDelegate {
func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
self.searchCompletions = completer.results
}
}
카메라뷰 이동시키기
특정 동작을 한 후 자동으로 카메라 뷰를 이동시키고 싶을 때가 있습니다, 예를들어 지도에서 다른 곳을 보고 있다가 주변 카페를 검색했을 때 주변 카페를 기준으로 카메라를 이동시키고 싶은 상황이 있겠습니다.
다음과 같은 설정으로 검색 결과를 기준으로 위치를 지동으로 업데이트할 수 있습니다.
@State private var position: MapCameraPosition = .automatic
Map(position: $position)
.onChange(of: searchResults) { oldValue, newValue in
position = .automatic
}
아니면 특정 위치를 설정하여 position 바인딩을 통해 원하는 위치로 이동시킬 수도 있습니다.
A뷰
@State private var position: MapCameraPosition = .automatic
var body: some View {
// 검색을 위한 뷰
SearchButton(position: $position, searchResults: $searchResults)
// position을 사용하여 카메라 위치 조절 가능
Map(position: $position, scope: mapScope)
}
B뷰
@Binding var position: MapCameraPosition
Button {
position = .region(.init(center: .init(latitude: 127.1, longitude: 37.1), latitudinalMeters: .greatestFiniteMagnitude, longitudinalMeters: .greatestFiniteMagnitude))
} label: {
Text("이동")
}
MapCamera
지도 앱에서 표시되는 것들은 모두 MapCamear 가 관리합니다.
MapCameraPosition 카메라 위치
카메라(MapCamara)는 특정한 거리를 둔 채 좌표를 계산하며 카메라의 방향(MapCameraPosition)이 지도에 보이는 것을 결정합니다.
MapCamera를 만들거나 구성하는 대신에 MapCameraPosition을 사용하면 표시해야 할 것만 따로 특정시킬 수 있습니다.
MapCameraPosition 은 어떤 지역이나 장소가 표시될지를 조절할 수 있습니다
MapCameraPosition 설정하기
다음은 MapCameraPosition 설정하는 방법들입니다
- camera(MapCamera): MapCamera 에 따른 MapCameraPosition 를 생성합니다.
- region(MKCoordinateRegion): MKCoordinateRegion 에 따른 MapCameraPosition 를 생성합니다.
- rect(MKMapRect): MKMapRect 에 따른 MapCameraPosition 를 생성합니다.
- item(MKMapItem): MKMapItem 에 따른 MapCameraPosition 를 생성합니다.
- userLocation(): 사용자의 위치에 따른 MapCameraPosition 를 생성합니다
camera 로 MapCameraPosition 생성하기
distance: 카메라가 중심 좌표에서 떨어진 거리를 미터 단위로 설정
heading: 카메라가 바라보는 방향(헤딩)을 설정. 값은 degrees 로 지정되며 242는 대략 서남서 방향입니다.
pitch: 카메라의 각도(pitch) 를 설정합니다. 카메라가 땅을 향해 기울어진 각도를 나타냅니다.
position = .camera(
MapCamera(centerCoordinate:
CLLocationCoordinate2D(
latitude: 42.360431,
longitude: -71.055930
),
distance: 980,
heading: 242,
pitch: 60
)
)
이때 pitch(각도)를 잘 설정하면 아래처럼 3D 화면으 표현하기 좋습니다.
userLocation 사용하여 사용자 위치로 MapCameraPosition 설정하기
지도의 카메라 위치를 사용자 위치로 설정하는 것을 의미합니다.
fallback: automatic은 만약 사용자의 위치를 추적하지 못하거나 오류가 발생했을 경우, fallback 옵션으로 대체할 위치를 지정합니다.
position = .userLocation(fallback: .automatic)
userLocation으로 설정했을 때 변화하는 프로퍼티 값
카메라가 사용자의 위치를 따라오고 있는지 나타내는 followsUserLocation 은 true 로 변경됩니다.
- 카메라의 위치 상태를 userLocation 으로 설정한다면 true 아니면 false 값을 갖게 됩니다.
position.followsUserLocation == true
만약 사용자가 화면을 이동하면 카메라는 더 이상 따라오지 않고 positionedByUser 는 true 로 변경됩니다.
- 즉 사용자가 지도와 상호작용은 한다면 true 값으 갖게 됩니다.
position.positionedByUser == true
카메라가 이동된 후 지역 검색
여행을 했을 때 특정 위치의 카페를 찾고 싶을 수 있습니다. 예를들어 화면을 드래그하여 다른 지역으로 이동 후 이동된 해당 지역에서 검색을 해야하는 경우가 존재합니다. 이런 상황에서는 어떻게 해야하는지 다루겠습니다.
🟢 onMapCameraChange 메서드로 지도 이동을 감지합니다.
onMapCameraChange 메서드는 지도 카메라의 프레임이 변경을 감지하고 동작을 수행합니다.
frequency: 클로저의 동작이 언제 수행될 지 지정하는 파라미터 입니다.
- continuous(상호작용 내내 수행)
- onEnd(변경이 끝났을 때에 수행)
클로저로 받는 인자값(MapCameraUpdateContext)은 다음과 같이 사용됩니다.
우선 이동된 지역을 저장하기 위한 프로퍼티 visibleRegion 을 만들어줍니다.
이후 onMapCameraChange 메서드를 사용하면 클로저로 받는 인수값을 사용하여 이동된 카메라 기준의 지역을 저장합니다.
@State private var visibleRegion: MKCoordinateRegion?
// @State private var visibleCamera: MapCamera?
// @State private var visibleRect: MKMapRect?
Map()
.onMapCameraChange { context in
visibleRegion = context.region
// visibleCamera = context.camera
// visibleRect = context.rect
}
🟢 변경된 region 프로퍼티 값을 사용하여 검색
이후 지도의 변경이 있다면 onMapCameraChange 로 인해 visibleRegion 으로 저장된 지역을 가지고 MKLocalSearch.Request() 에 사용하여 검색합니다.
var visibleRegion: MKCoordinateRegion?
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = visibleRegion ?? MKCoordinateRegion(
center: locationManager.region.center, span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125))
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
print(searchResults)
}
locationManager.requestLocation()
}
마커
마커 선택할 수 있게 만들기
마커를 선택할 수 있으려면 선택된 마커를 저장하기 위한 프로퍼티 래퍼를 만들어야 합니다.
@State private var selectedResult: MKMapItem?
Map 에 selection 파라미터에 바인딩 프로퍼티를 넣게 된다면 선택된 마커를 저장할 수 있을 뿐만 아니라 강조도 할 수 있게 됩니다.
Map(position: $position, selection: $selectedResult, scope: mapScope)
마커를 탭하게 된다면 다음과 같이 선택되었다고 알려 주는 애니메이션이 나옵니다.
tag 를 사용하여 마커 저장하기
tag 를 사용한다면 커스텀 마커를 따로 사용 및 focused 할 수도 있습니다.
@State private var selectedTag: Int?
var body: some View {
Map(selection: $selectedTag) {
Marker(item: mapItem)
.tag(1)
Marker("Not a MapItem", coordinate: .place)
.tag(2)
}
}
LookAroundPreview 로 장소 미리보기
LookAroundPreview란 아래 사진처럼 장소를 미리볼 수 있는 UI 를 제공하고 있습니다.
LookAroundPreview 사용하기
위에서 나온 것처럼 LookAroundPreview 는 다음과 같이 코드로 작성됩니다.
initialScene 메서드에 MKLookAroundScene 이 입력받아지면 해당 인스턴스 정보에 맞는 장소 미리보기가 보여집니다.
이후 overlay 혹은 다른 방법을 통해서 해당 뷰에 대한 정보나 걸리는 시간 등을 표현할 수 있습니다.
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
🟢 MKLookAroundSceneRequest 로 MKLookAroundScene 가져오기
MKLookAroundScene 데이터를 받기 위해서는 MKLookAroundSceneRequest 를 사용해야 합니다.
MKLookAroundSceneRequest 에 MKMapItem 인 selectedResult 를 담으면 MKLookAroundScene 를 반환하는 Request 객체를 만들 수 있으며 해당 객체의 scene 을 통해서 MKLookAroundScene 을 받아 LookAroundPreview에 사용할 수 있습니다.
func getLookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
LookAroundPreview 전체 코드
struct LookAround: View {
@State private var lookAroundScene: MKLookAroundScene?
var selectedResult: MKMapItem
var route: MKRoute?
var body: some View {
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
.onAppear {
getLookAroundScene()
}
.onChange(of: selectedResult) { oldValue, newValue in
getLookAroundScene()
}
}
func getLookAroundScene() {
lookAroundScene = nil
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
private var travelTime: String? {
guard let route else { return nil }
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.hour, .minute]
return formatter.string(from: route.expectedTravelTime)
}
}
길찾기
MKDirection.Request 으로 경로 계산하기
🟢 MKDirection.Request 으로 경로 계산할 수 있습니다
길찾기 기능을 구현하기 위해서는 해당 경로를 계산 요청하기 위한 MKDirections.Request() 이 필요합니다.
MKDirection.Request 는 다음과 같은 요청 옵션으로 구성될 수 있습니다.
var source: MKMapItem?
경로의 시작점을 지정할 수 있습니다.
var destination: MKMapItem?
경로의 목적지를 지정할 수 있습니다.
var transportType: MKDirectionsTransportType
교통수단의 타입을 지정할 수 있습니다. (automobile, walking, transit, any)
var highwayPreference: MKDirections.RoutePreference
고속도로 선호 여부를 지정할 수 있습니다. (avoid, any)
var tollPreference: MKDirections.RoutePreference
톨비 요금소 선호 여부를 지정할 수 있습니다.
var requestsAlternateRoutes: Bool
여러개의 경로가 가능할 때 해당 경로들을 나타낼 지를 지정할 수 있습니다
var departureDate: Date? & var arrivalDate: Date?
출발 시간과 도착 시간을 설정합니다
다음은 MKDirection.Request 를 사용하여 경로를 계산하는 함수를 구현한 코드입니다.
var route: MKRoute?
func getDirections() {
route = nil
guard let selectedResult else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .init(latitude: 36, longitude: 127)))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
애플맵으로 길찾기
길을 찾기 위해서는 목적지를 정해야 합니다. 그렇기 위해서 지도에서 탭을 한 지점을 기준으로 길을 찾겠습니다.
원하는 목적지를 저장하기 위해서는 MKMapItem 타입으로 저장해야 합니다.
@State var mapItem: MKMapItem = .forCurrentLocation()
.onTapGesture(perform: { screenCoord in
if let pinLocation = reader.convert(screenCoord, from: .local) {
print(pinLocation)
tapPosition = pinLocation
mapItem = MKMapItem(placemark: MKPlacemark(coordinate: pinLocation))
}
})
이후 저장된 목적지를 찾기 위해서는 다음 코드를 사용하면 됩니다. 아래 버튼을 누르게 되면 해당 위치의 길을 Apple Map 으로 길찾기가 실행됩니다.
let mapItem: MKMapItem
Button(action: {
MKMapItem.openMaps(with: [mapItem]) // 애플맵을 열어서 경로를 안내한다
}, label: {
HStack {
Image(systemName: "car.circle.fill")
Text("Take me there")
}
}).buttonStyle(.bordered)
.tint(.green)
MKMapItem 에서 launchOptions 사용
openMaps 에는 launchOptions 를 사용하면 애플 맵을 열 때 추가적인 동작을 수행하도록 만들 수 있습니다.
MKMapItem.openMaps(with:[MKMapItem], launchOptions: [String : Any]?)
let mapItem: MKMapItem
mapItem.openInMaps(launchOptions: [String : Any]? = nil) -> Bool
🔵 launchOptions 의 종류
MKLaunchOptionsDirectionsModeKey
설명: 지도가 열릴 때 사용자가 선택한 교통수단에 맞춰 경로를 제시하는 모드입니다.
- MKLaunchOptionsDirectionsModeDriving (자동차 모드)
- MKLaunchOptionsDirectionsModeWalking (도보 모드)
- MKLaunchOptionsDirectionsModeTransit (대중교통 모드)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMaps(launchOptions: launchOptions)
MKLaunchOptionsMapTypeKey
설명: 지도가 열릴 때 지도 유형을 설정할 수 있는 키입니다. 예를 들어, 기본 지도(standard), 위성 지도(satellite), 혼합 지도(hybrid) 중에서 선택할 수 있습니다.
- MKMapType.standard.rawValue (기본 지도)
- MKMapType.satellite.rawValue (위성 지도)
- MKMapType.hybrid.rawValue (혼합 지도)
let launchOptions = [MKLaunchOptionsMapTypeKey: MKMapType.satellite.rawValue]
mapItem.openInMaps(launchOptions: launchOptions)
MKLaunchOptionsMapCenterKey
설명: 지도를 열 때 중심으로 설정할 좌표를 지정하는 키입니다. 이 값을 사용하면 지도가 특정 좌표를 중심으로 표시됩니다.
let centerCoordinate = CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194)
let launchOptions = [MKLaunchOptionsMapCenterKey: NSValue(mkCoordinate: centerCoordinate)]
mapItem.openInMaps(launchOptions: launchOptions)
MKLaunchOptionsMapSpanKey
설명: 지도가 열릴 때 어느 정도의 범위를 보여줄지를 지정할 수 있는 키입니다. 이 값은 위도와 경도의 범위를 정의하여 지도의 줌 수준을 설정합니다.
let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
let launchOptions = [MKLaunchOptionsMapSpanKey: NSValue(mkCoordinateSpan: span)]
mapItem.openInMaps(launchOptions: launchOptions)
MKLaunchOptionsShowsTrafficKey
설명: 지도에서 실시간 교통 정보를 표시할지 여부를 결정하는 키입니다. 이 값을 통해 교통 상황을 지도로 보여줄지 말지를 설정할 수 있습니다. (동작하는지 여부는 불확실)
let launchOptions = [MKLaunchOptionsShowsTrafficKey: true]
mapItem.openInMaps(launchOptions: launchOptions)
MKLaunchOptionsCameraKey
설명: 특정한 카메라 뷰로 지도를 보여줄 수 있는 키입니다. 이 값은 MKMapCamera 객체로, 사용자가 보는 각도나 방향을 제어할 수 있습니다.
let camera = MKMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), fromDistance: 1000, pitch: 45, heading: 0)
let launchOptions = [MKLaunchOptionsCameraKey: camera]
mapItem.openInMaps(launchOptions: launchOptions)
계산된 경로 사용하기 MKRoute
🟢 반환된 경로 정보는 MKRoute 로 반환됩니다
MKRoute는 출발지와 도착지 사이의 계산된 경로에 대한 정보를 정의하는 객체입니다.
이 클래스는 MKDirections.Reponse 객체에 의해 생성된 경로이며 다음과 같은 프로퍼티와 메서드들이 존재합니다.
var polyline: MKPolyline
경로의 세부적인 지리적 선형 정보를 나타냅니다.
var steps: [MKRoute.Step]
경로의 각 단계를 나타내는 배열입니다. MKRoute.Step 객체로 경로를 나누어 단계별로 설명합니다.
각각의 Step은 한 부분의 경로를 설명하며, 예를 들어 "오른쪽으로 500m 이동" 같은 지시사항이 포함될 수 있습니다.
var name: String
경로에 할당된 이름입니다. 도로 이름이나 루트 이름이 포함됩니다.
var hasHighways: Bool
경로에 고속도로가 포함되어 있는지 여부를 나타내는 Bool 값입니다.
var expectedTravelTime: TimeInterval
경로의 소요 시간을 초 단위로 반환합니다.
var transportType: MKDirectionsTransportType
전체적인 경로의 교통 수단을 반환합니다.
MapPolygon 으로 경로 선 그리기
🟢 경로 선으로 표시하기
계산된 MKRoute 를 사용하면 경로를 선으로 만들어낼 수 있습니다.
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
MKCoordinateRegion, CLLocation 으로 거리 구하기
🟢 두 좌표를 안다면 좌표 간의 거리를 계산할 수 있습니다.
- MKCoordinateRegion 은 lat,long 정보를 포함하고 있는 center: CLLocationCoordinate2D 를 가지고 있으며 이를 CLLocation 으로 거리를 계산할 수 있습니다.
- CLLocation 에서는 cooridnate (long, lat) 과 같은 위치 정보 뿐만 아니라 두 좌표의 거리를 m (미터) 단위로 계산해주는 메서드 distance(from: CLLocation) 가 존재합니다.
/// 두 좌표 간의 거리를 계산하는 함수
func calculateDistance(lat1: CLLocationDegrees, lon1: CLLocationDegrees,
lat2: CLLocationDegrees, lon2: CLLocationDegrees) -> CLLocationDistance {
let loc1 = CLLocation(latitude: lat1, longitude: lon1)
let loc2 = CLLocation(latitude: lat2, longitude: lon2)
return loc1.distance(from: loc2) // 미터 단위로 반환
}
func checkNearMe(cameraRegion: MKCoordinateRegion) -> Bool {
// 두 좌표 간의 거리를 계산
manager.requestWhenInUseAuthorization()
let distance = calculateDistance(lat1: region.center.latitude, lon1: region.center.longitude,
lat2: cameraRegion.center.latitude, lon2: cameraRegion.center.longitude)
if distance > 500 { // 카메라 뷰 중앙으로부터 500미터보다 멀리 떨어져있으면
return false
} else {
return true
}
}
MapCircle 사용해서 지도에 원 그리기
MapCircle 를 사용한다면 다음처럼 구현하여 원하는 위치에 원을 그릴 수 있습니다.
MapCircle(center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), radius: 1000)
.stroke(Color.blue, lineWidth: 2)
.foregroundStyle(Color.gray.opacity(0.3))
구글맵에서 제공하는 기본 Annotation 선택하기
🟢 구글 맵에 들어가면 기본적으로 제공되는 Annotation 들과 사용자가 상호작용이 가능합니다
애플 맵에서 기본적으로 제공하는 여러 Annotation들이 있습니다. WWDC22 발표 전까지는 개발자가 만든 Annotation 을 제외한 다른 지도의 Annotation 들과는 상호작용을 하지 못했지만 현재는 가능합니다.
Selectable Map Features API
선택을 하기 위해서는 Selectable Map Features API 을 사용해야 합니다.
Selectable Map Features API 에는 다음과 같은 옵션을 제공합니다.
- POI(Points of Interest); 상점, 레스토랑, 랜드마크 등의 관심 지점
- Territories: 도시, 주 등의 지형
- Physical features: 산맥, 호수 등
API 를 사용하는 방법은 다음과 같습니다.
1. 위에서 언급한 3가지 종류 (POI, Territories, Physical features) 중 하나를 선택합니다.
mapFeatureSelectionContent 로 지도 기본 마커 탭하기
SwiftUI 에서는 mapFeatureSelectionContent 를 사용하여 지도에 존재하는 마커를 탭하여 원하는 정보와 함께 띄우 수 있습니다.
@State private var mapFeature : MapFeature?
var body: some View {
Map(selection:$mapFeature)
.mapFeatureSelectionContent { feature in
if let image = feature.image {
Marker(feature.title ?? "", coordinate: feature.coordinate)
}
}
}
🔴 mapFeatureSelectionContent 의 문제점
mapFeatureSelectionContent 의 문제점은 MapFeature 과 MKMapItem 이 동시에 selection 으로 사용될 수 없는 것입니다.
selection 으로는 하나의 바인딩 값만 받기에 지도에 존재하는 기본 마커는 탭이 가능하지만 검색 결과 또는 동적인 사용자 지정 마커 같은 경우에는 탭이 되지 않는다는 것입니다.
'SwiftUI' 카테고리의 다른 글
WWDC24 Translation API (번역 API) (3) | 2024.10.05 |
---|---|
WWDC20 Data Enssentials in SwiftUI (EnvironmentObject, ObservableObject, ObservedObject, StateObject, AppStorage, SceneStorage, 생명주기) (1) | 2024.10.03 |
SwiftUI - iCloud 사용하기 (0) | 2024.09.05 |
Swift - 대소문자 바꾸기 (아스키코드, map) 2가지 방법 (0) | 2024.08.23 |
SwiftUI - 버튼, 텍스트 터치 영역 확장시키기 (0) | 2024.08.18 |