apple/Docs, iOS, Swift

[WWDC23] Meet MapKit for SwiftUI

lgvv 2023. 6. 18. 16:05

Meet MapKit for SwiftUI

 

🔨 Xcode 15.0 Beta

iOS 17.0 +

Apple M1 Max

 

WWDC23 - Video

https://developer.apple.com/wwdc23/10043

 

Meet MapKit for SwiftUI - WWDC23 - Videos - Apple Developer

Discover how expanded SwiftUI support for MapKit has made it easier than ever for you to integrate Maps into your app. We'll show you how...

developer.apple.com

 

 

이번 WWDC23에서 흥미로운 세션이 많았지만, 가장 끌리는 MapKit을 정리해보고자 한다.

특히 SwiftData에서 CoreData가 Objective-C era 라고 표현한 부분에서 전율이 일었음.

 

✅ 시청리스트

 - AppIntent

 - SwiftData 

 - Xcode

 - MapKit

 - ScrollView

 - Macro 

 

 

✅ MapKit 전체 코드로 알아보기

 

 

🚨 (2023.06.19 00:25) XCode 15.0 beta를 기준으로 UserAnnotation()이 작동하지 않고 있습니다. 앱에서만 작동하지 않는게 아니라, 시뮬레이터 내 지도 앱에서도 작동하지 않는 것으로 확인했습니다. 현재는 iOS 17을 실제 디바이스에 설치할 수 없어서 설치 후 다시 확인해보도록 합시다.

 

 

 - 아래 코드를 그대로 복사해서 사용하시면 예제를 확인하실 수 있습니다.

 

 

 

//
//  ContentView2.swift
//  WWDC23_Map
//
//  Created by Karrot on 2023/06/18.
//

import SwiftUI
import MapKit

struct ContentView: View {
    @State private var position: MapCameraPosition = .automatic // 지도에서 카메라의 위치를 나타내는 변수
    @State private var visibleRegion: MKCoordinateRegion? // 지도에서 보여줄 위치를 나타내는 변수
    
    @State private var searchResults: [MKMapItem] = [] // 검색을 통해 결과로 보여줄 아이템들
    @State private var selectedResult: MKMapItem? // 선택한 아이템
    @State private var route: MKRoute? // 지도에 보여줄 루트
    
    @Namespace var mapScope // 맵 스코프를 위한 변수
    
    var body: some View {
        Map(position: $position, selection: $selectedResult, scope: mapScope) {
            Annotation("주차장", coordinate: .parking) {
                ZStack {
                    RoundedRectangle(cornerRadius: 5)
                        .fill(.background)
                    RoundedRectangle(cornerRadius: 5)
                        .stroke(.secondary, lineWidth: 5)
                    Image(systemName: "car")
                        .padding(5)
                }
            }
            .annotationTitles(.hidden)
            
            ForEach(searchResults, id: \.self) { result in
                Marker(item: result)
            }
            .annotationTitles(.hidden)
            
            UserAnnotation() // 사용자의 현재 위치
            
            if let route { // 검색결과에 따른 루트가 존재하면 아래의 코드로 라인을 연결
                MapPolyline(route)
                    .stroke(.blue, lineWidth: 5)
            }
            
            MapPolyline(coordinates: FancyMapPolyLine.walkingCoordinates) // 라인 연결의 커스텀 예시
                .stroke(FancyMapPolyLine.gradient, style: FancyMapPolyLine.stroke)
            
        }
        .mapStyle(.standard(elevation: .realistic)) // 맵 스타일 지정 옵션
        .safeAreaInset(edge: .bottom) {
            HStack {
                Spacer()
                VStack(spacing: 0) {
                    if let selectedResult {
                        ItemInfoView(selectedResult: selectedResult, route: route)
                            .frame(height: 128)
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                            .padding([.top, .horizontal])
                    }
                    
                    BeantwonButtons(position: $position,
                                    searchResults: $searchResults,
                                    visibleRegion: visibleRegion)
                }
                
                
                Spacer()
            }
            .background(.thinMaterial)
        }
        .onChange(of: searchResults) {
            position = .automatic
        }
        .onChange(of: selectedResult) {
            getDirection() //
        }
        .onMapCameraChange { context in
            visibleRegion = context.region
        }
//        .mapControls {
//            MapUserLocationButton() // 내위치
//            MapCompass() // 맵 자체가 디폴트가 회전할 때 컴패스 보여준다.
//            MapScaleView() // MacOS에서 빛을 바란다.
//        }
        .overlay(alignment: .bottomTrailing) {
            VStack {
                MapUserLocationButton(scope: mapScope)
                MapCompass(scope: mapScope) // 맵 자체가 디폴트가 회전할 때 컴패스 보여준다.
                MapScaleView(scope: mapScope) // MacOS에서 빛을 바란다.
                    .mapControlVisibility(.visible)
            }
            .padding(.trailing, 10)
            .buttonBorderShape(.circle)
        }
        .mapScope(mapScope)
    }
    
    /// route를 찾는 함수
    func getDirection() {
        route = nil // route 초기화
        guard let selectedResult else { return } // 선택한 것이 있다면
        
        let request = MKDirections.Request() // 요청을 위한 변수
        request.source = MKMapItem(placemark: MKPlacemark(coordinate: .parking)) // 출발지 좌표
        request.destination = selectedResult // 목적지
        
        Task {
            let directions = MKDirections(request: request) // 출발지랑 목적지 넣어서 request
            let response = try? await directions.calculate() // response 비동기로 기다리기, 계산은 알아서 해줌
            
            route = response?.routes.first // 출발지와 목적지 사이의 여러 케이스 중 가장 첫번째 결과 선택
        }
    }
}

struct BeantwonButtons: View {
    
    @Binding var position: MapCameraPosition
    @Binding var searchResults: [MKMapItem]
    
    var visibleRegion: MKCoordinateRegion?
    
    var body: some View {
        HStack {
            Button {
                search(for: "playground")
            } label: {
                Label("playground", systemImage: "figure.and.child.holdinghands")
            }
            .buttonStyle(.borderedProminent)
            
            Button {
                search(for: "beach")
            } label: {
                Label("beach", systemImage: "beach.umbrella")
            }
            .buttonStyle(.borderedProminent)
        }
        .labelStyle(.iconOnly)
    }
    
    /// 위치를 검색하는 함수
    func search(for query: String) {
        let request = MKLocalSearch.Request() // 지역검색을 위한 request 생성
        request.naturalLanguageQuery = query // 검색 쿼리문 넣어주기 - 한국어도 검색이 가능하나, 부정확한듯.
        request.resultTypes = .pointOfInterest // 핀으로 해야 지도에 핀 나타남
        request.region = visibleRegion ?? MKCoordinateRegion( // visibleRegion이 지정되어 있으면 해당 위치를 기반으로 아니면 default값인 주자창을 기반으로
            center: .parking,
            span: MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
        )
        
        Task {
            let search = MKLocalSearch(request: request) // 요청
            let response = try? await search.start() // 위치검색 시작!
            searchResults = response?.mapItems ?? [] // 결과 반환, 없을수도 있음.
        }
    }
}

#Preview {
    ContentView()
}

/// AroundScene에 사용되는 뷰
struct ItemInfoView: View {
    @State private var lookAroundScene: MKLookAroundScene? // aroundScene 변수
    
    var selectedResult: MKMapItem
    var route: MKRoute?
    
    /// route의 값을 가지고 얼마나 걸릴지 예상되는 시간을 계산해서 반환해줌. -> 다만, 한국에서 부정확하다고 느낌
    private var travelTime: String? {
        guard let route else { return nil }
        let formatter = DateComponentsFormatter()
        formatter.unitsStyle = .abbreviated
        formatter.allowedUnits = [.hour, .minute]
        return formatter.string(from: route.expectedTravelTime)
    }
    
    var body: some View {
        LookAroundPreview(initialScene: lookAroundScene) // AroundScene을 보여줌
            .overlay(alignment: .bottomTrailing) {
                HStack {
                    Text("\(selectedResult.name ?? "")")
                    if let travelTime {
                        Text(travelTime)
                    }
                }
                .font(.caption)
                .foregroundStyle(.white)
                .padding(10)
            }
            .onAppear {
                getLookAroundScene() // LookAroundPreview 나타날 때 로드
            }
            .onChange(of: selectedResult) {
                getLookAroundScene() // 선택결과가 바뀌면 리로드
            }
    }
    
    func getLookAroundScene() {
        lookAroundScene = nil // 초기화
        Task {
            let request = MKLookAroundSceneRequest(mapItem: selectedResult) // 어라운드 씬
            lookAroundScene = try? await request.scene // 씬을 넣어주기
        }
    }
}

// PolyLine을 Fancy하게 사용해보자~!
struct FancyMapPolyLine {
    static let walkingCoordinates: [CLLocationCoordinate2D] = [.company, .lunch, .isLakeCenter, .parking]
    
    static let gradient = LinearGradient(
        colors: [.red, .green, .blue],
        startPoint: .leading, endPoint: .trailing
    )
    
    static let stroke = StrokeStyle(
        lineWidth: 5,
        lineCap: .round, lineJoin: .round, dash: [10, 10]
    )
}

extension CLLocationCoordinate2D {
    static let parking = CLLocationCoordinate2D(latitude: 37.399112, longitude: 127.102671)
    static let company = CLLocationCoordinate2D(latitude:  37.401177, longitude: 127.101234)
    static let lunch = CLLocationCoordinate2D(latitude: 37.400169, longitude: 127.104638)
    
    static let isLakeCenter = CLLocationCoordinate2D(latitude: 37.399057, longitude: 127.104580)
    
    
}

extension [CLLocationCoordinate2D] {
    static let shuttle: [CLLocationCoordinate2D] = [
        .init(latitude: 37.400925, longitude: 127.100926),
        .init(latitude: 37.400920, longitude: 127.101395)
    ]
    
    
    static let cafe: [CLLocationCoordinate2D] = [
        .init(latitude: 37.4007736, longitude: 127.101928),
        .init(latitude: 37.4007736, longitude: 127.102274),
        .init(latitude: 37.3999919, longitude: 127.102274),
        .init(latitude: 37.3999919, longitude: 127.101928),
        .init(latitude: 37.4007736, longitude: 127.101928)
    ]
}

extension CLLocationDistance {
    static let isLakeRadius = CLLocationDistance(40)
}

extension MKCoordinateRegion {
    static let nexon = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 37.40241,
            longitude: 127.10377),
        span: MKCoordinateSpan(
            latitudeDelta: 0.5,
            longitudeDelta: 0.5)
    )
    
    static let midas = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 37.40028,
            longitude: 127.10148),
        span: MKCoordinateSpan(
            latitudeDelta: 0.5,
            longitudeDelta: 0.5)
    )
}

 

더 다양한 커스텀을 위한 코드 예시

- 맵 스타일도 여러개를 적용해보면 좋음.

            Marker("주차장", monogram: "당근",
                   coordinate: .parking)
            .tint(.blue)
            Marker("회사", systemImage: "figure.wave",
                   coordinate: .company)
            .tint(.bar)
            
            Annotation(
                "점심",
                coordinate: .lunch,
                anchor: .bottom
            ) {
                Image(systemName: "figure.wave")
                    .padding(4)
                    .foregroundStyle(.white)
                    .background(Color.indigo)
                    .cornerRadius(4)
            }
            
            
            MapCircle(center: .isLakeCenter, radius: .isLakeRadius)
                .foregroundStyle(.orange.opacity(0.75))
            
            MapPolyline(coordinates: .shuttle)
                .stroke(.blue, lineWidth: 13)

            MapPolygon(coordinates: .cafe)
                .foregroundStyle(.purple)

 

'apple > Docs, iOS, Swift' 카테고리의 다른 글

[Swift] plain ol' data(POD)  (0) 2023.08.08
[UIKit] UILabel Inset  (0) 2023.06.23
[XCode 15.0 beta] Preview Macro Bug  (0) 2023.06.08
[Swift] 커링(Currying)  (1) 2023.02.28
[iOS] UIImage.Orientation  (0) 2022.12.09