389 lines
13 KiB
Swift
389 lines
13 KiB
Swift
//
|
|
// PigeonMessageVC.swift
|
|
// QuickLocation
|
|
//
|
|
|
|
import UIKit
|
|
import AVFoundation
|
|
import CoreLocation
|
|
import SwiftyUserDefaults
|
|
import RxSwift
|
|
|
|
final class PigeonMessageVC: BaseViewController {
|
|
|
|
private var rootView: PigeonMessageView!
|
|
private var members: [GroupMemberModel] = []
|
|
private var selectedMember: GroupMemberModel?
|
|
private var selectedImage: UIImage?
|
|
|
|
private var audioRecorder: AVAudioRecorder?
|
|
private var audioPlayer: AVAudioPlayer?
|
|
private var recordURL: URL?
|
|
private var recordTimer: Timer?
|
|
private var playbackTimer: Timer?
|
|
private var recordedDuration: TimeInterval = 0
|
|
private var isPressingRecord = false
|
|
|
|
override func loadView() {
|
|
rootView = PigeonMessageView(frame: UIScreen.main.bounds)
|
|
view = rootView
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
bindActions()
|
|
setupRecordingGesture()
|
|
loadMembers()
|
|
}
|
|
|
|
override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
guard isMovingFromParent || navigationController?.isBeingDismissed == true else { return }
|
|
cleanupAudio(deleteFile: true)
|
|
}
|
|
|
|
private func bindActions() {
|
|
rootView.historyButton.addTarget(self, action: #selector(showHistory), for: .touchUpInside)
|
|
rootView.changeRecipientButton.addTarget(
|
|
self,
|
|
action: #selector(changeRecipient),
|
|
for: .touchUpInside
|
|
)
|
|
rootView.addImageButton.addTarget(self, action: #selector(selectImage), for: .touchUpInside)
|
|
rootView.deleteImageButton.addTarget(self, action: #selector(deleteImage), for: .touchUpInside)
|
|
rootView.playPauseButton.addTarget(self, action: #selector(togglePlayback), for: .touchUpInside)
|
|
rootView.sendButton.addTarget(self, action: #selector(sendMessage), for: .touchUpInside)
|
|
rootView.onModeChanged = { [weak self] _ in
|
|
self?.stopPlayback()
|
|
if self?.audioRecorder?.isRecording == true {
|
|
self?.stopRecording(cancel: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setupRecordingGesture() {
|
|
let gesture = UILongPressGestureRecognizer(
|
|
target: self,
|
|
action: #selector(handleRecordGesture(_:))
|
|
)
|
|
gesture.minimumPressDuration = 0.15
|
|
rootView.recordButton.addGestureRecognizer(gesture)
|
|
}
|
|
|
|
private func loadMembers() {
|
|
DLToast.showLoading()
|
|
GroupService.groupInfo().subscribe(onNext: { [weak self] response in
|
|
DLToast.dismiss()
|
|
guard let self, let model = response.model else { return }
|
|
self.members = model.select_group_employee
|
|
self.selectedMember = self.members.first
|
|
self.refreshRecipient()
|
|
}).disposed(by: disposeBag)
|
|
}
|
|
|
|
private func refreshRecipient() {
|
|
rootView.configureRecipient(
|
|
selectedMember,
|
|
distanceText: distanceText(for: selectedMember)
|
|
)
|
|
}
|
|
|
|
private func distanceText(for member: GroupMemberModel?) -> String {
|
|
guard let member,
|
|
let latitude = Defaults[\.currentLatitude],
|
|
let longitude = Defaults[\.currentLongitude] else {
|
|
return "与您相距 --"
|
|
}
|
|
let parts = member.last_position.components(separatedBy: ":")
|
|
guard parts.count >= 2,
|
|
let memberLatitude = Double(parts[0]),
|
|
let memberLongitude = Double(parts[1]) else {
|
|
return "与您相距 --"
|
|
}
|
|
let current = CLLocation(latitude: latitude, longitude: longitude)
|
|
let target = CLLocation(latitude: memberLatitude, longitude: memberLongitude)
|
|
let kilometers = current.distance(from: target) / 1000
|
|
if kilometers < 1 {
|
|
return "与您相距 \(Int(kilometers * 1000))m"
|
|
}
|
|
return String(format: "与您相距 %.1fkm", kilometers)
|
|
}
|
|
|
|
@objc private func goBack() {
|
|
navigationController?.popViewController(animated: true)
|
|
}
|
|
|
|
@objc private func showHistory() {
|
|
let historyVC = PigeonMessageHistoryVC(members: members)
|
|
navigationController?.pushViewController(historyVC, animated: true)
|
|
}
|
|
|
|
@objc private func changeRecipient() {
|
|
guard !members.isEmpty else {
|
|
DLToast.show(text: "暂无可选收件人")
|
|
return
|
|
}
|
|
PigeonRecipientPickerView.show(
|
|
members: members,
|
|
selectedId: selectedMember?.user_id ?? ""
|
|
) { [weak self] member in
|
|
self?.selectedMember = member
|
|
self?.refreshRecipient()
|
|
}
|
|
}
|
|
|
|
@objc private func selectImage() {
|
|
let picker = PhotoPickerVC(maxCount: 1)
|
|
picker.onConfirm = { [weak self] images in
|
|
guard let image = images.first else { return }
|
|
self?.selectedImage = image
|
|
self?.rootView.setSelectedImage(image)
|
|
}
|
|
present(picker, animated: true)
|
|
}
|
|
|
|
@objc private func deleteImage() {
|
|
selectedImage = nil
|
|
rootView.setSelectedImage(nil)
|
|
}
|
|
|
|
@objc private func handleRecordGesture(_ gesture: UILongPressGestureRecognizer) {
|
|
switch gesture.state {
|
|
case .began:
|
|
isPressingRecord = true
|
|
requestMicrophoneAndStart()
|
|
case .ended:
|
|
isPressingRecord = false
|
|
if audioRecorder?.isRecording == true {
|
|
stopRecording(cancel: false)
|
|
}
|
|
case .cancelled, .failed:
|
|
isPressingRecord = false
|
|
stopRecording(cancel: true)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func requestMicrophoneAndStart() {
|
|
guard let manager = AuthorizeManager.manager(type: .microphone) else { return }
|
|
switch manager.authorizeStatus() {
|
|
case .authorized:
|
|
if isPressingRecord {
|
|
startRecording()
|
|
}
|
|
case .notDetermined:
|
|
manager.authorize { [weak self] status in
|
|
guard let self else { return }
|
|
if status == .authorized, self.isPressingRecord {
|
|
self.startRecording()
|
|
} else if status != .authorized {
|
|
self.showMicrophoneDenied()
|
|
}
|
|
}
|
|
case .restricted, .denied:
|
|
showMicrophoneDenied()
|
|
}
|
|
}
|
|
|
|
private func showMicrophoneDenied() {
|
|
Permission.openAppSetting(
|
|
title: "无法使用麦克风",
|
|
message: "请在系统设置中允许访问麦克风"
|
|
)
|
|
}
|
|
|
|
private func startRecording() {
|
|
stopPlayback()
|
|
if let recordURL {
|
|
try? FileManager.default.removeItem(at: recordURL)
|
|
}
|
|
|
|
do {
|
|
let session = AVAudioSession.sharedInstance()
|
|
try session.setCategory(
|
|
.playAndRecord,
|
|
mode: .default,
|
|
options: [.defaultToSpeaker, .allowBluetoothHFP]
|
|
)
|
|
try session.setActive(true)
|
|
|
|
let url = FileManager.default.temporaryDirectory
|
|
.appendingPathComponent("pigeon_\(UUID().uuidString).wav")
|
|
let settings: [String: Any] = [
|
|
AVFormatIDKey: kAudioFormatLinearPCM,
|
|
AVSampleRateKey: 8000,
|
|
AVNumberOfChannelsKey: 1,
|
|
AVLinearPCMBitDepthKey: 16,
|
|
AVLinearPCMIsFloatKey: false
|
|
]
|
|
let recorder = try AVAudioRecorder(url: url, settings: settings)
|
|
recorder.isMeteringEnabled = true
|
|
recorder.prepareToRecord()
|
|
recorder.record(forDuration: 60)
|
|
|
|
recordURL = url
|
|
audioRecorder = recorder
|
|
recordedDuration = 0
|
|
rootView.clearVoiceDraft()
|
|
rootView.setRecording(isRecording: true)
|
|
rootView.setVoiceDuration(0, hasRecording: false)
|
|
startRecordTimer()
|
|
} catch {
|
|
DLToast.show(text: "录音启动失败")
|
|
stopRecording(cancel: true)
|
|
}
|
|
}
|
|
|
|
private func startRecordTimer() {
|
|
recordTimer?.invalidate()
|
|
let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in
|
|
guard let self, let recorder = self.audioRecorder else { return }
|
|
recorder.updateMeters()
|
|
let level = pow(10, CGFloat(recorder.averagePower(forChannel: 0)) / 38)
|
|
self.rootView.pushMeterLevel(level)
|
|
self.rootView.setVoiceDuration(recorder.currentTime, hasRecording: false)
|
|
if recorder.currentTime >= 59.95 {
|
|
self.isPressingRecord = false
|
|
self.stopRecording(cancel: false)
|
|
}
|
|
}
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
recordTimer = timer
|
|
}
|
|
|
|
private func stopRecording(cancel: Bool) {
|
|
recordTimer?.invalidate()
|
|
recordTimer = nil
|
|
|
|
let duration = audioRecorder?.currentTime ?? 0
|
|
audioRecorder?.stop()
|
|
audioRecorder = nil
|
|
rootView.setRecording(isRecording: false)
|
|
try? AVAudioSession.sharedInstance().setActive(false)
|
|
|
|
guard !cancel, duration >= 1, recordURL != nil else {
|
|
if !cancel, duration > 0 {
|
|
DLToast.show(text: "录音时间不能少于1秒")
|
|
}
|
|
if let recordURL {
|
|
try? FileManager.default.removeItem(at: recordURL)
|
|
}
|
|
self.recordURL = nil
|
|
recordedDuration = 0
|
|
rootView.clearVoiceDraft()
|
|
return
|
|
}
|
|
|
|
recordedDuration = min(60, duration)
|
|
rootView.setVoiceDuration(recordedDuration, hasRecording: true)
|
|
rootView.setPlaying(false)
|
|
}
|
|
|
|
@objc private func togglePlayback() {
|
|
guard let recordURL else { return }
|
|
if let player = audioPlayer {
|
|
if player.isPlaying {
|
|
player.pause()
|
|
rootView.setPlaying(false)
|
|
playbackTimer?.invalidate()
|
|
playbackTimer = nil
|
|
} else {
|
|
player.play()
|
|
rootView.setPlaying(true)
|
|
startPlaybackTimer()
|
|
}
|
|
return
|
|
}
|
|
|
|
do {
|
|
let player = try AVAudioPlayer(contentsOf: recordURL)
|
|
player.delegate = self
|
|
player.isMeteringEnabled = true
|
|
player.prepareToPlay()
|
|
player.play()
|
|
audioPlayer = player
|
|
rootView.setPlaying(true)
|
|
startPlaybackTimer()
|
|
} catch {
|
|
DLToast.show(text: "语音播放失败")
|
|
}
|
|
}
|
|
|
|
private func startPlaybackTimer() {
|
|
playbackTimer?.invalidate()
|
|
let timer = Timer(timeInterval: 0.05, repeats: true) { [weak self] _ in
|
|
guard let self, let player = self.audioPlayer else { return }
|
|
player.updateMeters()
|
|
let level = pow(10, CGFloat(player.averagePower(forChannel: 0)) / 38)
|
|
self.rootView.pushMeterLevel(level)
|
|
self.rootView.setVoiceDuration(player.currentTime, hasRecording: true)
|
|
}
|
|
RunLoop.main.add(timer, forMode: .common)
|
|
playbackTimer = timer
|
|
}
|
|
|
|
private func stopPlayback() {
|
|
playbackTimer?.invalidate()
|
|
playbackTimer = nil
|
|
audioPlayer?.stop()
|
|
audioPlayer = nil
|
|
rootView?.setPlaying(false)
|
|
if recordedDuration > 0 {
|
|
rootView?.setVoiceDuration(recordedDuration, hasRecording: true)
|
|
}
|
|
}
|
|
|
|
@objc private func sendMessage() {
|
|
guard selectedMember != nil else {
|
|
DLToast.show(text: "请先选择收件人")
|
|
return
|
|
}
|
|
|
|
switch rootView.mode {
|
|
case .image:
|
|
guard selectedImage != nil else {
|
|
DLToast.show(text: "请先选择一张图片")
|
|
return
|
|
}
|
|
selectedImage = nil
|
|
rootView.clearImageDraft()
|
|
case .voice:
|
|
guard recordURL != nil, recordedDuration >= 1 else {
|
|
DLToast.show(text: "请先完成录音")
|
|
return
|
|
}
|
|
cleanupAudio(deleteFile: true)
|
|
rootView.clearVoiceDraft()
|
|
}
|
|
DLToast.show(text: "发送成功")
|
|
}
|
|
|
|
private func cleanupAudio(deleteFile: Bool) {
|
|
recordTimer?.invalidate()
|
|
playbackTimer?.invalidate()
|
|
recordTimer = nil
|
|
playbackTimer = nil
|
|
audioRecorder?.stop()
|
|
audioRecorder = nil
|
|
audioPlayer?.stop()
|
|
audioPlayer = nil
|
|
if deleteFile, let recordURL {
|
|
try? FileManager.default.removeItem(at: recordURL)
|
|
self.recordURL = nil
|
|
}
|
|
recordedDuration = 0
|
|
try? AVAudioSession.sharedInstance().setActive(false)
|
|
}
|
|
}
|
|
|
|
extension PigeonMessageVC: AVAudioPlayerDelegate {
|
|
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
|
|
playbackTimer?.invalidate()
|
|
playbackTimer = nil
|
|
audioPlayer = nil
|
|
rootView.setPlaying(false)
|
|
rootView.setVoiceDuration(recordedDuration, hasRecording: recordedDuration >= 1)
|
|
}
|
|
}
|