🗺️ MapKit 완전정복
Apple 지도로 위치 기반 앱을 만드세요. 마커, 경로, 3D 건물까지!
⭐ 난이도: ⭐⭐
⏱️ 예상 시간: 1-2h
📂 App Services
✨ MapKit이란?
MapKit은 Apple Maps를 앱에 통합할 수 있는 프레임워크입니다. SwiftUI의 Map 뷰로 간단하게 구현할 수 있습니다.
🗺️ 기본 지도 표시 (SwiftUI)
BasicMap.swift
import SwiftUI import MapKit struct ContentView: View { @State private var position: MapCameraPosition = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ) ) var body: some View { Map(position: $position) } } // 지도 스타일 Map(position: $position) { } .mapStyle(.standard) // .standard, .hybrid, .imagery
📍 마커 추가
Markers.swift
import SwiftUI import MapKit struct Location: Identifiable { let id = UUID() let name: String let coordinate: CLLocationCoordinate2D } struct MapWithMarkers: View { @State private var position: MapCameraPosition = .automatic let locations = [ Location(name: "경복궁", coordinate: CLLocationCoordinate2D(latitude: 37.5788, longitude: 126.9770)), Location(name: "남산타워", coordinate: CLLocationCoordinate2D(latitude: 37.5512, longitude: 126.9882)), Location(name: "한강공원", coordinate: CLLocationCoordinate2D(latitude: 37.5326, longitude: 126.9610)) ] var body: some View { Map(position: $position) { ForEach(locations) { location in // 기본 마커 Marker(location.name, coordinate: location.coordinate) } } } } // 커스텀 색상 마커 Marker("레스토랑", systemImage: "fork.knife", coordinate: coord) .tint(.red) // 어노테이션 (더 많은 커스터마이징) Annotation("카페", coordinate: coord) { ZStack { Circle() .fill(Color.blue) .frame(width: 30, height: 30) Image(systemName: "cup.and.saucer.fill") .foregroundColor(.white) } }
🎯 사용자 위치 표시
UserLocation.swift
import SwiftUI import MapKit struct MapWithUserLocation: View { @State private var position: MapCameraPosition = .userLocation(fallback: .automatic) var body: some View { Map(position: $position) { UserAnnotation() // 사용자 위치 마커 } .mapControls { MapUserLocationButton() // 내 위치 버튼 MapCompass() // 나침반 MapScaleView() // 축척 } } }
🛣️ 경로 표시 (Directions)
Route.swift
import MapKit func calculateRoute(from start: CLLocationCoordinate2D, to end: CLLocationCoordinate2D) async throws -> MKRoute? { let request = MKDirections.Request() request.source = MKMapItem(placemark: MKPlacemark(coordinate: start)) request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end)) request.transportType = .automobile // .walking, .transit let directions = MKDirections(request: request) let response = try await directions.calculate() return response.routes.first } // SwiftUI에서 경로 표시 struct RouteMapView: View { @State private var route: MKRoute? @State private var position: MapCameraPosition = .automatic var body: some View { Map(position: $position) { if let route = route { MapPolyline(route.polyline) .stroke(Color.blue, lineWidth: 5) } // 출발/도착 마커 Marker("출발", systemImage: "mappin.circle.fill", coordinate: start) .tint(.green) Marker("도착", systemImage: "flag.fill", coordinate: end) .tint(.red) } .task { route = try? await calculateRoute(from: start, to: end) } } }
🔍 장소 검색 (Search)
Search.swift
import MapKit func searchPlaces(query: String, region: MKCoordinateRegion) async throws -> [MKMapItem] { let request = MKLocalSearch.Request() request.naturalLanguageQuery = query // "카페", "레스토랑" 등 request.region = region let search = MKLocalSearch(request: request) let response = try await search.start() return response.mapItems } // SwiftUI 통합 struct SearchMapView: View { @State private var searchResults: [MKMapItem] = [] @State private var searchText = "" @State private var position: MapCameraPosition = .automatic var body: some View { VStack { TextField("장소 검색", text: $searchText) .textFieldStyle(.roundedBorder) .padding() .onSubmit { Task { await search() } } Map(position: $position) { ForEach(searchResults, id: \.self) { item in Marker(item.name ?? "장소", coordinate: item.placemark.coordinate) } } } } func search() async { let region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1) ) searchResults = (try? await searchPlaces(query: searchText, region: region)) ?? [] } }
📍 역지오코딩 (좌표 → 주소)
Geocoding.swift
import CoreLocation func reverseGeocode(_ coordinate: CLLocationCoordinate2D) async throws -> String { let geocoder = CLGeocoder() let location = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) let placemarks = try await geocoder.reverseGeocodeLocation(location) guard let placemark = placemarks.first else { return "주소 없음" } let address = [ placemark.thoroughfare, // 도로명 placemark.locality, // 시/구 placemark.country // 국가 ].compactMap { $0 }.joined(separator: ", ") return address } // 정지오코딩 (주소 → 좌표) func geocode(address: String) async throws -> CLLocationCoordinate2D? { let geocoder = CLGeocoder() let placemarks = try await geocoder.geocodeAddressString(address) return placemarks.first?.location?.coordinate }
🎨 카메라 제어
CameraControl.swift
import SwiftUI import MapKit struct CameraControlView: View { @State private var position: MapCameraPosition = .automatic var body: some View { VStack { Map(position: $position) HStack { // 특정 좌표로 이동 Button("서울") { position = .region( MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05) ) ) } // 카메라 각도 조절 (3D) Button("3D 뷰") { position = .camera( MapCamera( centerCoordinate: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), distance: 1000, // 고도 heading: 45, // 방향 pitch: 60 // 기울기 ) ) } } .padding() } } }
🏢 맵 스타일 & 3D 건물
MapStyles.swift
Map(position: $position) { } .mapStyle(.standard) // 기본 .mapStyle(.hybrid) // 위성 + 도로 .mapStyle(.imagery) // 위성만 // 3D 건물 표시 .mapStyle(.standard(elevation: .realistic)) // POI (관심 장소) 필터 .mapStyle(.standard(pointsOfInterest: .including([.cafe, .restaurant])))
📱 UIKit 통합 (MKMapView)
MKMapView.swift
import SwiftUI import MapKit struct MapViewRepresentable: UIViewRepresentable { @Binding var region: MKCoordinateRegion func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator return mapView } func updateUIView(_ mapView: MKMapView, context: Context) { mapView.setRegion(region, animated: true) } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, MKMapViewDelegate { var parent: MapViewRepresentable init(_ parent: MapViewRepresentable) { self.parent = parent } func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { parent.region = mapView.region } } }
🎯 선택 가능한 마커
SelectableMarkers.swift
struct SelectableMapView: View { @State private var selectedLocation: Location? @State private var position: MapCameraPosition = .automatic var body: some View { Map(position: $position, selection: $selectedLocation) { ForEach(locations) { location in Marker(location.name, coordinate: location.coordinate) .tag(location) } } .sheet(item: $selectedLocation) { location in VStack { Text(location.name) .font(.title) Text("위도: \(location.coordinate.latitude)") Text("경도: \(location.coordinate.longitude)") } .padding() } } }
💡 HIG 체크리스트
✅ 사용자 위치 표시 시 권한 요청
✅ 로딩 상태 표시
✅ 네트워크 에러 처리
✅ 적절한 줌 레벨 설정
✅ 마커는 명확하고 구분 가능하게