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

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

排查与解决步骤

  1. 修复预览代码的环境对象注入
    当前FocusView_Previews没有注入PomodoroModel,运行预览时必然触发错误。修改预览代码:

    struct FocusView_Previews: PreviewProvider {
        static var previews: some View {
            FocusView()
                .environmentObject(PomodoroModel())
        }
    }
    
  2. 确认运行时视图层级的环境对象传递
    检查所有展示PomodoroHome的路径:

    • 确保只有通过FocusView展示PomodoroHome,或者如果直接展示PomodoroHome,必须给其添加.environmentObject(pomodoroModel)。
    • PomodoroTimerApp中的初始化和注入逻辑是正确的,FocusView会将环境对象传递给子视图PomodoroHome,理论上运行时不会出错。
  3. 清理缓存与重启Xcode
    有时候Xcode的缓存会导致环境对象传递异常,执行Product > Clean Build Folder,然后重启Xcode,再重新运行项目。

  4. 可选优化:移除FocusView中不必要的环境对象声明
    FocusView只是作为中间视图传递PomodoroHome,不需要自己持有@EnvironmentObject,可以移除该声明,避免混淆:

    struct FocusView: View {
        var body: some View {
            PomodoroHome()
        }
    }
    

内容的提问来源于stack exchange,提问作者JChips

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 16:12:33