SwiftUI报错:未找到PomodoroModel类型的ObservableObject
SwiftUI番茄钟应用环境对象错误排查与解决
开发SwiftUI番茄钟应用时遇到致命错误:
No ObservableObject of type PomodoroModel found. A View.environmentObject(_:) for PomodoroModel may be missing as an ancestor of this view
已在PomodoroTimerApp.swift中通过@StateObject初始化PomodoroModel,并为FocusView添加了.environmentObject(pomodoroModel);FocusView嵌套展示PomodoroHome,而PomodoroHome使用@EnvironmentObject获取PomodoroModel,但仍触发该错误。以下是相关代码及排查解决步骤:
相关代码
PomodoroHome.swift
import SwiftUI struct PomodoroHome: View { @EnvironmentObject var pomodoroModel: PomodoroModel var body: some View { VStack { Text("Focus Timer") .font(.title2.bold()) GeometryReader{proxy in VStack(spacing: 15){ // MARK: Timer Ring ZStack{ Circle() .fill(.whiteBlack.opacity(0.03)) .padding(-40) Circle() .trim(from: 0, to:pomodoroModel.progress) .stroke(.white.opacity(0.03),lineWidth: 80) .padding(-40) // MARK: Shadow Circle() .stroke(Color("whiteBlack"),lineWidth: 5) .blur(radius: 15) .padding(-2) Circle() .fill(Color("blackWhite")) Circle() .trim(from: 0, to: pomodoroModel.progress) .stroke(Color("whiteBlack").opacity(0.7),lineWidth: 10) // MARK: Knob GeometryReader{proxy in let size = proxy.size Circle() .fill(Color("whiteBlack")) .frame(width: 30, height: 30) .overlay(content: { Circle() .fill(.white) .padding(5) }) .frame(width: size.width, height: size.height, alignment: .center) // MARK: Since view is rotated thats why using X .offset(x: size.height / 2) .rotationEffect(.init(degrees: pomodoroModel.progress * 360)) } Text(pomodoroModel.timerStringValue) .font(.system(size: 45, weight: .light)) .rotationEffect(.init(degrees: -90)) .animation(.none, value: pomodoroModel.progress) } .padding(60) .frame(height: proxy.size.width) .rotationEffect(.init(degrees: -90)) .animation(.easeInOut, value: pomodoroModel.progress) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) Button { if pomodoroModel.isStarted{ pomodoroModel.stopTimer() }else{ pomodoroModel.addNewTimer = true } } label: { Image(systemName: !pomodoroModel.isStarted ? "timer" : "pause") .font(.largeTitle.bold()) .foregroundColor(.black) .frame(width: 80, height: 80) .background{ Circle() .fill(Color("whiteBlack").opacity(0.03)) } .shadow(color: Color("whiteBlack").opacity(0.6), radius: 8, x: 0, y: 0) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) } } .padding() // .background{ // Color("whiteBlack") // } //.preferredColorScheme(.dark) .overlay(content: { ZStack{ Color.black .opacity(pomodoroModel.addNewTimer ? 0.25 : 0) .onTapGesture { pomodoroModel.hour = 0 pomodoroModel.minutes = 0 pomodoroModel.seconds = 0 pomodoroModel.addNewTimer = false } NewTimerView() .frame(maxHeight: .infinity,alignment: .bottom) .offset(y: pomodoroModel.addNewTimer ? 0 : 400) } .animation(.easeInOut, value: pomodoroModel.addNewTimer) }) .onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in if pomodoroModel.isStarted{ pomodoroModel.updateTimer() } } } // MARK: New Timer Bottom Sheet @ViewBuilder func NewTimerView()->some View{ VStack(spacing: 15){ Text("Add New Timer") .font(.title2.bold()) .padding(.top,10) HStack(spacing: 15){ Text("\(pomodoroModel.hour) hr") .font(.title3) .fontWeight(.semibold) .foregroundColor(.black.opacity(0.3)) .padding(.horizontal,20) .padding(.vertical,12) .background{ Capsule() .fill(.whiteBlack.opacity(0.07)) } .contextMenu{ ContextMenuOptions(maxValue: 12, hint: "hr") { value in pomodoroModel.hour = value } } Text("\(pomodoroModel.minutes) min") .font(.title3) .fontWeight(.semibold) .foregroundColor(.black.opacity(0.3)) .padding(.horizontal,20) .padding(.vertical,12) .background{ Capsule() .fill(.whiteBlack.opacity(0.07)) } .contextMenu{ ContextMenuOptions(maxValue: 60, hint: "min") { value in pomodoroModel.minutes = value } } Text("\(pomodoroModel.seconds) sec") .font(.title3) .fontWeight(.semibold) .foregroundColor(.black.opacity(0.3)) .padding(.horizontal,20) .padding(.vertical,12) .background{ Capsule() .fill(.whiteBlack.opacity(0.07)) } .contextMenu{ ContextMenuOptions(maxValue: 60, hint: "sec") { value in pomodoroModel.seconds = value } } } .padding(.top,20) Button { pomodoroModel.startTimer() } label: { Text("Save") .font(.title3) .fontWeight(.semibold) .foregroundColor(.white) .frame(height:60) .padding(.horizontal,100) .background{ Capsule() .fill(Color("whiteBlack")) } } .disabled(pomodoroModel.seconds == 0) .opacity(pomodoroModel.seconds == 0 ? 0.5 : 1) .padding(.top) } .padding() .frame(maxWidth: .infinity) .background{ RoundedRectangle(cornerRadius: 10, style: .continuous) .fill(Color("blackWhite")) .ignoresSafeArea() } } // MARK: Reusable Context Menu Options @ViewBuilder func ContextMenuOptions(maxValue: Int,hint: String,onClick: @escaping (Int)->())->some View{ ForEach(0...maxValue,id: \.self){value in Button("\(value) \(hint)"){ onClick(value) } } } } struct Home_Previews: PreviewProvider { static var previews: some View { FocusView() .environmentObject(PomodoroModel()) } }
PomodoroModel.swift
import SwiftUI class PomodoroModel: NSObject,ObservableObject { // MARK: Timer Properties @Published var progress: CGFloat = 1 @Published var timerStringValue: String = "00:00" @Published var isStarted: Bool = false @Published var addNewTimer: Bool = false @Published var hour: Int = 0 @Published var minutes: Int = 0 @Published var seconds: Int = 0 // MARK: Total Seconds @Published var totalSeconds: Int = 0 @Published var staticTotalSeconds: Int = 0 // MARK: Starting Timer func startTimer(){ withAnimation(.easeInOut(duration: 0.25)){isStarted = true} // MARK: Setting String Time Value timerStringValue = "\(hour == 0 ? "" : "\(hour):")\(minutes >= 10 ? "\(minutes)":"0\(minutes)")\(seconds >= 10 ? "\(seconds)":"0\(seconds)")" // MARK: Calculating Total Seconds for Timer Animation totalSeconds = (hour * 3600) + (minutes) } // MARK: Stopping Timer func stopTimer(){ } // MARK: Updating Timer func updateTimer(){ } }
PomodoroTimerApp.swift
import SwiftUI struct PomodoroTimerApp: App { // MARK: Since we're doing background fetching initialising here @StateObject var pomodoroModel: PomodoroModel = .init() var body: some Scene { WindowGroup { FocusView() .environmentObject(pomodoroModel) } } }
FocusView.swift
import SwiftUI struct FocusView: View { @EnvironmentObject var pomodoroModel: PomodoroModel var body: some View { PomodoroHome() } } struct FocusView_Previews: PreviewProvider { static var previews: some View { FocusView() } } // MARK: this is ContentView.swift
排查与解决步骤
修复预览代码的环境对象注入
当前FocusView_Previews没有注入PomodoroModel,运行预览时必然触发错误。修改预览代码:struct FocusView_Previews: PreviewProvider { static var previews: some View { FocusView() .environmentObject(PomodoroModel()) } }确认运行时视图层级的环境对象传递
检查所有展示PomodoroHome的路径:- 确保只有通过
FocusView展示PomodoroHome,或者如果直接展示PomodoroHome,必须给其添加.environmentObject(pomodoroModel)。 PomodoroTimerApp中的初始化和注入逻辑是正确的,FocusView会将环境对象传递给子视图PomodoroHome,理论上运行时不会出错。
- 确保只有通过
清理缓存与重启Xcode
有时候Xcode的缓存会导致环境对象传递异常,执行Product > Clean Build Folder,然后重启Xcode,再重新运行项目。可选优化:移除FocusView中不必要的环境对象声明
FocusView只是作为中间视图传递PomodoroHome,不需要自己持有@EnvironmentObject,可以移除该声明,避免混淆:struct FocusView: View { var body: some View { PomodoroHome() } }
内容的提问来源于stack exchange,提问作者JChips
相关产品推荐
相关产品推荐

