일모도원(日暮途遠) 개발자

[SwiftUI Map] MapKit사용하기 본문

iOS개발/SwiftUI

[SwiftUI Map] MapKit사용하기

달님개발자 2023. 9. 15. 14:26

SwiftUI에서 애플 지도를 사용할려면 MapKit을 사용하면 된다.

import MapKit

 

지도를 그려주기 위해서는 Map View를 이용하면 된다. 크게는 coordinateRegion과 mapRect가 있는데, 주로 coordinateRegion를 사용한다.

Map(coordinateRegion: Binding<MKCoordinateRegion>

Map(mapRect: Binding<MKMapRect>

 

아래코드는 서울지역을 보여주는 코드이다. MKCoordinateRegion으로 중심 경위도를 주고, span으로 반경을 주면 된다.

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780), // Seoul coordinates
        span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
    )

    var body: some View {
        Map(coordinateRegion: $region)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

폰의 화면을 꽉 채울려면 .ingoresSafeArea()를 사용하면 된다.

        Map(coordinateRegion: $region)
            .ignoresSafeArea()

 

아래처럼 MapConstants로 상수를 만들어서 사용하면 코드가 조금 더 깔끔해진다.

struct MapConstants {
    static let seoulCenter = CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780)
    static let defaultSpan = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
}

struct ContentView: View {
    @State private var region = MKCoordinateRegion(
        center: MapConstants.seoulCenter,
        span: MapConstants.defaultSpan
    )

MapConstants에 필요하면 아래처럼 더 추가하면 돈다.

static let cityHallCoordinate = CLLocationCoordinate2D(latitude: 37.5663, longitude: 126.9778)


이제 지도위에 아이콘등을 표시해보자. MapMarker를 사용하면 되는데, CLLocationCoordinate2D로 좌표를 주고, 색상을 줄수 있다.

// Available when SwiftUI is imported with MapKit
@available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *)
public struct MapMarker : MapAnnotationProtocol {
    public init(coordinate: CLLocationCoordinate2D, tint: Color? = nil)
}

실제 Map뷰에 MapMarker를 그려주기 위해서는 아래처럼 사용해야 한다.

coordinateRegion으로 지도화면을 보여줄 지역을 정해주고, annotationItems로 지도위에 뿌리 annotation들을 같이 줘야한다. 이 annotation들은 coordinateRegion을 벗어날수 있다.

Map(coordinateRegion: Binding<MKCoordinateRegion>, 
	annotationItems: RandomAccessCollection) { Identifiable in
    code
}

annotation은 여러 정보를 담을수 있는데, 필수적인게 CLLocationCoordinate2D 좌표이다. 그리고 Identifiable을 구현해야 한다. 그래서 UUID를 사용했다. name은 부가 정보이다. 다른 정보가 필요하면 더 추가하면 된다.

struct AnnotationItem: Identifiable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D
    let name: String
}

이제 만든 구조체에 화면에 뿌릴 annotation정보들을 주자.  실 사용예에서는 네트웍이나 DB등에서 읽어오면 된다.

let annotations: [AnnotationItem] = [
    AnnotationItem(coordinate: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194), name: "San Francisco"),
    AnnotationItem(coordinate: CLLocationCoordinate2D(latitude: 34.0522, longitude: -118.2437), name: "Los Angeles"),
    AnnotationItem(coordinate: CLLocationCoordinate2D(latitude: 40.7128, longitude: -74.0060), name: "New York"),
    AnnotationItem(coordinate: MapConstants.cityHallCoordinate, name: "Seoul City Hall")
]

이제 MapMarker를 이용해서 annotation들을 뿌려주자.

Map(coordinateRegion: $region, annotationItems: annotations) { item in
    MapMarker(coordinate: item.coordinate, tint: .red)
}
.ignoresSafeArea()

에뮬레이터에서는 Option키를 누른상태에서 마우스 드래그를 하면 확대/축소가 가능하다.

 

참고로 MapPin은 사용 비권장으로 되었다. MapMarker를 사용하자. MapPin을 사용하면 아래처럼 진짜 Pin처럼 보인다.

MapPin(coordinate: item.coordinate, tint: .red)

'MapPin' was deprecated in iOS 16.0: Use MapMarker


MapMarker는 커스터마이징을 할수 없다. 그럴때는  MapAnnotation을 사용하자.

아래는 동그라미를 보여주거나 텍스트를 보여주는 예제이다.

        Map(coordinateRegion: $region, showsUserLocation: false, userTrackingMode: .constant(.none), annotationItems: annotations) { item in
            MapAnnotation(coordinate: item.coordinate) {
                Circle()
                    .fill(Color.red.opacity(0.3))
                    .frame(width: 200, height: 200)
            }
        }
        .ignoresSafeArea()

Map(coordinateRegion: $region, showsUserLocation: false, userTrackingMode: .constant(.none), annotationItems: annotations) { item in
    MapAnnotation(coordinate: item.coordinate) {
        Text(item.name)
            .padding(.horizontal, 10)
            .padding(.vertical, 10)
            .background(Color.red.opacity(0.5))
            .cornerRadius(20)
    }
}
.ignoresSafeArea()

MapAnnotation을 이용하면 원하는 모양으로 자유자재로 아이콘을 만들수 있다.

MyAnnotationView를 만들어서 MapAnnotation을 보여주는 샘플.

Map(coordinateRegion: $region, annotationItems: annotations) { item in
    MapAnnotation(coordinate: item.coordinate, content: {
        MyAnnotationView(name: item.name)
    })            
}
.ignoresSafeArea()

struct MyAnnotationView: View {
    let accentColor = Color.blue
    let name: String
    init(name: String) {
        self.name = name
    }
    var body: some View {
        VStack(spacing: 0) {
            Text(name)
                .padding(.horizontal, 10)
                .padding(.vertical, 5)
                .background(Color.red.opacity(0.5))
                .cornerRadius(20)
            Image(systemName: "map.circle.fill")
                .resizable()
                .scaledToFit()
                .frame(width: 30, height: 30)
                .font(.headline)
                .foregroundColor(.white)
                .padding(6)
                .background(accentColor)
                .cornerRadius(36)
            Image(systemName: "triangle.fill")
                .resizable()
                .scaledToFit()
                .foregroundColor(accentColor)
                .frame(width: 10, height: 10)
                .rotationEffect(Angle(degrees: 180))
                .offset(y:-3)
                .padding(.bottom, 40)
        }
    }
}

이제 annotation을 선택하면 화면 하단에 Sheet를 보여주고, 선택한 annotation의 이름을 보여주는 코드를 만들어 보자.

MyAnnotationView에 onTapGesture를 추가하고 showSheet을 변경하자. 그리고 showSheet가 true이면 쉬트 뷰를 보여주기 위해 ".sheet"를 추가하자.

@State private var showSheet = false
	MyAnnotationView(name: item.name)
        .onTapGesture {
            showSheet.toggle()
        }
        .sheet(isPresented: $showSheet) {
            BottomSheetContent(name: item.name)
        }

BottomSheetContent는 각자 요구에 맞게 만들면 된다. 이제 annotation을 클릭하면 BottomSheetContent가 보이게 된다.

struct BottomSheetContent: View {
    var name: String
    
    var body: some View {
        VStack {
            Text("선택 : \(name)")
                .font(.title)
        }
        .frame(maxHeight: .infinity)
        .background(Color.white)
        .cornerRadius(20)
        .padding()
    }
}

 

 

 

 

 

Privacy - Location When In Use Usage Description

지도를 사용할려면 권한이 필요합니다.

 

 

import SwiftUI
import CoreLocation

class MyLocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    private var locationManager = CLLocationManager()
    static let shared = MyLocationManager()

    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.startUpdatingLocation()
    }
    
    public func requestLocation() {
        locationManager.requestWhenInUseAuthorization()
    }

    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        case .notDetermined:
            requestLocation()
            print("Location authorization status: Not Determined")
        case .restricted:
            print("Location authorization status: Restricted")
        case .denied:
            print("Location authorization status: Denied")
        case .authorizedAlways:
            print("Location authorization status: Authorized Always")
        case .authorizedWhenInUse:
            print("Location authorization status: Authorized When In Use")
        @unknown default:
            print("Location authorization status: Unknown")
        }
    }
}

@ObservedObject var locationManager = MyLocationManager.shared

.ignoresSafeArea()

        .onAppear {

            locationManager.requestLocation()

        }

Map(coordinateRegion: $region, showsUserLocation: true, annotationItems: annotations)

struct ContentView: View {
    @ObservedObject var locationManager = MyLocationManager.shared
    @State private var showSheet = false
    @State private var region = MKCoordinateRegion(
        center: MapConstants.seoulCenter,
        span: MapConstants.defaultSpan
    )

    var body: some View {
        Map(coordinateRegion: $region, showsUserLocation: true, annotationItems: annotations) { item in
            MapAnnotation(coordinate: item.coordinate, content: {
                MyAnnotationView(name: item.name)
                    .onTapGesture {
                        showSheet.toggle()
                    }
                    .sheet(isPresented: $showSheet) {
                        BottomSheetContent(name: item.name)
                    }
            })
        }
        .ignoresSafeArea()
        .onAppear {
            locationManager.requestLocation()
        }
    }
}

 

'iOS개발 > SwiftUI' 카테고리의 다른 글

[SwiftUI] 앱 전체에 다크모드 적용하기  (0) 2023.02.28