282 lines
10 KiB
Swift
282 lines
10 KiB
Swift
//
|
||
// 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<GroupMemberListSectionModel> = {
|
||
return RxTableViewSectionedReloadDataSource<GroupMemberListSectionModel>(
|
||
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()
|
||
}
|
||
}
|