Ответ
Основная проблема: MapKit (MKMapView, MKMapViewDelegate) тесно связан с UIKit и контроллером, что нарушает принципы Clean Swift, где ViewController является пассивным View, а бизнес-логика изолирована в Interactor.
Конкретные сложности:
- Нарушение Single Responsibility: ViewController вынужден обрабатывать картографические делегаты, смешивая логику отображения и реакцию на действия пользователя.
- Сложность тестирования:
MKMapViewи его делегаты тяжело мокировать для unit-тестов Interactor и Presenter. - Утечка абстракций: События карты (выбор аннотации, изменение региона) представлены типами 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-циклу.