Ответ
Основная проблема: 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-циклу.
Ответ 18+ 🔞
А, ну это же классика, блядь! Чистый Swift, да? А тут тебе MapKit лезет со своими пиздюлинами, всё ломает, как слон в посудной лавке.
Смотри, в чём сука трагедия. Ты сидишь, такой красавчик, архитектуру выстраиваешь: вот Interactor — мозг, вот Presenter — переводчик, вот View — тупая рожа на экране. И тут вылазит эта MKMapView, как хитрая жопа, и начинает: «А дай-ка я в твоём ViewController’е поживу, и делегаты твои поем, и всю логику твою насру».
Конкретно, где пиздец:
- Ответственность на одного. Твой бедный ViewController превращается в уборщика: он и карту показывает, и на тычки по ней реагирует, и ещё бог знает что делает. Single Responsibility? Да похуй! Один за всех, и все на одного.
- Протестируй это, ёпта. Как ты будешь Interactor’у в тестах говорить: «Слушай, вот тут пользователь тапнул по аннотации»? Эмулировать
MKMapViewDelegate? Это ж пиздец какой-то, а не unit-тест. Волнение ебать, терпения ноль ебать. - Типы MapKit’а везде. У тебя в самой сердцевине, в бизнес-логике, вдруг появляется
MKAnnotationView. Это как в стерильную операционную завезти лопату с навозом. Утечка абстракций, блядь!
Решение — сделать отсебятину, то есть MapManager.
Смысл в чём? Создаём отдельного мудака, который будет этим MapKit’ом рулить. Он будет тем самым говном, которое принимает на себя все пинки от карты, а тебе на выходе выдаёт уже чистые, доменные события.
// 1. Протокол для общения. Менеджер будет орать сюда, когда что-то случится.
protocol MapManagerOutput: AnyObject {
func mapManager(_ manager: MapManager, didSelectAnnotationWith id: String) // Смотри, не MKAnnotation, а просто String — ID!
func mapManager(_ manager: MapManager, regionDidChangeTo coordinate: CoordinateModel) // И не CLLocationCoordinate2D, а наша доменная хуйня!
}
// 2. Сам менеджер, главный по тарелочкам.
final class MapManager: NSObject {
weak var output: MapManagerOutput?
private unowned let mapView: MKMapView // Держим карту за жопу
init(mapView: MKMapView) {
self.mapView = mapView
super.init()
mapView.delegate = self // И ВОТ МЫ ПОДСАЖИВАЕМСЯ НА ИГЛУ! Но только мы, менеджер. ViewController об этом даже не знает.
}
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.
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 // Говорим: «Кричи сюда, когда что-то будет».
}
}
// ViewController реализует протокол менеджера. Он НЕ ЗНАЕТ про MKMapViewDelegate!
extension MapViewController: MapManagerOutput {
func mapManager(_ manager: MapManager, didSelectAnnotationWith id: String) {
// Получили чистое событие — передали дальше в VIP-цикл.
let request = Map.Selection.Request(annotationId: id)
interactor?.handleAnnotationSelection(request: request)
}
}
Итог, блядь: MapManager становится тем самым громоотводом, который ловит все молнии от MapKit. Его можно протестировать отдельно (ну, почти). А твой VIP-цикл остаётся чистым, как слеза младенца. ViewController — тупой и пассивный, как и положено. Всё, ебать, красиво.