SwiftUI中变量变更为何一个View更新另一个不更新?
问题分析与解决方案
核心问题原因
你的代码存在两个关键问题,导致窗口内的文本标签不更新:
1. 状态修改未在主线程执行
Timer.scheduledTimer的回调默认运行在当前RunLoop的线程,虽然通常是主线程,但存在RunLoop模式不匹配的可能(比如窗口失去焦点时),导致修改@State变量的操作无法触发SwiftUI的视图更新。SwiftUI要求所有状态变更必须在主线程执行才能正确刷新视图。
2. 两个独立的ContentView实例
你在AppDelegate中创建了一个全新的ContentView()实例用于菜单栏,而窗口中的是另一个完全独立的实例。两者的@State变量是各自隔离的——点击窗口按钮修改的是窗口实例的timeGone,不会影响菜单栏的实例。你提到菜单栏标签能更新,大概率是测试场景的偏差,本质上两个实例的状态完全无关。
分步解决方案
步骤1:修复主线程状态更新问题
修改startTimer方法,强制在主线程更新状态变量:
func startTimer(){ timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {_ in DispatchQueue.main.async { self.timeGone += 1 } }) }
步骤2:实现跨视图状态共享
如果需要窗口和菜单栏共享同一个计时状态,必须使用ObservableObject创建共享状态模型,替代仅适用于单个视图的@State:
1. 创建共享计时器模型
import SwiftUI import Combine class TimerModel: ObservableObject { @Published var timeGone = 0 private var timer: Timer? func startTimer() { timer?.invalidate() // 避免重复创建计时器 timeGone = 0 // 重置计时 timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in DispatchQueue.main.async { self?.timeGone += 1 } } } func stopTimer() { timer?.invalidate() timer = nil } }
2. 修改ContentView使用共享模型
将@State替换为@ObservedObject,接收外部传递的共享模型:
struct ContentView: View { @ObservedObject var timerModel: TimerModel var MenuBar: some View { VStack{ Text(String(timerModel.timeGone)) .font(.system(size: 12)) .foregroundColor(.white) .padding(.vertical, 1) .padding(.horizontal, 4) .overlay( RoundedRectangle(cornerRadius: 5.0) .stroke(Color.white, lineWidth: 1.2) ) } .padding(.vertical, 2) .padding(.horizontal, 2) .background(Color.clear) } var body: some View { VStack { Text(String(timerModel.timeGone)) .fontWeight(.semibold) .font(.system(size: 90)) Spacer().frame(height: 30) Button("Start") { timerModel.startTimer() } .buttonStyle(StartButtonStyle()) } .fixedSize() .frame(width: 340.0, height: 200.0) .padding(.all, 50.0) } }
3. 修改AppDelegate传递共享模型
创建全局共享的模型实例,同时传递给窗口和菜单栏的ContentView:
class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem! private let timerModel = TimerModel() // 共享的模型实例 private lazy var contentView: NSView? = { guard let window = statusItem.value(forKey: "window") as? NSWindow else { return nil } return window.contentView }() func applicationDidFinishLaunching(_ notification: Notification) { setupMenuBar() // 创建并显示主窗口,传递共享模型 let hostingController = NSHostingController(rootView: ContentView(timerModel: timerModel)) let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 440, height: 300), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false ) window.contentViewController = hostingController window.center() window.makeKeyAndOrderFront(nil) } func setupMenuBar(){ statusItem = NSStatusBar.system.statusItem(withLength: 45) guard let contentView = self.contentView, let menuButton = statusItem.button else {return} let hostingView = NSHostingView(rootView: ContentView(timerModel: timerModel).MenuBar) hostingView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(hostingView) NSLayoutConstraint.activate([ hostingView.topAnchor.constraint(equalTo: contentView.topAnchor), hostingView.rightAnchor.constraint(equalTo: contentView.rightAnchor), hostingView.leftAnchor.constraint(equalTo: contentView.leftAnchor), hostingView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) ]) } }
额外注意事项
@State仅适用于单个视图内部的状态管理,跨视图共享状态必须使用ObservableObject配合@ObservedObject/@StateObject。- 计时器使用完毕后要调用
invalidate()避免内存泄漏,比如视图消失时或重新启动计时器前。 - 所有修改SwiftUI状态的操作必须确保在主线程执行,否则无法触发视图更新。
内容的提问来源于stack exchange,提问作者Edi Bociu
相关产品推荐
相关产品推荐

