// // MapViewController.swift // QuickLocation // import UIKit import RxSwift import RxCocoa import CoreLocation #if !targetEnvironment(simulator) import MAMapKit #endif final class MapViewController: BaseViewController { override var isNavigationBarHidden: Bool { true } override var preferredStatusBarStyle: UIStatusBarStyle { .darkContent } // MARK: - Properties fileprivate var rootView: MapView! private let viewModel = MapViewModel() private let locationManager = CLLocationManager() private var currentHeading: Double = 0 // MARK: - Lifecycle override func loadView() { #if !targetEnvironment(simulator) MAMapView.updatePrivacyAgree(.didAgree) MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain) #endif rootView = MapView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() setupMap() setupLocation() bindViewModel() bindUI() viewModel.loadMembers() } // MARK: - Map Setup private func setupMap() { #if !targetEnvironment(simulator) rootView.mapView.delegate = self #endif } private func setupLocation() { locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.requestWhenInUseAuthorization() locationManager.startUpdatingHeading() } // MARK: - Bindings private func bindViewModel() { viewModel.output.members .subscribe(onNext: { [weak self] members in self?.updateAnnotations(with: members) self?.rootView.configureMemberList(with: members) { member in self?.viewModel.selectMember(member) } }) .disposed(by: disposeBag) viewModel.output.trackingMode .subscribe(onNext: { [weak self] tracking in #if !targetEnvironment(simulator) self?.rootView.mapView.userTrackingMode = tracking ? .follow : .none #endif }) .disposed(by: disposeBag) viewModel.selectedMember .subscribe(onNext: { [weak self] member in self?.focusOnMember(member) }) .disposed(by: disposeBag) } private func bindUI() { // Left control buttons let sosTap = UITapGestureRecognizer() rootView.sosButton.addGestureRecognizer(sosTap) sosTap.rx.event.subscribe(onNext: { _ in print("SOS tapped") }) .disposed(by: disposeBag) let checkinTap = UITapGestureRecognizer() rootView.checkinButton.addGestureRecognizer(checkinTap) checkinTap.rx.event.subscribe(onNext: { _ in print("Check-in tapped") }) .disposed(by: disposeBag) let bubbleTap = UITapGestureRecognizer() rootView.bubbleButton.addGestureRecognizer(bubbleTap) bubbleTap.rx.event.subscribe(onNext: { _ in print("Bubble tapped") }) .disposed(by: disposeBag) // Location button rootView.locationButton.rx.tap .subscribe(onNext: { [weak self] in self?.viewModel.toggleTracking() }) .disposed(by: disposeBag) // Refresh button rootView.refreshButton.rx.tap .subscribe(onNext: { [weak self] in self?.viewModel.loadMembers() }) .disposed(by: disposeBag) // Invite button rootView.inviteButton.rx.tap .subscribe(onNext: { _ in print("Invite tapped") }) .disposed(by: disposeBag) // Avatar button rootView.avatarButton.rx.tap .subscribe(onNext: { _ in print("Avatar tapped") }) .disposed(by: disposeBag) // Announcement close rootView.announcementCloseBtn.rx.tap .subscribe(onNext: { [weak self] in self?.rootView.announcementBar.isHidden = true }) .disposed(by: disposeBag) } // MARK: - Annotations private func updateAnnotations(with members: [CircleMember]) { #if !targetEnvironment(simulator) let existing = rootView.mapView.annotations ?? [] let existingMemberIDs = Set(existing.compactMap { ($0 as? MemberAnnotation)?.member.id }) let newMemberIDs = Set(members.map { $0.id }) let toRemove = existing.filter { ann in guard let ma = ann as? MemberAnnotation else { return false } return !newMemberIDs.contains(ma.member.id) } rootView.mapView.removeAnnotations(toRemove) let toAdd = members.filter { !existingMemberIDs.contains($0.id) } let annotations = toAdd.map { MemberAnnotation(member: $0) } rootView.mapView.addAnnotations(annotations) #endif } private func focusOnMember(_ member: CircleMember) { #if !targetEnvironment(simulator) rootView.mapView.setCenter(member.coordinate, animated: true) #endif } private func updateCurrentUserHeading() { #if !targetEnvironment(simulator) guard let annotations = rootView.mapView.annotations else { return } for ann in annotations { guard let ma = ann as? MemberAnnotation, ma.member.isCurrentUser else { continue } if let view = rootView.mapView.view(for: ma) as? MemberAnnotationView { view.updateHeading(currentHeading) } } #endif } } #if !targetEnvironment(simulator) // MARK: - MAMapViewDelegate extension MapViewController: MAMapViewDelegate { func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { guard let memberAnnotation = annotation as? MemberAnnotation else { return nil } let identifier = "MemberAnnotation" var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? MemberAnnotationView if annotationView == nil { annotationView = MemberAnnotationView(annotation: memberAnnotation, reuseIdentifier: identifier) } else { annotationView?.annotation = memberAnnotation } annotationView?.configure(with: memberAnnotation.member) return annotationView } func mapView(_ mapView: MAMapView!, didSelect view: MAAnnotationView!) { guard let annotation = view.annotation as? MemberAnnotation else { return } viewModel.selectMember(annotation.member) } func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) { guard updatingLocation, let location = userLocation.location else { return } if rootView.mapView.userTrackingMode == .follow { rootView.mapView.setCenter(location.coordinate, animated: true) } } } #endif // MARK: - CLLocationManagerDelegate extension MapViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { let h = newHeading.trueHeading guard h >= 0 else { return } currentHeading = h updateCurrentUserHeading() } func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { if status == .authorizedWhenInUse || status == .authorizedAlways { #if !targetEnvironment(simulator) rootView.mapView.showsUserLocation = true rootView.mapView.userTrackingMode = .follow #endif } } }