Meet MapKit for SwiftUI
🔨 Xcode 15.0 Beta
iOS 17.0 +
Apple M1 Max
WWDC23 - Video
https://developer.apple.com/wwdc23/10043
이번 WWDC23에서 흥미로운 세션이 많았지만, 가장 끌리는 MapKit을 정리해보고자 한다.
특히 SwiftData에서 CoreData가 Objective-C era 라고 표현한 부분에서 전율이 일었음.
✅ 시청리스트
- AppIntent
- SwiftData
- Xcode
- MapKit
- ScrollView
- Macro
✅ MapKit 전체 코드로 알아보기
🚨 (2023.06.19 00:25) XCode 15.0 beta를 기준으로 UserAnnotation()이 작동하지 않고 있습니다. 앱에서만 작동하지 않는게 아니라, 시뮬레이터 내 지도 앱에서도 작동하지 않는 것으로 확인했습니다. 현재는 iOS 17을 실제 디바이스에 설치할 수 없어서 설치 후 다시 확인해보도록 합시다.
- 아래 코드를 그대로 복사해서 사용하시면 예제를 확인하실 수 있습니다.
//
// ContentView2.swift
// WWDC23_Map
//
// Created by Karrot on 2023/06/18.
//
import SwiftUI
import MapKit
struct ContentView: View {
@State private var position: MapCameraPosition = .automatic // 지도에서 카메라의 위치를 나타내는 변수
@State private var visibleRegion: MKCoordinateRegion? // 지도에서 보여줄 위치를 나타내는 변수
@State private var searchResults: [MKMapItem] = [] // 검색을 통해 결과로 보여줄 아이템들
@State private var selectedResult: MKMapItem? // 선택한 아이템
@State private var route: MKRoute? // 지도에 보여줄 루트
@Namespace var mapScope // 맵 스코프를 위한 변수
var body: some View {
Map(position: $position, selection: $selectedResult, scope: mapScope) {
Annotation("주차장", coordinate: .parking) {
ZStack {
RoundedRectangle(cornerRadius: 5)
.fill(.background)
RoundedRectangle(cornerRadius: 5)
.stroke(.secondary, lineWidth: 5)
Image(systemName: "car")
.padding(5)
}
}
.annotationTitles(.hidden)
ForEach(searchResults, id: \.self) { result in
Marker(item: result)
}
.annotationTitles(.hidden)
UserAnnotation() // 사용자의 현재 위치
if let route { // 검색결과에 따른 루트가 존재하면 아래의 코드로 라인을 연결
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
MapPolyline(coordinates: FancyMapPolyLine.walkingCoordinates) // 라인 연결의 커스텀 예시
.stroke(FancyMapPolyLine.gradient, style: FancyMapPolyLine.stroke)
}
.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])
}
BeantwonButtons(position: $position,
searchResults: $searchResults,
visibleRegion: visibleRegion)
}
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
position = .automatic
}
.onChange(of: selectedResult) {
getDirection() //
}
.onMapCameraChange { context in
visibleRegion = context.region
}
// .mapControls {
// MapUserLocationButton() // 내위치
// MapCompass() // 맵 자체가 디폴트가 회전할 때 컴패스 보여준다.
// MapScaleView() // MacOS에서 빛을 바란다.
// }
.overlay(alignment: .bottomTrailing) {
VStack {
MapUserLocationButton(scope: mapScope)
MapCompass(scope: mapScope) // 맵 자체가 디폴트가 회전할 때 컴패스 보여준다.
MapScaleView(scope: mapScope) // MacOS에서 빛을 바란다.
.mapControlVisibility(.visible)
}
.padding(.trailing, 10)
.buttonBorderShape(.circle)
}
.mapScope(mapScope)
}
/// route를 찾는 함수
func getDirection() {
route = nil // route 초기화
guard let selectedResult else { return } // 선택한 것이 있다면
let request = MKDirections.Request() // 요청을 위한 변수
request.source = MKMapItem(placemark: MKPlacemark(coordinate: .parking)) // 출발지 좌표
request.destination = selectedResult // 목적지
Task {
let directions = MKDirections(request: request) // 출발지랑 목적지 넣어서 request
let response = try? await directions.calculate() // response 비동기로 기다리기, 계산은 알아서 해줌
route = response?.routes.first // 출발지와 목적지 사이의 여러 케이스 중 가장 첫번째 결과 선택
}
}
}
struct BeantwonButtons: View {
@Binding var position: MapCameraPosition
@Binding var searchResults: [MKMapItem]
var visibleRegion: MKCoordinateRegion?
var body: some View {
HStack {
Button {
search(for: "playground")
} label: {
Label("playground", systemImage: "figure.and.child.holdinghands")
}
.buttonStyle(.borderedProminent)
Button {
search(for: "beach")
} label: {
Label("beach", systemImage: "beach.umbrella")
}
.buttonStyle(.borderedProminent)
}
.labelStyle(.iconOnly)
}
/// 위치를 검색하는 함수
func search(for query: String) {
let request = MKLocalSearch.Request() // 지역검색을 위한 request 생성
request.naturalLanguageQuery = query // 검색 쿼리문 넣어주기 - 한국어도 검색이 가능하나, 부정확한듯.
request.resultTypes = .pointOfInterest // 핀으로 해야 지도에 핀 나타남
request.region = visibleRegion ?? MKCoordinateRegion( // visibleRegion이 지정되어 있으면 해당 위치를 기반으로 아니면 default값인 주자창을 기반으로
center: .parking,
span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
)
Task {
let search = MKLocalSearch(request: request) // 요청
let response = try? await search.start() // 위치검색 시작!
searchResults = response?.mapItems ?? [] // 결과 반환, 없을수도 있음.
}
}
}
#Preview {
ContentView()
}
/// AroundScene에 사용되는 뷰
struct ItemInfoView: View {
@State private var lookAroundScene: MKLookAroundScene? // aroundScene 변수
var selectedResult: MKMapItem
var route: MKRoute?
/// route의 값을 가지고 얼마나 걸릴지 예상되는 시간을 계산해서 반환해줌. -> 다만, 한국에서 부정확하다고 느낌
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) // AroundScene을 보여줌
.overlay(alignment: .bottomTrailing) {
HStack {
Text("\(selectedResult.name ?? "")")
if let travelTime {
Text(travelTime)
}
}
.font(.caption)
.foregroundStyle(.white)
.padding(10)
}
.onAppear {
getLookAroundScene() // LookAroundPreview 나타날 때 로드
}
.onChange(of: selectedResult) {
getLookAroundScene() // 선택결과가 바뀌면 리로드
}
}
func getLookAroundScene() {
lookAroundScene = nil // 초기화
Task {
let request = MKLookAroundSceneRequest(mapItem: selectedResult) // 어라운드 씬
lookAroundScene = try? await request.scene // 씬을 넣어주기
}
}
}
// PolyLine을 Fancy하게 사용해보자~!
struct FancyMapPolyLine {
static let walkingCoordinates: [CLLocationCoordinate2D] = [.company, .lunch, .isLakeCenter, .parking]
static let gradient = LinearGradient(
colors: [.red, .green, .blue],
startPoint: .leading, endPoint: .trailing
)
static let stroke = StrokeStyle(
lineWidth: 5,
lineCap: .round, lineJoin: .round, dash: [10, 10]
)
}
extension CLLocationCoordinate2D {
static let parking = CLLocationCoordinate2D(latitude: 37.399112, longitude: 127.102671)
static let company = CLLocationCoordinate2D(latitude: 37.401177, longitude: 127.101234)
static let lunch = CLLocationCoordinate2D(latitude: 37.400169, longitude: 127.104638)
static let isLakeCenter = CLLocationCoordinate2D(latitude: 37.399057, longitude: 127.104580)
}
extension [CLLocationCoordinate2D] {
static let shuttle: [CLLocationCoordinate2D] = [
.init(latitude: 37.400925, longitude: 127.100926),
.init(latitude: 37.400920, longitude: 127.101395)
]
static let cafe: [CLLocationCoordinate2D] = [
.init(latitude: 37.4007736, longitude: 127.101928),
.init(latitude: 37.4007736, longitude: 127.102274),
.init(latitude: 37.3999919, longitude: 127.102274),
.init(latitude: 37.3999919, longitude: 127.101928),
.init(latitude: 37.4007736, longitude: 127.101928)
]
}
extension CLLocationDistance {
static let isLakeRadius = CLLocationDistance(40)
}
extension MKCoordinateRegion {
static let nexon = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 37.40241,
longitude: 127.10377),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5)
)
static let midas = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 37.40028,
longitude: 127.10148),
span: MKCoordinateSpan(
latitudeDelta: 0.5,
longitudeDelta: 0.5)
)
}
더 다양한 커스텀을 위한 코드 예시
- 맵 스타일도 여러개를 적용해보면 좋음.
Marker("주차장", monogram: "당근",
coordinate: .parking)
.tint(.blue)
Marker("회사", systemImage: "figure.wave",
coordinate: .company)
.tint(.bar)
Annotation(
"점심",
coordinate: .lunch,
anchor: .bottom
) {
Image(systemName: "figure.wave")
.padding(4)
.foregroundStyle(.white)
.background(Color.indigo)
.cornerRadius(4)
}
MapCircle(center: .isLakeCenter, radius: .isLakeRadius)
.foregroundStyle(.orange.opacity(0.75))
MapPolyline(coordinates: .shuttle)
.stroke(.blue, lineWidth: 13)
MapPolygon(coordinates: .cafe)
.foregroundStyle(.purple)
'apple > Docs, iOS, Swift' 카테고리의 다른 글
[Swift] plain ol' data(POD) (0) | 2023.08.08 |
---|---|
[UIKit] UILabel Inset (0) | 2023.06.23 |
[XCode 15.0 beta] Preview Macro Bug (0) | 2023.06.08 |
[Swift] 커링(Currying) (1) | 2023.02.28 |
[iOS] UIImage.Orientation (0) | 2022.12.09 |