🇺🇸 EN

🗺️ 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 체크리스트
✅ 사용자 위치 표시 시 권한 요청
✅ 로딩 상태 표시
✅ 네트워크 에러 처리
✅ 적절한 줌 레벨 설정
✅ 마커는 명확하고 구분 가능하게

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple HIG 원문
📖
Apple 공식 문서

📎 Apple 공식 자료

📘 공식 문서 💻 샘플 코드 🎬 WWDC 세션