Какие сложности возникают при интеграции MapKit в архитектуру Clean Swift (VIP) и как их решить?

«Какие сложности возникают при интеграции MapKit в архитектуру Clean Swift (VIP) и как их решить?» — вопрос из категории Архитектура, который задают на 10% собеседований IOS Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Основная проблема: MapKit (MKMapView, MKMapViewDelegate) тесно связан с UIKit и контроллером, что нарушает принципы Clean Swift, где ViewController является пассивным View, а бизнес-логика изолирована в Interactor.

Конкретные сложности:

  1. Нарушение Single Responsibility: ViewController вынужден обрабатывать картографические делегаты, смешивая логику отображения и реакцию на действия пользователя.
  2. Сложность тестирования: MKMapView и его делегаты тяжело мокировать для unit-тестов Interactor и Presenter.
  3. Утечка абстракций: События карты (выбор аннотации, изменение региона) представлены типами MapKit, а не доменными моделями.

Решение — выделение MapManager: Создайте отдельный сервис (MapManager), который выступает адаптером между MapKit и VIP-циклом.

// 1. Протокол для передачи событий от карты к бизнес-логике
protocol MapManagerOutput: AnyObject {
    func mapManager(_ manager: MapManager, didSelectAnnotationWith id: String)
    func mapManager(_ manager: MapManager, regionDidChangeTo coordinate: CoordinateModel)
}

// 2. Менеджер, инкапсулирующий работу с MapKit
final class MapManager: NSObject {
    weak var output: MapManagerOutput?
    private unowned let mapView: MKMapView

    init(mapView: MKMapView) {
        self.mapView = mapView
        super.init()
        mapView.delegate = self
    }

    func configure(with annotations: [AnnotationModel]) { /* ... */ }
}

// 3. Реализация делегатов MapKit внутри менеджера
extension MapManager: MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        guard let annotation = view.annotation as? MyCustomAnnotation else { return }
        // Конвертируем событие MapKit в доменную модель
        output?.mapManager(self, didSelectAnnotationWith: annotation.identifier)
    }
}

// 4. Использование в ViewController (Clean Swift View)
final class MapViewController: UIViewController {
    var interactor: MapBusinessLogic?
    private var mapManager: MapManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        let mapView = MKMapView(frame: view.bounds)
        view.addSubview(mapView)

        mapManager = MapManager(mapView: mapView)
        mapManager.output = self
    }
}

extension MapViewController: MapManagerOutput {
    func mapManager(_ manager: MapManager, didSelectAnnotationWith id: String) {
        // Передаём событие в VIP-цикл
        let request = Map.Selection.Request(annotationId: id)
        interactor?.handleAnnotationSelection(request: request)
    }
}

Итог: MapManager становится тестируемым компонентом, а ViewController остаётся «глупым», просто передавая события дальше по VIP-циклу.