// // HomeViewController.swift // QuickLocation // // Created by 八条 on 2026/5/27. // import UIKit import RxSwift import RxCocoa import RxDataSources import CoreLocation #if !targetEnvironment(simulator) import MAMapKit #endif class HomeViewController: BaseViewController { override var isNavigationBarHidden: Bool { true } override var preferredStatusBarStyle: UIStatusBarStyle { .default } // MARK: - Properties fileprivate var rootView: HomeView! private var tableView: UITableView { rootView.groupMemberView.tableView } private var viewModel = HomeViewModel() private let locationManager = CLLocationManager() private var currentHeading: Double = 0 private var members: [CircleMember] = [] private var currentUserMember: CircleMember? private var currentUserAnnotation: MemberAnnotation? override func loadView() { #if !targetEnvironment(simulator) MAMapView.updatePrivacyAgree(.didAgree) MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain) #endif rootView = HomeView(frame: UIScreen.main.bounds) view = rootView } override func viewDidLoad() { super.viewDidLoad() bindViewModel() setupMap() setupHeading() reactiveAction() requestUserInfo() requestGroupInfo() } private func reactiveAction() { rootView.groupView.rx.tapGesture.subscribe { _ in guard let groupModel = self.viewModel.groupModel else { return } let groupViewFrame = self.view.convert(self.rootView.groupView.frame, from: self.rootView) let startPointY = groupViewFrame.origin.y + groupViewFrame.height GroupListPopView.show(start: CGPoint(x: 0, y: startPointY + 20), groupModel: groupModel) { groupKey in guard let key = groupKey else { return } self.requestOperateGroup(groupKey: key) } }.disposed(by: disposeBag) rootView.groupMemberView.refreshBtn.rx.tap.subscribe(onNext: { _ in self.requestGroupInfo() }).disposed(by: disposeBag) rootView.groupMemberView.inviteJoinBtn.rx.tap.subscribe(onNext: { _ in AppRouter.push(Route.inviteJoin, userInfo: ["groupInfo": self.viewModel.groupInfo]) }).disposed(by: disposeBag) rootView.locationView.rx.tapGesture.subscribe { _ in if let ann = self.currentUserAnnotation { self.rootView.mapView.setCenter(ann.coordinate, animated: true) } }.disposed(by: disposeBag) NotificationCenter.default.rx.notification(.RefreshGroupInfoNotification, object: nil) .subscribe { [weak self] notification in self?.requestGroupInfo() }.disposed(by: disposeBag) } private func bindViewModel() { viewModel.output.sectionedItems .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) tableView.rx.modelSelected(GroupMemberModel.self) .subscribe(viewModel.cellAction.inputs) .disposed(by: disposeBag) } // MARK: - UITableViewDataSource lazy private var dataSource: RxTableViewSectionedReloadDataSource = { return RxTableViewSectionedReloadDataSource( configureCell: { (_, tableView, indexPath, model) in let cell: GroupMemberCell = tableView.dequeueReusableCell(for: indexPath) cell.configure(model: model, isCurrentUser: self.viewModel.isCurrentUser(id: model.user_id), isOwn: self.viewModel.isGroupOwn(id: model.user_id)) return cell }) }() // MARK: - API private func requestUserInfo() { UserService.userInfo().subscribe { response in guard let model = response.model else { return } AppContextManager.shared.saveAccount(model) self.rootView.avatarImgView.image = model.userIcon }.disposed(by: disposeBag) } private func requestGroupInfo() { GroupService.groupInfo().subscribe { response in guard let model = response.model else { return } self.viewModel.groupModel = model self.rootView.groupMemberView.setupCountData(self.viewModel.memberCount, self.viewModel.memberOnlineCount) self.rootView.groupNameLab.text = self.viewModel.groupName self.syncMemberAnnotations(model.select_group_employee) }.disposed(by: disposeBag) } private func requestOperateGroup(groupKey: String) { GroupService.operate(opType: "setdefault", requestData: ["group_key" : groupKey]).subscribe { response in self.requestGroupInfo() }.disposed(by: disposeBag) } // MARK: - Map Setup private func setupMap() { #if !targetEnvironment(simulator) rootView.mapView.delegate = self let r = MAUserLocationRepresentation() r.showsAccuracyRing = false r.showsHeadingIndicator = false r.enablePulseAnnimation = false r.lineWidth = 0 r.image = transparentImage() rootView.mapView.update(r) rootView.mapView.showsUserLocation = true rootView.mapView.userTrackingMode = .none #endif } private func transparentImage() -> UIImage { UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0) let img = UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() UIGraphicsEndImageContext() return img } // MARK: - Heading (CLLocationManager) private func setupHeading() { locationManager.delegate = self locationManager.startUpdatingHeading() } private func updateCurrentUserHeading() { #if !targetEnvironment(simulator) guard let ann = currentUserAnnotation, let view = rootView.mapView.view(for: ann) as? MemberAnnotationView else { return } view.updateHeading(currentHeading) #endif } // MARK: - Map Annotations from API data private func syncMemberAnnotations(_ list: [GroupMemberModel]) { #if !targetEnvironment(simulator) let isGroupOwner: (String) -> Bool = { [weak self] id in self?.viewModel.isGroupOwn(id: id) ?? false } let currentUserId = AppContextManager.shared.userId // 当前用户始终由 GPS 定位,单独注入 let me = CircleMember( id: "current", name: AppContextManager.shared.name, avatar: AppContextManager.shared.account?.head_pic ?? "1", isOnline: true, isOwner: false, coordinate: kCLLocationCoordinate2DInvalid, address: "", heading: 0, lastUpdateText: "在线" ) // 其他成员从 select_group_employee 取,过滤掉当前用户 var others: [CircleMember] = [] for model in list where model.user_id != currentUserId || currentUserId.isEmpty { let m = CircleMember(member: model, isOwner: isGroupOwner(model.user_id)) guard CLLocationCoordinate2DIsValid(m.coordinate) else { continue } others.append(m) } let newMembers = others + [me] members = newMembers currentUserMember = me let mapView = rootView.mapView let existing = mapView.annotations?.compactMap { $0 as? MemberAnnotation } ?? [] let existingIDs = Set(existing.map { $0.member.id }) let newIDs = Set(newMembers.map { $0.id }) let toRemove = existing.filter { !newIDs.contains($0.member.id) } mapView.removeAnnotations(toRemove) let toAdd = newMembers.filter { !existingIDs.contains($0.id) } let annotations = toAdd.map { MemberAnnotation(member: $0) } mapView.addAnnotations(annotations) #endif } } #if !targetEnvironment(simulator) // MARK: - MAMapViewDelegate extension HomeViewController: MAMapViewDelegate { func mapView(_ mapView: MAMapView!, viewFor annotation: MAAnnotation!) -> MAAnnotationView! { if annotation is MAUserLocation { return nil } 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) if memberAnnotation.member.isCurrentUser { currentUserAnnotation = memberAnnotation annotationView?.updateHeading(currentHeading) } return annotationView } func mapView(_ mapView: MAMapView!, didUpdate userLocation: MAUserLocation!, updatingLocation: Bool) { guard updatingLocation, let location = userLocation.location else { return } let coordinate = location.coordinate guard CLLocationCoordinate2DIsValid(coordinate) else { return } if let ann = currentUserAnnotation { ann.coordinate = coordinate } mapView.setCenter(coordinate, animated: true) mapView.setUserTrackingMode(.follow, animated: true) } func mapViewRequireLocationAuth(_ locationManager: CLLocationManager!) { locationManager.requestAlwaysAuthorization() } } #endif // MARK: - CLLocationManagerDelegate (heading only) extension HomeViewController: CLLocationManagerDelegate { func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) { let h = newHeading.trueHeading guard h >= 0 else { return } currentHeading = h updateCurrentUserHeading() } }