You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS能否同时生成触觉反馈并录制其声音?异步振动失效问题排查

解决iOS振动与录音同时运行时振动失效的问题

问题原因分析

  • Timer运行环境异常:后台队列的RunLoop默认未启动,你在后台队列中创建的Timer无法按时触发playVibration方法,导致振动实际未执行。
  • 音频会话配置缺失:未正确设置AVAudioSession类别,录音启动时可能占用音频资源干扰振动;同时未申请录音权限,可能引发录音初始化异常间接影响振动功能。
  • 振动实现方式可靠性不足:使用AudioServicesPlaySystemSound重复播放系统振动音效,这种方式并非为持续振动场景设计,且与音频录制的兼容性较差。

修复方案

1. 修正Timer的RunLoop配置

将振动Timer放到主队列的RunLoop中(主队列RunLoop默认持续运行),确保Timer能正常触发:

func startVibration() {
    print("Vibration starts")
    DispatchQueue.main.async {
        self.vibrationTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.playVibration), userInfo: nil, repeats: true)
    }
}

若坚持使用后台队列,需手动启动RunLoop(注意此操作会阻塞当前线程):

func startVibration() {
    print("Vibration starts")
    let timer = Timer(timeInterval: 0.1, target: self, selector: #selector(playVibration), userInfo: nil, repeats: true)
    RunLoop.current.add(timer, forMode: .common)
    self.vibrationTimer = timer
    RunLoop.current.run()
}

2. 配置音频会话并申请权限

在viewDidLoad中完成录音权限申请和音频会话配置,确保录音与振动可同时运行:

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 申请录音权限
    AVAudioSession.sharedInstance().requestRecordPermission { granted in
        if !granted {
            print("录音权限未授权")
        }
    }
    
    // 配置音频会话为播放+录制模式
    do {
        try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
        try AVAudioSession.sharedInstance().setActive(true)
    } catch {
        print("音频会话配置失败:\(error.localizedDescription)")
    }
}

3. 使用Core Haptics实现稳定持续振动

Core Haptics是苹果官方推荐的触觉反馈框架,比重复播放系统音效更稳定,且与音频操作兼容性更好:
首先导入框架:

import CoreHaptics

添加相关属性与方法:

var hapticEngine: CHHapticEngine?
var hapticPlayer: CHHapticPatternPlayer?

func startContinuousVibration() {
    guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
        print("设备不支持Core Haptics")
        return
    }
    
    do {
        hapticEngine = try CHHapticEngine()
        try hapticEngine?.start()
        
        // 创建3秒持续振动的触觉模式
        let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
        let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
        let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: 0, duration: 3)
        
        let pattern = try CHHapticPattern(events: [event], parameters: [])
        hapticPlayer = try hapticEngine?.makePlayer(with: pattern)
        
        try hapticPlayer?.start(atTime: CHHapticTimeImmediate)
        print("Core Haptics振动启动")
        
        // 3秒后同步停止振动与录音
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            self.hapticPlayer?.stop(atTime: CHHapticTimeImmediate)
            self.stopRecording()
            print("Core Haptics振动停止")
        }
        
    } catch {
        print("Core Haptics操作失败:\(error.localizedDescription)")
    }
}

4. 补充录音停止逻辑

在振动结束时同步停止录音,完善流程:

func stopRecording() {
    audioRecorder?.stop()
    print("Recording stopped")
    // 此处可调用音频分析方法
    // analyzeRecordedVibration(audioURL: audioRecorder?.url ?? getDocumentsDirectory())
}

完整修复代码示例

import UIKit
import AVFoundation
import CoreHaptics

class ViewController: UIViewController, AVAudioRecorderDelegate {
    
    var audioRecorder: AVAudioRecorder?
    var hapticEngine: CHHapticEngine?
    var hapticPlayer: CHHapticPatternPlayer?
  
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 申请录音权限
        AVAudioSession.sharedInstance().requestRecordPermission { granted in
            if !granted {
                print("录音权限未授权")
            }
        }
        
        // 配置音频会话
        do {
            try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print("音频会话配置失败:\(error.localizedDescription)")
        }
    }
    
    @IBAction func testButtonPressed(_ sender: UIButton) {
        playVibrationAndRecord()
    }
    
    func playVibrationAndRecord() {
        DispatchQueue.global(qos: .background).async {
            self.startContinuousVibration()
        }
        
        DispatchQueue.global(qos: .background).async {
            self.startRecording()
        }
    }
    
    func startContinuousVibration() {
        guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
            print("设备不支持Core Haptics")
            return
        }
        
        do {
            hapticEngine = try CHHapticEngine()
            try hapticEngine?.start()
            
            let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0)
            let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5)
            let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness], relativeTime: 0, duration: 3)
            
            let pattern = try CHHapticPattern(events: [event], parameters: [])
            hapticPlayer = try hapticEngine?.makePlayer(with: pattern)
            
            try hapticPlayer?.start(atTime: CHHapticTimeImmediate)
            print("Core Haptics振动启动")
            
            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                self.hapticPlayer?.stop(atTime: CHHapticTimeImmediate)
                self.stopRecording()
                print("Core Haptics振动停止")
            }
            
        } catch {
            print("Core Haptics操作失败:\(error.localizedDescription)")
        }
    }
    
    func startRecording() {
        print("Recording started")
        let audioFilename = getDocumentsDirectory().appendingPathComponent("recording.m4a")
        
        let settings = [
            AVFormatIDKey: Int(kAudioFormatMPEG4AAC),
            AVSampleRateKey: 44100,
            AVNumberOfChannelsKey: 1,
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]
        
        do {
            audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
            audioRecorder?.delegate = self
            audioRecorder?.record()
        } catch {
            print("Error recording audio: \(error.localizedDescription)")
        }
    }
    
    func stopRecording() {
        audioRecorder?.stop()
        print("Recording stopped")
    }
    
    func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
    
    func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
        if flag {
            print("录音完成")
        } else {
            print("录音失败")
        }
    }
}

内容的提问来源于stack exchange,提问作者Cyclone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 09:26:01