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

iOS SwiftUI中App进后台时Sheet丢失状态的问题问询

iOS 17.6/iOS 18 Beta 中SwiftUI Sheet后台返回丢失状态的问题分析与临时修复

问题现象

在iOS 17.6和iOS 18 Beta版本中,当App进入后台再返回前台时,.sheet()内的View会丢失@State状态(示例中计数器重置为0)。相关代码如下:

import SwiftUI

class ExampleModel: ObservableObject {
    static var shared = ExampleModel()

    @Published var inProgress: Bool = false

    init() {
        heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.runHeartbeat()
        }
    }

    var heartbeatTimer: Timer?

    private func runHeartbeat() {
        Task { @MainActor in
            print("Heartbeat")
            self.inProgress.toggle()
        }
    }
}

struct MainView: View {
    enum MainTab {
        case exampleTab
    }

    @ObservedObject var model: ExampleModel = .shared
    @State var showSheet = false
    @State var currentTab = MainTab.exampleTab

    var body: some View {
        let _ = Self._printChanges()

        TabView(selection: $currentTab) {
            NavigationView {
                VStack {
                    Form {
                        Button("Show Sheet") { self.showSheet.toggle() }
                            .sheet(
                                isPresented: self.$showSheet,
                                content: {
                                    CounterView()
                                }
                            )
                    }
                }
            }
            .tabItem { Label("Example", systemImage: "scribble") }
            .tag(MainView.MainTab.exampleTab)
        }
    }
}

struct CounterView: View {
    @State var counter = 0
    var body: some View {
        let _ = Self._printChanges()

        Button("Value \(counter)") { self.counter += 1 }
    }
}

复现步骤

  • 打开App,点击「Show Sheet」按钮打开弹窗
  • 多次点击计数器按钮修改数值
  • 将App切换到后台,再返回前台
  • 观察到计数器重置为0,Sheet丢失原有状态

原因分析

该Bug的触发依赖几个关键条件:

  • 视图层级组合:TabView + NavigationView + Form + .sheet() 的嵌套结构
  • 后台期间的状态更新:ExampleModel通过Timer持续更新@Published属性,触发父视图重绘

SwiftUI的View身份(identity)由其在视图树中的位置、绑定状态等因素决定。当App在后台时,ObservableObject的持续更新会触发父视图(如Form、NavigationView)重绘,导致Sheet的视图身份被意外重置——此时Sheet内容会被重新初始化,原有的@State状态自然丢失。通过_printChanges()可以确认,只有Sheet内的View身份发生了重置,说明父视图的重绘破坏了Sheet视图树的稳定性。

临时修复方案

1. 将Sheet移到更高层级视图

把.sheet修饰符从Form内的Button移到TabView或NavigationView等更上层的视图,避免Form的重绘影响Sheet的身份:

struct MainView: View {
    // ... 原有代码省略 ...
    var body: some View {
        let _ = Self._printChanges()
        
        TabView(selection: $currentTab) {
            NavigationView {
                VStack {
                    Form {
                        Button("Show Sheet") { self.showSheet.toggle() }
                    }
                }
            }
            .tabItem { Label("Example", systemImage: "scribble") }
            .tag(MainView.MainTab.exampleTab)
        }
        // 将Sheet移到TabView层级
        .sheet(isPresented: self.$showSheet) {
            CounterView()
        }
    }
}

2. 用@StateObject托管Sheet状态

将Sheet内的@State状态迁移到ObservableObject中,用@StateObject持有——即使Sheet视图被重建,状态也会被保留:

class CounterModel: ObservableObject {
    @Published var counter = 0
}

struct CounterView: View {
    @StateObject private var model = CounterModel()
    var body: some View {
        let _ = Self._printChanges()
        Button("Value \(model.counter)") { model.counter += 1 }
    }
}

3. 后台暂停Timer更新

在App进入后台时暂停ExampleModel的Timer,前台恢复,避免后台期间ObservableObject持续触发父视图重绘:

class ExampleModel: ObservableObject {
    static var shared = ExampleModel()
    @Published var inProgress: Bool = false
    var heartbeatTimer: Timer?
    
    init() {
        setupLifecycleListeners()
        startTimer()
    }
    
    private func setupLifecycleListeners() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appDidEnterBackground),
            name: UIApplication.didEnterBackgroundNotification,
            object: nil
        )
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(appWillEnterForeground),
            name: UIApplication.willEnterForegroundNotification,
            object: nil
        )
    }
    
    @objc private func appDidEnterBackground() {
        heartbeatTimer?.invalidate()
        heartbeatTimer = nil
    }
    
    @objc private func appWillEnterForeground() {
        startTimer()
    }
    
    private func startTimer() {
        heartbeatTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
            self?.runHeartbeat()
        }
    }
    
    private func runHeartbeat() {
        Task { @MainActor in
            print("Heartbeat")
            self?.inProgress.toggle()
        }
    }
}

4. 使用sheet(item:)替代isPresented

通过绑定可选类型的item控制Sheet显示,这种方式的视图身份管理更稳定,减少重绘导致的状态丢失:

struct MainView: View {
    enum MainTab {
        case exampleTab
    }
    
    @ObservedObject var model: ExampleModel = .shared
    @State var sheetTrigger: Bool? = nil // 用可选类型替代Bool
    @State var currentTab = MainTab.exampleTab
    
    var body: some View {
        let _ = Self._printChanges()
        
        TabView(selection: $currentTab) {
            NavigationView {
                VStack {
                    Form {
                        Button("Show Sheet") { sheetTrigger = true }
                    }
                }
            }
            .tabItem { Label("Example", systemImage: "scribble") }
            .tag(MainView.MainTab.exampleTab)
        }
        .sheet(item: $sheetTrigger) { _ in
            CounterView()
        }
    }
}

// 为Bool实现Identifiable协议
extension Bool: Identifiable {
    public var id: Self { self }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 18:55:11