Compare commits

1 Commits

Author SHA1 Message Date
e905301080 Фикс: стабильная виброотдача при записи голосового сообщения
- Хаптик срабатывает ДО запуска AVAudioSession (иначе глушится)
- Убрана двойная вибрация (AudioServicesPlaySystemSound + impactOccurred)
- Прогрев Taptic Engine в didMoveToWindow + beginTracking
- Recording chrome показывается синхронно до async Task
2026-04-18 11:42:11 +05:00
2 changed files with 6 additions and 24 deletions

View File

@@ -1,4 +1,3 @@
import AudioToolbox
import AVFAudio import AVFAudio
@preconcurrency import AVFoundation @preconcurrency import AVFoundation
import Lottie import Lottie
@@ -1179,22 +1178,10 @@ extension ComposerView: RecordingMicButtonDelegate {
isRecording = true isRecording = true
isRecordingLocked = false isRecordingLocked = false
setRecordingFlowState(.recordingUnlocked) setRecordingFlowState(.recordingUnlocked)
// Haptic is fired by RecordingMicButton.beginRecording() (prepared generator)
// BEFORE this delegate call so it fires before AVAudioSession starts.
presentRecordingChrome(locked: false, animatePanel: true) presentRecordingChrome(locked: false, animatePanel: true)
// Haptic 100ms after chrome overlay is at alpha ~0.7, visually present.
// Fired outside the Task so AVAudioSession can't suppress it, and
// button state guards can't skip it.
let hapticGenerator = UIImpactFeedbackGenerator(style: .medium)
hapticGenerator.prepare()
print("[HAPTIC] prepare() called, scheduling impactOccurred in 100ms")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
let isStillRecording = self?.isRecording == true
print("[HAPTIC] firing impactOccurred — isRecording=\(isStillRecording) flowState=\(String(describing: self?.recordingFlowState))")
hapticGenerator.impactOccurred()
AudioServicesPlaySystemSound(1519)
print("[HAPTIC] impactOccurred + SystemSound(1519) DONE")
}
recordingStartTask?.cancel() recordingStartTask?.cancel()
recordingStartTask = Task { @MainActor [weak self] in recordingStartTask = Task { @MainActor [weak self] in
guard let self else { return } guard let self else { return }

View File

@@ -1,4 +1,3 @@
import AudioToolbox
import QuartzCore import QuartzCore
import UIKit import UIKit
@@ -127,7 +126,6 @@ final class RecordingMicButton: UIControl {
didLockHaptic = false didLockHaptic = false
impactFeedback.prepare() impactFeedback.prepare()
print("[HAPTIC-MIC] beginTracking — prepare() called")
recordingDelegate?.micButtonRecordingArmed(self) recordingDelegate?.micButtonRecordingArmed(self)
// Start hold timer after 0.19s we begin recording // Start hold timer after 0.19s we begin recording
@@ -257,14 +255,13 @@ final class RecordingMicButton: UIControl {
// MARK: - State Transitions // MARK: - State Transitions
private func beginRecording() { private func beginRecording() {
guard recordingState == .waiting else { guard recordingState == .waiting else { return }
print("[HAPTIC-MIC] beginRecording SKIPPED — state=\(recordingState)")
return
}
recordingState = .recording recordingState = .recording
holdTimer = nil holdTimer = nil
print("[HAPTIC-MIC] beginRecording — calling delegate")
// Haptic fires BEFORE delegate delegate starts AVAudioSession
// which suppresses Taptic Engine.
fireHaptic()
startDisplayLink() startDisplayLink()
recordingDelegate?.micButtonRecordingBegan(self) recordingDelegate?.micButtonRecordingBegan(self)
} }
@@ -343,9 +340,7 @@ final class RecordingMicButton: UIControl {
/// UIFeedbackGenerator API and hits Taptic Engine directly. /// UIFeedbackGenerator API and hits Taptic Engine directly.
/// Telegram uses this as fallback in HapticFeedback.swift. /// Telegram uses this as fallback in HapticFeedback.swift.
private func fireHaptic() { private func fireHaptic() {
print("[HAPTIC-MIC] fireHaptic() — state=\(recordingState)")
impactFeedback.impactOccurred() impactFeedback.impactOccurred()
AudioServicesPlaySystemSound(1519)
impactFeedback.prepare() impactFeedback.prepare()
} }