이전시간에는 롯데타워 주변에 주차장과, 병원을 Marker로 찍어 봤습니다!
이번 시간에는 좀 더 심화한 내용인
- 지도의 카메라 개념
- 지도의 영역 값을 얻는 방법
- 지도의 영역 값을 기준으로 특정 장소(주차장, 병원)를 검색하는 방법
- MKLookAroundScene을 사용하여 프리뷰를 보는 방법
- 사용자의 현 위치를 기점으로 특정 장소와의 거리 계산 표시 방법 등
다양한 심화 내용을 학습해 보겠습니다.
최종적으로 아래와 같은 앱을 만들어 볼거에요!!
지도를 부산, 강릉으로 이동시켜 보자
롯데타워에서 멀리 이동하여 주차장, 병원을 검색하면 지도에 롯데타워 근처의 결과가 더 이상 자동으로 표시되지 않습니다.
사용자가 지도와 상호작용한 후 검색 결과를 표시하려면 지도가 마커의 프레임이 되도록 지도의 카메라 위치 상태를 다시 설정해야 합니다.
바다가 이쁜 부산, 강릉 위도 경도를 이용해서 버튼을 누르면 해당 지역으로 이동할 수 있게
그리고 롯데타워 주변 주차장, 병원 버튼을 누르면 자동으로 이동할 수 있게 지도의 카메라를 조절해 보겠습니다.
extension MKCoordinateRegion {
static let busan = MKCoordinateRegion (
center: CLLocationCoordinate2D( latitude: 35.1795543, longitude: 129.0756416),
span: MKCoordinateSpan ( latitudeDelta: 0.1, longitudeDelta: 0.1)
) // 부산 좌표
static let gangneung = MKCoordinateRegion (
center: CLLocationCoordinate2D( latitude: 37.751853, longitude: 128.8760574),
span: MKCoordinateSpan( latitudeDelta: 0.5, longitudeDelta: 0.5)
) // 강릉 좌표
}
span이 뭐지?
span은 지도의 경도 및 위도 방향으로 얼마나 넓게 보여줄지에 대한 값입니다.
MKCoordinateSpan 클래스를 사용하여 정의하며, latitudeDelta는 위도 방향으로의 크기, longitudeDelta는 경도 방향으로의 크기를 나타냅니다.
여기서 주어진 코드에서 span은 두 개의 MKCoordinateRegion에 대해 각각 다르게 설정되어 있습니다:
- busan: **latitudeDelta**와 **longitudeDelta**가 모두 0.1로 설정되어 있습니다. 이는 부산 지역을 표시할 때, 지도가 위도와 경도 방향으로 각각 0.1만큼의 범위를 갖도록 하는 것입니다.
- gangneung: **latitudeDelta**와 **longitudeDelta**가 모두 0.5로 설정되어 있습니다. 이는 강릉 지역을 표시할 때, 지도가 위도와 경도 방향으로 각각 0.5만큼의 범위를 갖도록 하는 것입니다.
따라서 span을 조절하면 지도의 확대 수준이나 축소 수준을 조절할 수 있습니다. 더 큰 latitudeDelta 및 longitudeDelta 값은 더 큰 영역을 보여주게 되어 지도가 더 크게 확대됨을 의미합니다.
⭐️⭐️⭐️
// 카메라 위치추적 변수 지도에 추가한 콘텐츠를 화면에 잡아주는 기본 automatic
@State private var position: MapCameraPosition = .automatic
MapCameraPosition은 MapKit에서 사용되는 열거형(enumeration)으로, 지도의 카메라 위치와 관련된 여러 가지 옵션을 나타냅니다. 이 열거형은 Map 뷰의 초기화에서 position 매개변수로 사용되어 카메라의 초기 위치를 설정하거나, 지도를 이동할 때 카메라의 동작을 제어하는 데 사용됩니다.
주요 MapCameraPosition 옵션은 다음과 같습니다:
- .automatic: 이 옵션은 지도를 자동으로 이동하게끔 카메라를 설정합니다. 주로 사용자의 현재 위치를 중심으로 지도를 표시하고, 사용자가 이동하면 카메라가 자동으로 이동하여 위치를 유지합니다.
- .coordinated(latitude:longitude:latitudeDelta:longitudeDelta:): 이 옵션은 특정 좌표를 중심으로 하고, 지정된 위도와 경도의 범위로 지도를 표시합니다. 주어진 좌표를 중심으로 하되, 사용자의 이동에 따라 자동으로 지도를 갱신하지는 않습니다.
- .none: 이 옵션은 카메라를 사용하지 않고, 특정 위치로 이동하지 않습니다. 즉, 초기 지도 상태를 고정시킵니다.
이러한 옵션들은 Map 뷰를 초기화할 때 position 매개변수에 전달되어 사용되며, 사용자가 지도와 상호 작용할 때마다 카메라의 위치를 조정하는 데 사용됩니다. MapCameraPosition을 통해 지도를 초기화하고 설정함으로써 사용자에게 더 편리하고 직관적인 지도 경험을 제공할 수 있습니다.
전체코드
//
// ContentView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
extension CLLocationCoordinate2D {
static let lotteTower = CLLocationCoordinate2D(latitude: 37.5125, longitude: 127.102778)
}
extension MKCoordinateRegion {
static let busan = MKCoordinateRegion (
center: CLLocationCoordinate2D( latitude: 35.1795543, longitude: 129.0756416),
span: MKCoordinateSpan ( latitudeDelta: 0.1, longitudeDelta: 0.1)
) // 도시좌표
static let gangneung = MKCoordinateRegion (
center: CLLocationCoordinate2D( latitude: 37.751853, longitude: 128.8760574),
span: MKCoordinateSpan( latitudeDelta: 0.5, longitudeDelta: 0.5)
) // 해안 좌표
}
struct ContentView: View {
// 💁 카메라 위치추적 변수 지도에 추가한 콘텐츠를 화면에 잡아주는 기본 automatic
@State private var position: MapCameraPosition = .automatic
@State private var searchResults: [MKMapItem] = []
var body: some View {
Map(position: $position){ // 💁 Map의 카메라를 초기화 할수있게 설정
Annotation("lotteTower", coordinate: .lotteTower) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "house.circle.fill")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
}
.mapStyle(.standard)
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
// 💁 $position(카메라)넣어주기
BeanTownButtons(position: $position, searchResults: $searchResults)
.padding(.top)
Spacer()
}
.background(.ultraThinMaterial)
}
.onChange(of: searchResults){
position = .automatic // 💁 카메라 업데이트
// 이제 다른곳에 있어도 지도를 직접 올려서 찾아 가지 않아도 됨
}
}
}
#Preview {
ContentView()
}
부산, 강릉 이동 버튼 추가
//
// BeantownButtonView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
struct BeanTownButtons: View {
@Binding var position: MapCameraPosition // 💁 카메라 위치상태 바인딩
@Binding var searchResults: [MKMapItem]
var body: some View {
HStack {
Button {
search(for: "parking")
} label: {
Label("parking lot", systemImage: "car")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "hospital")
} label: {
Label("restroom", systemImage: "cross.case")
}
.buttonStyle(.borderedProminent)
//💁 부산, 강릉으로 이동 버튼
Button {
position = .region(.busan) // 부산 표시
} label: {
Label("Busan", systemImage: "figure.open.water.swim")
}
.buttonStyle (.bordered)
Button {
position = .region(.gangneung) // 강릉 표시
} label: {
Label("Gangneung", systemImage: "figure.sailing")
}
.buttonStyle (.bordered)
}
.labelStyle(.iconOnly)
}
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
request.region = MKCoordinateRegion(
center: .lotteTower,
span: MKCoordinateSpan (latitudeDelta: 0.0125, longitudeDelta: 0.0125)
)
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
이번 part에서는
MapCameraPosition을 이용하여 지도의 화면을 조절해 보았습니다.
사용자가 지도를 움직여 보이는 영역에서 검색하기
종로구에서 가까운 주차장, 병원 찾기, 부산지역 주차장 찾기, 강릉지역 병원 찾기
카메라가 바뀔 때 눈앞에 보이는 지도 영역의 값을 얻는 방법
.
.
.
struct ContentView: View {
// 지도에 표시되는 지역을 추적하기 위해 상태를 추가
@State private var visibleRegion: MKCoordinateRegion?
.
.
.
.mapStyle(.standard)
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
//💁
BeanTownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
Spacer()
}
.background(.ultraThinMaterial)
}
.onChange(of: searchResults){
position = .automatic
}
.onMapCameraChange { context in
visibleRegion = context.region
}
/* 💁
onMapCameraChange에 제공된 클로저는 사용자가 지도와의 상호작용을 마치면 호출됩니다. 사용자가 지도와 상호작용하는 동안 클로저를 호출하려면 빈도 매개변수를 전달하여 지속적인 업데이트를 요청할 수 있습니다.
*/
}
}
#Preview {
ContentView()
}
코드설명
MKCoordinateRegion은 MapKit 프레임워크에서 사용되는 구조체로, 지도의 특정 영역을 정의합니다.
이 구조체는 중심 좌표 (center)와 지도 영역의 크기를 나타내는 스팬 (span)으로 구성됩니다.
- center: 중심 좌표는 해당 영역의 지도에서 가운데 위치하는 지점의 위도와 경도입니다.
- span: 스팬은 지도 영역의 크기를 나타내며, 위도 방향과 경도 방향의 크기를 각각 latitudeDelta와 longitudeDelta로 정의합니다.
즉 MKCoordinateRegion을 사용하면 특정 지점을 중심으로 하는 지도 영역을 정의할 수 있습니다.
즉 사용자가 바라보고 있는 지도의 한 부분을 저장하기 위한 변수 입니다.
.onMapCameraChange
swiftCopy code
.onMapCameraChange { context in
visibleRegion = context.region
}
- .onMapCameraChange: SwiftUI에서 제공하는 modifier로, 사용자가 지도와의 상호작용을 마치면 호출되는 클로저를 등록하는 역할을 합니다. 사용자가 지도와 상호작용하는 동안 해당 클로저를 지속적으로 호출하려면 빈도 매개변수를 사용할 수 있습니다.
- { context in visibleRegion = context.region }: 클로저 내부에서 context 매개변수를 받아와서 사용합니다. context.region은 사용자가 지도를 조작할 때 현재 지도 영역에 대한 정보를 제공합니다. 이 정보를 사용하여 visibleRegion 상태 변수를 업데이트합니다. 따라서 지도의 카메라가 변경될 때마다 visibleRegion이 해당 지도 영역을 반영하게 됩니다.
이 부분은 지도의 카메라가 변경될 때마다 해당 변경사항을 감지하고, 그에 따라 visibleRegion을 업데이트하여 현재 지도가 보여주는 영역을 추적하는데 사용됩니다.
쉽게 생각해서 이렇게 해주면 사용자가 지도를 움직일 때 보이는 지도의 부분에 대한 영역을 저장할 수 있습니다.
region이 뭔가요?
region은 onMapCameraChange 클로저의 context 매개변수에서 제공되는 속성 중 하나로, 사용자가 지도와의 상호 작용 중에 현재 지도 영역에 대한 정보를 포함합니다. 이 정보는 MKCoordinateRegion 타입으로 제공되며, 지도의 특정 영역을 정의합니다.
MKCoordinateRegion은 다음과 같은 속성을 포함합니다:
- center: 중심 좌표는 해당 영역의 지도에서 가운데 위치하는 지점의 위도와 경도입니다.
- span: 스팬은 지도 영역의 크기를 나타내며, 위도 방향과 경도 방향의 크기를 각각 **latitudeDelta**와 **longitudeDelta**로 정의합니다.
즉, context.region은 현재 지도의 영역을 정의하는 MKCoordinateRegion 객체를 나타냅니다. 따라서 onMapCameraChange 클로저 내에서 이 정보를 활용하여 지도의 변경사항에 대응하는 데 사용할 수 있습니다.
//
// BeantownButtonView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
struct BeanTownButtons: View {
.
.
.
var visibleRegion: MKCoordinateRegion? // 💁 사용자에게 표시되는 지역 내에서 검색되도록 BeantownButton을 업데이트
.
.
.
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
// 💁검색을 할때 사용자가 바라보고 있는 지도의 한부분에 대한 좌표를 가지고 검색
request.region = visibleRegion ?? MKCoordinateRegion(
center: .lotteTower,
span: MKCoordinateSpan (latitudeDelta: 0.0125, longitudeDelta: 0.0125)
)
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
street view 정보 표시 (지역 Preview, 지역이름, 현 위치로부터 도착 시간)
아쉽게 한국은 MKLookAroundScene 표시가 아직 많이 지원되지 않습니다. 다른 나라를 기준으로 잡고 결과를 확인해 보세요
//
// ContentView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
struct ContentView: View {
.
.
.
@State private var selectedResult: MKMapItem? //여러 결과중 하나 선택하여 담기 위함
// 주차장에서 선택한 검색결과까지 route정보를 구하기
@State private var route: MKRoute?
var body: some View {
// selection: $selectedResult 추가
Map(position: $position, selection: $selectedResult){
.
.
.
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
VStack(spacing:0) {
// 💁 여러 결과중 하나를 선택하면 해당 선택한 MKMapItem 과 출발지(롯데타워)부터 목적지까지의 거리를 전달
if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle (cornerRadius: 10))
.padding([.top, .horizontal])
}
BeanTownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
}
Spacer()
}
.background(.ultraThinMaterial)
}
.onChange(of: searchResults){
position = .automatic
getDirections() // 실행
}
.onMapCameraChange { context in
visibleRegion = context.region
}
}
func getDirections() {
route = nil
guard let selectedResult else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .lotteTower))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
#Preview {
ContentView()
}
Map(position: $position, selection: $selectedResult) 설명
여기서 selection: $selectedResult를 사용하는 것은 사용자가 지도에서 어떤 지점을 선택했을 때
selectedResult 변수를 업데이트하고자 하는 의도를 나타냅니다.
즉, 사용자가 지도에서 특정 지점을 선택하면 해당 지점에 대한 MKMapItem 정보가 selectedResult에 할당되어, 이를 통해
다양한 작업을 수행할 수 있습니다.
- 예를 들어, selectedResult가 업데이트될 때마다 길 찾기를 수행하거나 선택된 지점에 대한 추가 정보를 로드하는 등의 작업을 수행할 수 있습니다. 이것은 사용자가 지도에서 특정 위치를 선택할 때 앱이 해당 이벤트에 반응하고 관련된 동작을 수행할 수 있도록 하는 SwiftUI의 편리한 기능 중 하나입니다.
- Map 뷰의 selection 매개변수는 사용자가 지도에서 특정 위치를 선택했을 때 해당 위치에 대한 정보를 나타내기 위한 Binding을 나타냅니다. 이것은 사용자가 지도에서 특정 지점을 선택할 때마다 해당 위치에 대한 정보를 업데이트하고 관련된 동작을 수행할 수 있게 해줍니다.
@State private var route: MKRoute?가 무슨 변수이지?
여기서 route 변수는 ContentView에서 현재 선택된 지도 항목(selectedResult)과
"lotteTower"라는 기본 출발지 사이의 길 찾기 정보를 나타냅니다. 사용자가 지도에서 어떤 지점을 선택하면
(selectedResult가 업데이트될 때), getDirections 함수가 호출되어 해당 지점까지의 길 찾기 정보를 구하고 route 변수에 저장됩니다.즉, route 변수는 사용자가 선택한 지점과 출발지 간의 길 찾기 정보를 저장하는 데 사용되며, SwiftUI의 반응형 프레임워크를 활용하여 UI를 업데이트하는 데 활용됩니다.
- 상태 속성인 @State는 SwiftUI 뷰에서 변경이 감지되면 해당 뷰를 다시 그리게끔 하는 역할을 합니다. 따라서 route 값이 업데이트되면 해당 값을 사용하는 뷰들이 자동으로 업데이트되어 새로운 길 찾기 정보를 표시하게 됩니다.
- @State private var route: MKRoute?은 SwiftUI에서 사용되는 상태 속성입니다. 이 상태 속성은 현재 선택된 지도 항목과 MKDirections를 사용하여 해당 항목에 대한 길 찾기 정보(MKRoute)를 저장합니다.
getDirections 메서드는 다음 chapter에서 제대로 활용합니다. 이해하고 넘어와 주세요
import SwiftUI
import MapKit
struct ItemInfoView: View {
// ItemInfoView의 상태 속성으로 주변보기 씬을 저장하는 변수
@State private var lookAroundScene: MKLookAroundScene?
// 선택된 지도 항목 및 길 찾기 정보를 나타내는 속성
let selectedResult: MKMapItem
let route: MKRoute?
// 길 찾기 정보의 예상 이동 시간을 포맷팅하는 속성
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)
}
var body: some View {
// 주변보기 씬을 표시하는 뷰
LookAroundPreview(initialScene: lookAroundScene)
.overlay(alignment: .bottomTrailing) {
// 선택된 지도 항목의 이름과 예상 이동 시간을 표시하는 HStack
HStack {
Text("\(selectedResult.name ?? "")")
// 예상 이동 시간이 있는 경우 표시
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
// 뷰가 나타날 때 주변보기 씬을 가져오는 작업 수행
.onAppear {
getLookAroundScene()
}
// 선택된 지도 항목이 변경될 때마다 주변보기 씬을 업데이트하는 작업 수행
.onChange(of: selectedResult) {
getLookAroundScene()
}
}
// 주변보기 이미지를 가져오는 기능
func getLookAroundScene() {
// 초기화
lookAroundScene = nil
// 비동기 작업 시작
Task {
// 선택된 지도 항목의 주변보기 씬 요청
let request = MKLookAroundSceneRequest(mapItem: selectedResult)
lookAroundScene = try? await request.scene
}
}
}
MKLookAroundScene 타입 설명
MKLookAroundScene?: lookAroundScene은 주변보기(MKLookAroundScene) 씬을 나타내는 변수입니다. MKLookAroundScene은 MapKit에서 제공하는 클래스로, 지도 항목의 주변 환경을 시각적으로 나타내기 위한 3D 씬을 생성하는 데 사용됩니다. 이 변수는 @State로 선언되어 뷰의 상태를 나타냅니다.
getLookAroundScene 메서드에서 해당 주변보기 씬을 가져오고, 이후에 lookAroundScene이 업데이트되면 해당 변경을 SwiftUI에 알려서 뷰를 갱신하게 됩니다.
주변보기 씬은 지도 항목 주변의 실제 환경을 시각적으로 제공하기 위해 사용됩니다. 이것은 사용자가 선택한 위치의 주위를 실제 사진과 같이 볼 수 있게 해주는 기능입니다.
최종 결과물 : 사용자의 현 위치에서 주차장, 병원 찾기 앱
목차
- 사용자의 현 위치를 계속 추적하여 파악해 보자
- 사용자가 바라보고 있는 지도 화면을 기준으로 주차장, 병원을 찾아보자
- 사용자가 하나의 marker를 선택하면 사용자의 현 위치를 기점으로 걸리는 시간 및 경로를 시각화해 보자
- 지도에 추가적인 컨트롤 요소를 설정해 보자, 나침반, 현 위치로 돌아오기
⭐️ 꼭 권한 설정을 해주고 진행해야 합니다.
전반적으로 코드를 수정했어요. 일단 표시한 코드를 수정, 추가 하고 설명을 읽어주세요. ㅎㅎ
//
// ContentView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
// 💁
@MainActor class LocationsHandler: ObservableObject {
static let shared = LocationsHandler()
public let manager: CLLocationManager
init() {
self.manager = CLLocationManager()
if self.manager.authorizationStatus == .notDetermined {
self.manager.requestWhenInUseAuthorization()
}
}
}
.
.
.
struct ContentView: View {
@ObservedObject var locationsHandler = LocationsHandler.shared
@State private var visibleRegion: MKCoordinateRegion?
// 💁
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem?
@State private var route: MKRoute?
var body: some View {
Map(position: $position, selection: $selectedResult){
// 💁
ForEach(searchResults, id: \.self) {result in
Marker(item: result)
}
.annotationTitles(.hidden)
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
UserAnnotation()
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
VStack(spacing:0) {
if let selectedResult {
ItemInfoView(selectedResult: selectedResult, route: route)
.frame(height: 128)
.clipShape(RoundedRectangle (cornerRadius: 10))
.padding([.top, .horizontal])
}
}// 💁 바라보는 지도의 영역 위치 값을 보내 해당 영역에서 검색 하도록
BeanTownButtons(position: $position, searchResults: $searchResults, visibleRegion: visibleRegion)
.padding(.top)
}
Spacer()
}
.background(.ultraThinMaterial)
}// 💁
.onChange(of: searchResults) {
withAnimation{
position = .automatic
}
}
.onChange(of: selectedResult) {
getDirections()
}
.onMapCameraChange { context in
visibleRegion = context.region
}
// 💁
.mapControls { // 이제 버튼을 탭하여 내 위치를 표시할 수 있습니다. 내가 움직일 때 지도 카메라가 나를 따라다닐 것입니다.
MapUserLocationButton() // 누르면 내 위치로 바로 이동함, 내가 이동하면 카메라도 이동함
MapCompass()
MapScaleView()
/*
mapControls 설정은 지도를 회전하면 나침반을 띄우고 화면을 확대하거나 축소하면 축적을 표시함
*/
}
}
func getDirections() {
route = nil
guard let selectedResult else { return }
// 💁
let location = locationsHandler.manager.location
guard let coordinate = location?.coordinate else { return }
// 💁
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: coordinate))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
#Preview {
ContentView()
}
사용자의 현 위치를 계속 추적하여 파악해 보자
코드 설명
@MainActor class LocationsHandler: ObservableObject {
static let shared = LocationsHandler()
public let manager: CLLocationManager
init() {
self.manager = CLLocationManager()
if self.manager.authorizationStatus == .notDetermined {
self.manager.requestWhenInUseAuthorization()
}
}
}
이 코드는 특정 위치 정보를 다루기 위한 클래스를 정의하고 있습니다.
LocationsHandler라는 클래스는 위치 정보를 다루는 데 도움을 주는 클래스입니다.
이 클래스를 사용하면 위치 정보를 관리하고, 사용자에게 위치 정보 사용 권한을 요청할 수 있습니다.
이 클래스는 ObservableObject 를 채택하여 클래스의 상태가 변할 때 다른 객체들에게 알림을 보낼 수 있게 구현하였습니다. 이렇게 하면 다른 객체들이 위치 정보의 변화를 쉽게 감지하고 반응할 수 있겠죠?
클래스 안에는 manager라는 객체도 있는데. 이 객체는 CLLocationManager 즉 위치 정보를 다루는 데 도움이 되는
여러 기능들을 제공합니다. 클래스의 초기화 메서드인 init()에서는 manager를 초기화하고, 권한 상태를 확인한 뒤,
권한이 설정되어 있지 않은 경우 위치 정보 사용 권한을 요청합니다. 이렇게 하면 사용자에게 위치 정보 사용 권한을 요청할 수 있습니다.
위치 정보 사용 권한을 받으면 앱이 정확한 위치 정보를 이용할 수 있습니다.
요약 싱글톤으로 구현하여 사용자의 현위치 추적 권한을 얻고 계속해서 위치를 추적할수 있습니다.
// 💁
@State private var position: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
두 번째로, position 변수는 MapCameraPosition이라는 형식을 가지고 있고, 초기값으로
.userLocation(followsHeading: true, fallback: .automatic)을 가지고 있습니다.
MapCameraPosition은 지도에서 카메라의 위치를 나타내는 열거형(enum)입니다.
position 변수는 이 열거형을 가지고 있으며, 초기값으로 .userLocation(followsHeading: true, fallback: .automatic)을
가지고 있습니다. .userLocation(followsHeading: true, fallback: .automatic)은 MapCameraPosition의 한 경우(case)입니다.
이 경우, 카메라의 위치를 사용자의 현재 위치로 설정합니다. followsHeading 매개변수는 사용자의 위치가 변경될 때마다 카메라가 사용자의 이동 방향을 따라가도록 지정하는 데 사용됩니다.
true로 설정되어 있으므로 카메라는 사용자의 이동 방향을 따라갑니다.
fallback 매개변수는 사용자의 위치를 가져올 수 없는 경우 카메라의 위치를 대체로 설정하는 데 사용됩니다.
.automatic으로 설정되어 있으므로 자동 대체가 이루어집니다.
이 코드는 SwiftUI의 @State 속성을 사용하여 position 변수를 선언하고 초기값을 설정하는 역할을 합니다. position 변수는 지도의 카메라 위치를 나타내며, 초기값으로는 사용자의 위치를 따라가는 설정으로 설정되어 있습니다.
즉 앱이 시작되면 사용자의 위치를 추적해서 사용자 위치 기점으로 맵의 카메라를 조절하여 사용자의 위치를 따라가게 해주고 그렇지 않다면 사용자가 지도를 보는 위치 기점으로 카메라를 따라가겠다는 것입니다.
// 💁
.onChange(of: searchResults) {
withAnimation{
position = .automatic
}
}
그래서 사용자가 주차장, 병원을 찾는 버튼을 누르면 서서히 지도의 카메라를 조절하게 구현하였습니다.
지도에 추가적인 컨트롤 요소를 설정
.mapControls { // 이제 버튼을 탭하여 내 위치를 표시할 수 있습니다. 내가 움직일 때 지도 카메라가 나를 따라다닐 것입니다.
MapUserLocationButton() // 누르면 내 위치로 바로 이동함, 내가 이동하면 카메라도 이동함
MapCompass()
MapScaleView()
/*
mapControls 설정은 지도를 회전하면 나침반을 띄우고 화면을 확대하거나 축소하면 축적을 표시함
*/
}
주어진 코드는 지도에 추가적인 컨트롤 요소를 설정하는 부분입니다.
.mapControls는 지도에 컨트롤 요소를 추가하는 데 사용되는 메서드입니다.
이 메서드 내에서 다양한 컨트롤 요소를 설정할 수 있습니다.
- MapUserLocationButton()은 내 위치를 표시하고, 해당 위치로 바로 이동할 수 있는 버튼입니다.
- 버튼을 누르면 현재 사용자의 위치로 지도가 이동하며, 사용자가 이동하면 지도 카메라도 사용자를 따라다닙니다.
- MapCompass()는 지도 회전 시 나침반을 표시하는 요소입니다.
- 지도가 회전하면 나침반이 함께 회전하여 현재 방향을 표시합니다.
- MapScaleView()는 화면을 확대하거나 축소할 때 지도의 축척을 표시하는 요소입니다.
- 화면을 확대하거나 축소하면 해당 축척이 지도 상단에 표시됩니다.
이렇게 설정된 mapControls는 지도에 버튼을 추가하여 사용자의 위치를 표시하고, 사용자가 이동할 때 지도 카메라가 따라다니며, 지도 회전 시 나침반을 표시하고, 화면 확대/축소 시 축척을 표시합니다. 이를 통해 사용자는 지도를 더 편리하게 조작할 수 있습니다.
사용자가 하나의 Marker를 선택하면 사용자의 현 위치를 기점으로 걸리는 시간 및 경로를 시각화해 보자
Map(position: $position, selection: $selectedResult){
// 💁
ForEach(searchResults, id: \.self) {result in
Marker(item: result)
}
.annotationTitles(.hidden)
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
UserAnnotation() // 사용자의 현위치를 표시
}
맵이 시작하면 일단 사용자의 위치를 표시하다가 사용자가 주차장, 병원 버튼을 누른다면 selectedResult에 값이 들어오게 됩니다.
그래서 그 결과 list를 하나하나 Marker로 표시합니다.
그리고 값이 바뀌면 즉 하나의 Marker를 선택하면
func getDirections() {
route = nil
guard let selectedResult else { return }
// 💁
let location = locationsHandler.manager.location
guard let coordinate = location?.coordinate else { return }
// 💁
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: coordinate))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
해당 함수가 실행 됩니다.
바로 사용자 현 위치에서 선택한 지점의 거리 계산을 수행하는 getDirections() 함수입니다.
먼저, 함수 내에서 route 변수를 초기화합니다. 이 변수는 경로를 저장하는 역할을 합니다.
그 다음, selectedResult 변수가 nil이 아닌지 확인합니다.(결과 Marker 중 하나를 선택한 값) nil이라면 함수를 종료합니다.
locationsHandler.manager.location을 통해 사용자 현재 위치를 가져옵니다.
이때, 위치 정보가 유효한 경우에만 다음 과정을 수행합니다.
coordinate 변수를 통해 현재 위치의 좌표를 가져옵니다. (사용자 위도, 경도)
request 객체를 생성합니다. 이 객체는 경로 계산에 필요한 정보를 담고 있습니다.
request.source에는 현재 위치를 나타내는 MKMapItem 객체를 설정합니다.
이때, 좌표를 활용하여 MKPlacemark 객체를 생성하고, 이를 MKMapItem에 설정합니다.
request.destination에는 경로의 목적지를 설정합니다. selectedResult 변수를 이용하여 설정합니다.
그 다음, 비동기적인 작업을 수행하기 위해 Task를 사용합니다.
MKDirections 객체를 생성하고, 이를 이용하여 directions.calculate()를 호출합니다.
경로 계산이 완료되면, 응답 결과에서 첫 번째 경로를 가져와 route 변수에 저장합니다.
이렇게 설정된 getDirections() 함수는 현재 위치와 목적지를 기반으로 경로를 계산하고, 결과를 route 변수에 저장합니다. 이를 통해 경로 안내 기능을 구현할 수 있습니다.
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
그 다음 해당 route에는 현 위치에서 선택한 Marker 위치 값의 거리를 계산한 값이 있기 때문에
MapPolyline를 통하여 경로를 표시 할 수 있습니다.
MapPolyline은 지도 위에 선을 그리기 위한 SwiftUI View입니다. 이를 사용하여 경로를 시각적으로 표시할 수 있습니다.
MapPolyline은 Polyline 데이터와 선의 스타일을 인자로 받아 지도 위에 선을 그립니다.
Polyline은 경로를 나타내는 좌표의 집합입니다.
주어진 코드에서는 MapPolyline을 사용하여 route 변수에 저장된 경로를 표시합니다. 경로의 좌표는 Polyline 데이터로 전달되며, 선의 스타일은 파란색으로 설정되고 두께는 5로 설정됩니다.
결과적으로, MapPolyline을 사용하여 경로를 시각적으로 표시할 수 있으며, 이를 통해 사용자에게 명확한 경로 안내를 제공할 수 있습니다.
사용자가 바라보고 있는 지도 화면을 기준으로 주차장, 병원을 찾아보자
//
// BeantownButtonView.swift
// MapKitStudy
//
// Created by 정정욱 on 1/7/24.
//
import SwiftUI
import MapKit
struct BeanTownButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion? // 💁 사용자에게 표시되는 지역 내에서 검색되도록 BeantownButton을 업데이트
.
.
.
func search(for query: String) {
let request = MKLocalSearch.Request()
request.naturalLanguageQuery = query
request.resultTypes = .pointOfInterest
// 💁
request.region = visibleRegion ?? MKCoordinateRegion(
center: .lotteTower,
span: MKCoordinateSpan (latitudeDelta: 0.0125, longitudeDelta: 0.0125)
)
Task {
let search = MKLocalSearch(request: request)
let response = try? await search.start()
searchResults = response?.mapItems ?? []
}
}
}
코드설명
request.region = visibleRegion ?? MKCoordinateRegion(
center: .lotteTower,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
)
요약하면 사용자가 바라보는 지도의 영역 값을 가지고 검색하는데 해당 값이 없으면
롯데타워 주변으로 주차장, 병원을 찾겠다는 것입니다.
상세설명
여기서 request.region은 visibleRegion을 기반으로 설정되고 있습니다.
이는 지도 관련 기능에서 검색을 위한 지리적 영역을 제공하려는 일반적인 패턴입니다.
visibleRegion은 옵셔널한 MKCoordinateRegion으로, nil일 수 있습니다. 만약 visibleRegion이 nil이 아니면, 그것을 request.region의 값으로 사용합니다. 만약 visibleRegion이 nil이라면(즉, 제공되지 않았거나 nil인 경우), 기본 지역이 설정됩니다.
기본 지역은 특정 중심 위치 및 지역 크기로 정의된 MKCoordinateRegion입니다.
지역의 중심은 미리 정의된 위치 .lotteTower로 설정됩니다. 지역의 크기는 MKCoordinateSpan을 사용하여 latitudeDelta 및 longitudeDelta가 각각 0.0125로 설정됩니다. latitudeDelta 및 longitudeDelta는 지역의 너비와 높이 또는 "줌" 레벨을 결정하는 데 사용되는 값으로 각각 위도와 경도의 각도입니다.
요약하면, 이 코드는 로컬 검색 요청의 지역을 visibleRegion이 제공되었는지 여부에 따라 설정합니다. 제공되면 해당 지역을 사용하고, 그렇지 않으면 .lotteTower 주변의 특정 줌 레벨을 사용하는 기본 지역을 설정합니다.
'APP > iOS' 카테고리의 다른 글
MapKit with SwiftUI 기초부터 심화까지 1 : MapKit 다루기 기본 과정 (0) | 2024.01.08 |
---|---|
iOS23 SkillUpThon 2주차 정리 (0) | 2023.06.22 |
iOS23 SkillUpThon 1주차 정리 (0) | 2023.06.21 |
앱의 생명주기(Life Cycle) (0) | 2023.04.16 |
12주차 : doit Swift 분석 (0) | 2022.11.21 |