๐Ÿ—บ๏ธ MapKit ์™„์ „์ •๋ณต

Apple ์ง€๋„๋กœ ์œ„์น˜ ๊ธฐ๋ฐ˜ ์•ฑ์„ ๋งŒ๋“œ์„ธ์š”. ๋งˆ์ปค, ๊ฒฝ๋กœ, 3D ๊ฑด๋ฌผ๊นŒ์ง€!

โœจ 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 ๊ณต์‹ ๋ฌธ์„œ