Пример того, как включить функциональность MapKit в ваше приложение SwiftUI

SwiftUI имеет хорошую интеграцию с существующим фреймворком UIkit. Вы можете добиться взаимодействия SwiftUI с UIKit, заключив UIViewControllers в представления SwiftUI и наоборот; встраивать представления SwiftUI в контроллеры представлений.

В этом посте я покажу вам пример того, как включить функцию MapKit в ваше приложение SwiftUI, обернув контроллер представления.

Цель этой демонстрации - создать представление, содержащее панель поиска и вид карты. Пользователь вводит текст в строке поиска; первый результат в списке результатов, полученных при поиске по карте, добавляется к карте в виде булавки.

Наконец, я покажу, как взаимодействовать с внешним видом, создав делегата для контроллера.

Начните с View Controller

Создайте контроллер представления и программно включите все элементы, которые вы хотите отобразить в представлении; панель поиска и вид карты:

class MapViewController: UIViewController {
 var mapView: MKMapView!
 
 // MARK: - Search
 
 fileprivate var searchBar: UISearchBar!
 fileprivate var localSearchRequest: MKLocalSearch.Request!
 fileprivate var localSearch: MKLocalSearch!
 fileprivate var localSearchResponse: MKLocalSearch.Response!
 
 // MARK: - Map variables
 
 fileprivate var annotation: MKAnnotation!
 fileprivate var isCurrentLocation: Bool = false
 
 var selectedPin: MKPlacemark?
// MARK: - UIViewController's methods
 
 override func viewDidLoad() {
  super.viewDidLoad()
  
  mapView = MKMapView()
  
  let leftMargin:CGFloat = 10
                let topMargin:CGFloat = 60
                let mapWidth:CGFloat = view.frame.size.width-20
                let mapHeight:CGFloat = view.frame.size.height - 100
        
                mapView.frame = CGRect(x: leftMargin, y: topMargin, 
                      width: mapWidth, height: mapHeight)
  mapView.isZoomEnabled = true
                mapView.isScrollEnabled = true
  self.view.addSubview(mapView)
  
  searchBar = UISearchBar(frame: CGRect(x: 10, y: 0, width: mapWidth, height: 60))
  searchBar.delegate = self
  self.view.addSubview(searchBar)
  
  mapView.delegate = self
  mapView.mapType = .hybrid
 }
}

Карта Делегат

Объект MKMapView требует, чтобы контроллер представления соответствовал MKMapViewDelegate.

В этом примере я буду реализовывать только метод mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation), используемый для добавления аннотаций к карте.

Кроме того, в аннотации будет отображаться небольшая кнопка +. Я могу, например, использовать его для добавления метки в хранилище меток. Я расскажу об этом позже.

На данный момент я просто сохраняю пустую реализацию для действия savedPin.

extension MapViewController:MKMapViewDelegate{
 
 func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
  guard !(annotation is MKUserLocation) else { return nil }
  let reuseId = "pin"
  var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
  if pinView == nil {
   pinView = MKPinAnnotationView(annotation: annotation, 
                                reuseIdentifier: reuseId)
  }
pinView?.pinTintColor = UIColor.orange
  pinView?.canShowCallout = true
  let smallSquare = CGSize(width: 30, height: 30)
  let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
  
  button.setBackgroundImage(UIImage(systemName: "plus.square"), for: .normal)
  button.addTarget(self, action: #selector(savedPin), for: .touchUpInside)
  
  pinView?.leftCalloutAccessoryView = button
  
  return pinView
 }
 
 @objc func savedPin(){
  
 }
}

SearchBar Delegate

Строке поиска необходимо, чтобы контроллер соответствовал UISearchBarDelegate.

Я реализую только метод searchBarSearchButtonClicked(_ searchBar: UISearchBar); это выполняется, когда пользователь нажимает клавишу ВВОД после ввода текста в строке поиска.

Функция поиска использует MKLocalSearch для выполнения поиска с помощью набора карт Apple и возвращает список элементов карты. Затем я беру первую из списка и добавляю на карту булавку, содержащую метку, встроенную в этот элемент карты.

Методы addPin и addAnnotation помогают создать аннотацию из метки и добавить ее на карту. Наконец, showAlert используется для отображения предупреждения, если поиск не привел к каким-либо результатам.

// MARK: - UISearchBarDelegate
 func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if self.mapView.annotations.count != 0 {
   annotation = self.mapView.annotations[0]
   self.mapView.removeAnnotation(annotation)
  }
  
  localSearchRequest = MKLocalSearch.Request()
  localSearchRequest.naturalLanguageQuery = searchBar.text
  localSearch = MKLocalSearch(request: localSearchRequest)
  localSearch.start { (localSearchResponse, error) -> Void in
   if localSearchResponse == nil {
    return self.showAlert()
   }
   
   guard let mapItem = localSearchResponse?.mapItems.first else {
    return self.showAlert()
   }
   
   let placemark = mapItem.placemark
   
   self.addPin( placemark: placemark)
   self.selectedPin = placemark
  }
 }
 
 func showAlert(){
  let alert = UIAlertController(title: nil, message:"Place not found", preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "Try again", style: .default) { _ in })
  self.present(alert, animated: true){}
 }
 
 func addPin(placemark: MKPlacemark){
  let annotation = MKPointAnnotation()
  annotation.coordinate = placemark.coordinate
  annotation.title = placemark.name
  
  if let city = placemark.locality,
   let state = placemark.administrativeArea {
    annotation.subtitle = "\(city) \(state)"
  }
  
  addAnnotation(annotation: annotation)
 }
 
 func addAnnotation( annotation:MKPointAnnotation ){
  mapView.addAnnotation(annotation)
  let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
  let region = MKCoordinateRegion(center: annotation.coordinate, span: span)
  mapView.setRegion(region, animated: true)
 }
}

Взаимодействие с внешним миром

Очевидно, я не могу содержать функциональность онлайн внутри этого контроллера. Представления, которые будут обертывать, а затем встраивать этот контроллер, должны запускаться, когда внутри контроллера выполняются различные действия.

Поэтому я реализую протокол делегата для взаимодействия с моим MapViewController:

protocol MapViewDelegate{
 func saveLocation(placemark: MKPlacemark)
}

Впоследствии добавьте в контроллер переменную делегата:

var delegate:MapViewDelegate!

И, наконец, реализуйте метод действия savedPin() для использования этого делегата:

@objc func savedPin(){
 guard let delegate = delegate, let placemark = selectedPin else { return}
 delegate.saveLocation(placemark: placemark)
}

Окончательная реализация контроллера

import UIKit
import MapKit
protocol MapViewDelegate{
 func saveLocation(placemark: MKPlacemark)
}
class MapViewController: UIViewController {
 
 var delegate:MapViewDelegate!
 
 var mapView: MKMapView!
 
 // MARK: - Search
 
 fileprivate var searchBar: UISearchBar!
 fileprivate var localSearchRequest: MKLocalSearch.Request!
 fileprivate var localSearch: MKLocalSearch!
 fileprivate var localSearchResponse: MKLocalSearch.Response!
 
 // MARK: - Map variables
 
 fileprivate var annotation: MKAnnotation!
 fileprivate var isCurrentLocation: Bool = false
 
 var selectedPin: MKPlacemark?
// MARK: - UIViewController's methods
 
 override func viewDidLoad() {
  super.viewDidLoad()
  
  mapView = MKMapView()
  
  let leftMargin:CGFloat = 10
        let topMargin:CGFloat = 60
        let mapWidth:CGFloat = view.frame.size.width-20
        let mapHeight:CGFloat = view.frame.size.height - 100
        
        mapView.frame = CGRect(x: leftMargin, y: topMargin, width: mapWidth, height: mapHeight)
  mapView.isZoomEnabled = true
        mapView.isScrollEnabled = true
  self.view.addSubview(mapView)
  
  searchBar = UISearchBar(frame: CGRect(x: 10, y: 0, width: mapWidth, height: 60))
  searchBar.delegate = self
  self.view.addSubview(searchBar)
  
  mapView.delegate = self
  mapView.mapType = .hybrid
 }
}
 
extension MapViewController:MKMapViewDelegate{
 
 func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
  guard !(annotation is MKUserLocation) else { return nil }
  let reuseId = "pin"
  var pinView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseId) as? MKPinAnnotationView
  if pinView == nil {
   pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
  }
pinView?.pinTintColor = UIColor.orange
  pinView?.canShowCallout = true
  let smallSquare = CGSize(width: 30, height: 30)
  let button = UIButton(frame: CGRect(origin: CGPoint.zero, size: smallSquare))
  
  button.setBackgroundImage(UIImage(systemName: "plus.square"), for: .normal)
  button.addTarget(self, action: #selector(savedPin), for: .touchUpInside)
  
  pinView?.leftCalloutAccessoryView = button
  
  return pinView
 }
 
 @objc func savedPin(){
  guard let delegate = delegate, let placemark = selectedPin else { return}
  delegate.saveLocation(placemark: placemark)
 }
}
extension MapViewController:UISearchBarDelegate{
 // MARK: - UISearchBarDelegate
 func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
  if self.mapView.annotations.count != 0 {
   annotation = self.mapView.annotations[0]
   self.mapView.removeAnnotation(annotation)
  }
  
  localSearchRequest = MKLocalSearch.Request()
  localSearchRequest.naturalLanguageQuery = searchBar.text
  localSearch = MKLocalSearch(request: localSearchRequest)
  localSearch.start { (localSearchResponse, error) -> Void in
   if localSearchResponse == nil {
    return self.showAlert()
   }
   
   guard let mapItem = localSearchResponse?.mapItems.first else {
    return self.showAlert()
   }
   
   let placemark = mapItem.placemark
   
   self.addPin( placemark: placemark)
   self.selectedPin = placemark
  }
 }
 
 func showAlert(){
  let alert = UIAlertController(title: nil, message:"Place not found", preferredStyle: .alert)
  alert.addAction(UIAlertAction(title: "Try again", style: .default) { _ in })
  self.present(alert, animated: true){}
 }
 
 func addPin(placemark: MKPlacemark){
  let annotation = MKPointAnnotation()
  annotation.coordinate = placemark.coordinate
  annotation.title = placemark.name
  
  if let city = placemark.locality,
   let state = placemark.administrativeArea {
    annotation.subtitle = "\(city) \(state)"
  }
  
  addAnnotation(annotation: annotation)
 }
 
 func addAnnotation( annotation:MKPointAnnotation ){
  mapView.addAnnotation(annotation)
  let span = MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
  let region = MKCoordinateRegion(center: annotation.coordinate, span: span)
  mapView.setRegion(region, animated: true)
 }
}

Оберните контроллер

Теперь давайте обернем ранее созданный контроллер представления в UIViewControllerRepresentable, который будет использоваться позже в представлениях SwiftUI. Это стандартная реализация, по примеру Apple.

Класс coordinator, соответствующий протоколу MapViewDelegate, реализует метод saveLocation (пока только печатает). makeUIViewController создает контроллер сверху и возвращает его.

После этого updateUIViewController присоединяет Coordinator в качестве делегата к MapViewController.

struct MapSearchView: UIViewControllerRepresentable {
 class Coordinator: NSObject, MapViewDelegate {
  func saveLocation(placemark: MKPlacemark) {
   print("add placemark" )
  }
    }
func makeCoordinator() -> Coordinator {
  return Coordinator()
    }
func makeUIViewController(context: UIViewControllerRepresentableContext<MapSearchView>) -> MapViewController {
        let mapController = MapViewController()
  
        return mapController
    }
func updateUIViewController(_ uiViewController: MapViewController,
                                context: UIViewControllerRepresentableContext<MapSearchView>) {
  
  uiViewController.delegate = context.coordinator
    }

использование

Теперь давайте воспользуемся этим представлением в примере приложения SwiftUI:

struct ContentView: View {
    var body: some View {
       MapSearchView()
   }
}

Вот как выглядят элементы управления в моем примере приложения:

Надеюсь, вам понравилось это руководство, и вы начнете включать MapKit в свои приложения SwiftUI.