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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 17:09:22