→ MapKit

[MapKit] 길찾기, 네비게이션 구현

Swift librarian 2024. 2. 22. 09:21

새로 업데이트 된 MapKit 을 활용하여 길찾기, 네비게이션을 구현해 볼 것이다. 아래와 같이 길찾기를 하면 길찾기 결과가 맵에 보이고, 네비게이션을 누르면 네비게이션 안내가 나오게 된다.

 

맵 구현

Map { } 으로 맵을 간단하게 구현할 수 있다. 시작점, 도착점을 CLLocationCoordinate2D 형식으로 넣어준다. MKRoute 형식의 route 라는 변수도 만들어 준다.

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var route: MKRoute?
    @State private var name = ""
    @State private var time = 0.0
    @State private var distance = 0.0
    @State private var navigation: [String] = []
    @State private var showNav = false
    
    private let start = CLLocationCoordinate2D(latitude: 37.39535, longitude: 127.11259)
    private let end = CLLocationCoordinate2D(latitude: 37.34591, longitude: 127.09083)
    
    var body: some View {
        Map {
            UserAnnotation()
            
            Marker("시작점", coordinate: start)
            Marker("도착점", coordinate: end)
            
            if let route {
                MapPolyline(route)
                    .stroke(Color.cyan, style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round))
            }
        }
        .mapControls {
            MapUserLocationButton()
            MapCompass()
        }
        ...

 

 

길찾기 함수 구현

source, destination 을 설정해준뒤 MKDirections(request: ) 를 통해 결과값을 받는다. MKRouteMapPolyline 에 넣어주고, 나머지 name, time, distance, navigation 을 넣어주면 된다.

@State private var route: MKRoute?
@State private var name = ""
@State private var time = 0.0
@State private var distance = 0.0
@State private var navigation: [String] = []

private func getRoute() {
        route = nil
        if !navigation.isEmpty {
            navigation = []
        }
        
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end))
        request.transportType = .walking
        
        Task {
            let directions = MKDirections(request: request)
            let response = try? await directions.calculate()
            
            if let result = response?.routes.first {
                DispatchQueue.main.async {
                    self.route = result
                    self.name = result.name
                    self.time = result.expectedTravelTime
                    self.distance = result.distance
                    
                    for step in result.steps {
                        let distance = step.distance
                        let instructions = step.instructions
                        let stepInfo = instructions.isEmpty ? "\(distance)미터" : "\(instructions) \(distance)미터"
                        self.navigation.append(stepInfo)
                    }
                }
            }
        }
    }

 

전체코드

import SwiftUI
import MapKit

struct ContentView: View {
    private let locationManager = CLLocationManager()
    @State private var route: MKRoute?
    @State private var name = ""
    @State private var time = 0.0
    @State private var distance = 0.0
    @State private var navigation: [String] = []
    @State private var showNav = false
    
    private let start = CLLocationCoordinate2D(latitude: 37.39535, longitude: 127.11259)
    private let end = CLLocationCoordinate2D(latitude: 37.34591, longitude: 127.09083)
    
    var body: some View {
        Map {
            UserAnnotation()
            
            Marker("시작점", coordinate: start)
            Marker("도착점", coordinate: end)
            
            if let route {
                MapPolyline(route)
                    .stroke(Color.cyan, style: StrokeStyle(lineWidth: 6, lineCap: .round, lineJoin: .round))
            }
        }
        .mapControls {
            MapUserLocationButton()
            MapCompass()
        }
        .onAppear {
            locationManager.requestWhenInUseAuthorization()
        }
        .safeAreaInset(edge: .bottom) {
            VStack {
                Button {
                    withAnimation {
                        getRoute()
                    }
                } label: {
                    HStack {
                        Image(systemName: "arrow.triangle.turn.up.right.circle.fill")
                        Text("길찾기")
                    }
                }
                .buttonStyle(CustomButtonStyle())
                
                Button {
                    showNav.toggle()
                } label: {
                    HStack {
                        Image(systemName: "map.fill")
                        Text("네비게이션 보기")
                    }
                }
                .buttonStyle(CustomButtonStyle())
            }
        }
        .sheet(isPresented: $showNav) {
            NavigationStack {
                List {
                    Section("기본정보") {
                        Text("이름 : \(name)")
                        Text("시간 : \(Int(time/60))분")
                        Text("거리 : \(distance/1000, specifier: "%.1f")km")
                    }
                    
                    Section("네비게이션") {
                        if navigation.isEmpty {
                            Text("값이 없습니다.")
                        } else {
                            ForEach(navigation, id: \.self) { nav in
                                Text(nav)
                            }
                        }
                    }
                }
                .navigationTitle("네비게이션")
                .navigationBarTitleDisplayMode(.inline)
            }
        }
    }
    
    private func getRoute() {
        route = nil
        if !navigation.isEmpty {
            navigation = []
        }
        
        let request = MKDirections.Request()
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
        request.destination = MKMapItem(placemark: MKPlacemark(coordinate: end))
        request.transportType = .walking
        
        Task {
            let directions = MKDirections(request: request)
            let response = try? await directions.calculate()
            
            if let result = response?.routes.first {
                DispatchQueue.main.async {
                    self.route = result
                    self.name = result.name
                    self.time = result.expectedTravelTime
                    self.distance = result.distance
                    
                    for step in result.steps {
                        let distance = step.distance
                        let instructions = step.instructions
                        let stepInfo = instructions.isEmpty ? "\(distance)미터" : "\(instructions) \(distance)미터"
                        self.navigation.append(stepInfo)
                    }
                }
            }
        }
    }
}

struct CustomButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .frame(maxWidth: .infinity)
            .padding()
            .scaleEffect(configuration.isPressed ? 0.97 : 1)
            .animation(.bouncy, value: configuration.isPressed)
            .background {
                RoundedRectangle(cornerRadius: 20, style: .continuous)
                    .foregroundStyle(.ultraThinMaterial)
            }
            .background {
                RoundedRectangle(cornerRadius: 20, style: .continuous)
                    .stroke(lineWidth: 1)
                    .foregroundStyle(configuration.isPressed ? .cyan : .gray)
            }
            .padding(.horizontal)
    }
}