MapKit 을 사용한 예제 앱 개발

이번엔 최신 코드인 @Observable 매크로를 사용하여 viewModel 을 사용하였습니다.

@Observable는 @Published 를 따로 쓰지 않아도 상태 추적이 가능하도록 해주는 매크로입니다.

MapViewModel 생성

@Observable
class MapViewModel {
    var cameraPosition: MapCameraPosition = .userLocation(fallback: .automatic)

    var searchText: String = ""
}

@Observable 를 사용한 ViewModel 은 View에서 @StateObject가 아닌, @State로 불러올 수 있습니다.

import SwiftUI
import MapKit

struct ContentView: View {
    @State var viewModel = MapViewModel()

    var body: some View {
        NavigationStack {
            Map(position: $viewModel.cameraPosition)
        }
    }
}

만약 하위 뷰에서 해당 viewModel 을 사용하고자 한다면? 바인딩도 필요없고, environmentObject도 사용하지 않습니다. ContentView 에서 viewModel 을 완전히 소유하고 있기 때문에, 그냥 변수를 전달하면 됩니다.

// ContentView

ExtractSubView(viewModel: viewModel)


// ExtractSubView
struct ExtractSubView: View {
    var viewModel: MapViewModel
}

그리고 이제 Navigation Title 과 검색바(searchable)를 추가하였습니다.

.navigationTitle("SearchLocation")
.searchable(text: $viewModel.searchText)

그리고 mapStyle 을 사용하기 위해서, viewModel 에 MapStyle 타입의 프로퍼티를 추가하고, mapStyle 변경은 searchScope와 onChnage 사용하여 변경할 수 있도록 하였습니다.

// MapViewModel

// ContentView
                .searchScopes($style) {
                    Text("Standard").tag(0)
                    Text("Imagely").tag(1)
                    Text("Hybrid").tag(2)
                }
                .onChange(of: style) {
                    switch style {
                    case 0:
                        viewModel.mapStyle = .standard
                    case 1:
                        viewModel.mapStyle = .imagery
                    case 2:
                        viewModel.mapStyle = .hybrid
                    default:
                        viewModel.mapStyle = .standard
                    }
                }

User Location Button

mapControls 를 사용하여 간단하게 추가 가능합니다.

.mapControls {
    UserLocationButton()
}

그리고 이제 본격적으로 MapViewModel 이 CLLocationManagerDelegate 프로토콜을 준수하기 위한 init, 함수를 추가합니다.

// CLLocationManagerDelegate 을 준수하기 위해
// NSObject 를 추가하였고,
// NSObject 는 상속할 클래스에서 초기화를 받아야 하기 때문에 override init 호출로 바꿔줍니다.
class MapViewModel: NSObject, CLLocationManagerDelegate {

    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.requestWhenInUseAuthorization()
    }

    // MARK: CLLocationManagerDelegate
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        
    }
    
    func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
        print("error \(error.localizedDescription)")
    }
}

userLocation 에 따라 MapCameraPosition 값이 변경되는 코드!

아까 만든 SearchBar 에 .onSubmit 를 추가하여 검색 기능을 호출하도록 합니다.

.onSubmit(of: .search) {
    viewModel.searchLocation()
}

viewModel.searchLocation 코드입니다.

    func searchLocation() {
         print("search!")

         let request = MKLocalSearch.Request()
         request.naturalLanguageQuery = searchText
         request.resultTypes = .pointOfInterest

         let search = MKLocalSearch(request: request)

         search.start { [weak self] response, error in
             guard let response = response else { return }
             self?.searchResults = response.mapItems
         }
     }