SwiftUI开发节拍器时无法通过按钮中途停止while循环
问题根源
你的代码里metronome()函数直接在主线程执行,while循环+sleep(1)会完全阻塞主线程。iOS的UI渲染、点击事件响应全靠主线程处理,主线程被占死的时候,所有按钮点击、界面更新都会被挂起,直到循环执行结束才会响应,这时候你点停止按钮的操作根本没法在循环运行时被处理,自然停不下来。
另外你停止按钮的逻辑也有问题:点了之后还会再调用一次metronome(),就算不阻塞也会多跑一轮循环。
修复方案
别用sleep阻塞主线程,用Timer做定时触发,主线程只负责UI响应,计时交给系统调度,随时可以取消定时器停止节拍。
修正后的完整代码:
import SwiftUI import AVFoundation var player: AVAudioPlayer! struct ContentView: View { // 持有定时器实例,方便随时停止 @State private var metronomeTimer: Timer? @State private var bpm: Double = 60 // 默认60拍/分钟,对应1秒1拍 @State private var isRunning = false // 记录运行状态,避免重复创建定时器 var body: some View { ZStack { Color.white .ignoresSafeArea() VStack(spacing: 100){ VStack(spacing: 0){ Image(systemName: "play.circle") .foregroundColor(Color.green) .font(.title) Button{ startMetronome() } label: { Text("Start").font(.largeTitle).bold() } .padding([.leading, .bottom, .trailing], 20.0) .foregroundColor(Color.green) .disabled(isRunning) // 运行时禁用开始按钮,避免重复触发 } VStack(spacing: 0){ Image(systemName: "pause.circle") .foregroundColor(Color.red) .font(.title) Button{ stopMetronome() } label: { Text("Stop").font(.largeTitle).bold() } .padding([.leading, .bottom, .trailing], 20.0) .foregroundColor(Color.red) .disabled(!isRunning) // 未运行时禁用停止按钮 } .padding(.top, 20.0) } } } // 启动节拍器 func startMetronome() { // 先停掉之前可能存在的定时器 stopMetronome() isRunning = true // 按BPM计算拍间隔,不阻塞主线程 let timeInterval = 60.0 / bpm metronomeTimer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true) { _ in playSound() } // 启动后立刻响第一拍 playSound() } // 停止节拍器 func stopMetronome() { // 作废定时器立刻终止循环 metronomeTimer?.invalidate() metronomeTimer = nil isRunning = false } // 播放节拍音 func playSound() { guard let url = Bundle.main.url(forResource: "metronome", withExtension: "m4a") else { return } do { player = try AVAudioPlayer(contentsOf: url) player?.play() } catch{ print("音频播放错误: \(error)") } } }
关键改动点
- 移除了阻塞主线程的
while循环+sleep逻辑,改用系统提供的Timer做定时调度,不会占用主线程资源,UI随时可以响应点击 - 单独持有定时器实例,停止时直接调用
invalidate()作废定时器,立刻终止节拍,不需要靠修改循环变量判断 - 增加运行状态标记,避免重复点击启动按钮创建多个定时器,同时在对应状态下禁用不需要的按钮
- 修正了原来停止按钮额外调用
metronome()的逻辑错误,停止操作只需要终止定时器即可 - 把原来写死的1秒间隔改成根据
bpm计算,后续可以直接加个滑块控件调整BPM值,动态修改定时器间隔就行
注意:如果你需要做专业级高精度节拍器,
Timer会受主线程调度影响有微小误差,可以换用DispatchSourceTimer挂载到高优先级队列,或者用AVAudioEngine的音频时序逻辑实现,但核心原则不变:永远不要阻塞主线程。
内容的提问来源于stack exchange,提问作者Metron
相关产品推荐
相关产品推荐

