jsdw_ios/QuickLocation/Section/Map/MapViewController.swift

224 lines
7.5 KiB
Swift

//
// 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
}
}
}