224 lines
7.5 KiB
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
|
|
}
|
|
}
|
|
}
|