Swift变量变更时如何自动更新View?实战场景咨询
问题:SwiftUI视图无法在数据变更时自动执行状态逻辑
问题背景
我有一个Flutter应用会定期发送带数据的消息,在SwiftUI的ViewModel中已实现数据解码逻辑,ContentView里用@State声明ViewModel,通过NavigationLink把viewModel的变量传给ChallengeTabView。目前收到新消息时视图能同步更新currentLives这类值,但根据StateType枚举编写的switch逻辑,放在.onAppear或.task里只有重新进入页面才触发,希望让它每次currentChallenge变更时自动执行,同时想明确数据模型用类还是结构体更合适。
相关代码
ChallengeTabView 代码
struct ChallengeTabView: View { let currentChallenge: CoreChallengeData @State private var timer: Timer? @State private var counter = 0 @State private var isRunning = false @State private var showCancelAlert = false private var editableButtons: Bool { return currentChallenge.state.type != .DelayedChallengeState || currentChallenge.state.type != .NoActiveChallengeState } private var challengeIsActive: Bool { currentChallenge.state.type != .NoActiveChallengeState } @State private var message = "No Message" @State private var conter = 0 // 注:存在拼写错误,应为counter var body: some View { NavigationStack{ Text(message) TabView{ if challengeIsActive{ VStack(alignment: .leading){ Text("\(DateUtilities.formatSecondsToString(duration: currentChallenge.state.data.duration - counter))") .font(.largeTitle) HStack{ ForEach(0..<currentChallenge.state.data.maxLives, id: \.self){ life in if life > currentChallenge.state.data.currentLives - 1{ Image(systemName: "heart.slash") .resizable() .scaledToFit() .foregroundStyle(.tertiary) } else{ Image(systemName: "heart.fill") .resizable() .scaledToFit() .foregroundStyle(.red) } } } .frame(height: 30) } } else { Text("Keine Aktive Challenge") } } .navigationTitle("Challenge") .navigationBarTitleDisplayMode(.inline) } } } #Preview { ChallengeTabView(currentChallenge: CoreChallengeData( event: Event(type: "InitChallengeEvent"), state: CurrentState( data: ChallengeInformation( currentLives: 1, duration: 15, maxLives: 2, reward: 0, since: "2024-06-09T12:00:00" ), type: .PausedChallengeState ) )) }
数据结构体定义
enum StateType: String, Codable{ case NoActiveChallengeState, DelayedChallengeState, PausedChallengeState, ActiveChallengeState, BelowThresholdState } struct CoreChallengeData: Codable{ let event: Event let state: CurrentState } struct CurrentState: Codable { let data: ChallengeInformation let type: StateType } struct Event: Codable { let type: String } struct ChallengeInformation: Codable { let currentLives: Int let duration: Int let maxLives: Int let reward: Int let since: String }
ContentView 代码
struct ContentView: View { @State private var viewModel: WatchViewModel = WatchViewModel() @State private var vibrates = false @State private var showBatterySheet = false var body: some View { NavigationStack{ NavigationLink("Challenge View"){ ChallengeTabView(currentChallenge: viewModel.challengeMessage) } ScoreTabView(liveScores: viewModel.liveScores) .accessibilityLabel("Start") .onAppear{ //viewModel.sendDataMessage(for: .syncRequest) } .sheet(isPresented: $showBatterySheet) { VStack{ Text("Batterie:") Text("99%") .font(.title) .foregroundStyle(.green) } } } } }
解决方案
1. 让switch逻辑在currentChallenge变更时自动执行
.onAppear和.task仅在视图首次渲染时触发,要监听currentChallenge的变化,需使用**.onChange(of:)修饰符**,它会在指定变量的值发生变更时自动执行闭包内的逻辑。
修改ChallengeTabView的body部分,添加该修饰符:
var body: some View { NavigationStack{ // 原有视图代码保持不变 } .onChange(of: currentChallenge) { newValue in // 在这里编写你的switch逻辑 switch newValue.state.type { case .ActiveChallengeState: // 处理活跃挑战逻辑:启动计时器 isRunning = true timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in counter += 1 if counter >= newValue.state.data.duration { timer?.invalidate() isRunning = false } } case .PausedChallengeState: // 处理暂停逻辑:停止计时器 timer?.invalidate() isRunning = false case .NoActiveChallengeState: // 处理无活跃挑战逻辑:重置状态 timer?.invalidate() counter = 0 isRunning = false case .DelayedChallengeState: // 处理延迟挑战逻辑 timer?.invalidate() isRunning = false case .BelowThresholdState: // 处理低于阈值逻辑 timer?.invalidate() isRunning = false } } }
注:因为CoreChallengeData是结构体(值类型),只要其内部任何成员变化,整个值会被视为新值,.onChange会自动触发。
2. 结构体 vs 类的选择
- 结构体:适合你的数据模型(
CoreChallengeData、CurrentState等)。原因:- 值类型天生不可变(你的结构体属性均为
let),SwiftUI对值类型的变化检测更直接,值更新时依赖该值的视图会自动刷新; - 实现
Codable更简单,无需额外处理引用关系; - 线程安全,不存在引用计数问题。
- 值类型天生不可变(你的结构体属性均为
- 类:适合ViewModel(比如
WatchViewModel)。原因:- 引用类型支持持有可变状态、处理业务逻辑(如消息解码、发送);
- 多个视图可共享同一个实例,配合
@StateObject(初始化时创建)或@ObservedObject(外部传入)+@Published属性,能让视图自动响应状态变化。
额外修正:ContentView中用@State声明ViewModel是错误的,类类型应使用@StateObject来保证生命周期:
struct ContentView: View { @StateObject private var viewModel: WatchViewModel = WatchViewModel() // 替换@State为@StateObject // 其他代码保持不变 }
内容的提问来源于stack exchange,提问作者registrier rp
相关产品推荐
相关产品推荐

