SwiftUI模态Sheet列表二次打开失效及生命周期异常问题求助
Hey there, let's break down what's happening with your code step by step — this is a common gotcha in early SwiftUI betas, especially with async operations and BindableObject lifecycle.
Why does DispatchQueue.main.async cause the issue?
When your AreaListView is first presented, the MockModel initializes and calls performFetch(), which schedules an async block on the main queue. This block runs, updates items, and the willChange subject notifies SwiftUI to refresh the view — that works fine the first time.
The problem hits when you dismiss the sheet:
- Even though the
AreaListViewis destroyed, theMockModelinstance isn't immediately deallocated (we'll cover why next). - When you present the sheet a second time, a new
MockModelis created, and itsperformFetch()schedules another async block. - Before this new block can run, the old
MockModel'sdeinitfinally triggers, which interferes with the new model's state updates. Plus, due to the AttributeGraph cycle issue, SwiftUI fails to pick up thewillChangeevent that should refresh the view with the new items.
Why is deinit triggered only when opening the sheet the second time?
This directly ties to the === AttributeGraph: cycle detected through attribute 18 === error you're seeing. In early SwiftUI Beta versions (like Beta 4), @ObjectBinding (the predecessor to @ObservedObject) had known cycle reference bugs between views and BindableObjects:
- The view holds a reference to the
MockModel, and theBindableObject'swillChangesubject creates a subscription back to the view's AttributeGraph. This creates a cycle that prevents the model from being deallocated when the view is dismissed. - When you present the sheet again, creating a new
MockModelbreaks the old cycle, allowing the original model to finally be deallocated (hence thedeinitprint at that point).
What's the fix?
Let's address both the async issue and lifecycle problems:
- Properly manage Combine subscriptions (and fix async loading)
Your Future approach is better, but you need to retain and cancel subscriptions to avoid cycles. Add aCancellableto hold the subscription, and cancel it indeinit:
class MockModel: BindableObject { struct MockItem: Identifiable { var id: String } var willChange = PassthroughSubject<Void, Never>() var items: [MockItem] = [] { willSet { self.willChange.send() } } private var cancellable: Cancellable? // Hold the subscription here init() {} // Don't call fetch in init yet func performFetch() { cancellable = getItem() .receive(on: RunLoop.main) .sink(receiveValue: { [weak self] items in self?.items = items }) } func getItem() -> Future<[MockItem], Never> { return Future { promise in // Simulate background work here if needed DispatchQueue.global().async { let t = [MockItem(id: "first"), MockItem(id: "fjirst"), MockItem(id: "fiklkrst"), MockItem(id: "fijlkrst")] promise(.success(t)) } } } deinit { cancellable?.cancel() // Cancel subscription to break cycles print("deinit") } }
- Switch to
@ObservedObject(if available in Beta 4)
Later SwiftUI betas renamed@ObjectBindingto@ObservedObject, which fixed many lifecycle and cycle issues. Even in Beta 4, switching might resolve the AttributeGraph error:
struct AreaListView: View { @ObservedObject var model = MockModel() // Replace @ObjectBinding with @ObservedObject var body: some View { NavigationView { List { ForEach(model.items) { item in Text(item.id) } } } .onAppear { self.model.performFetch() // Trigger fetch when view is fully loaded } } }
- Trigger fetch when the view appears
CallingperformFetch()ininitcan lead to timing issues. Instead, use.onAppearto start the fetch only after the view is connected to SwiftUI's view hierarchy, ensuring updates are properly detected.
Key Takeaways
- Early SwiftUI betas had rough edges with
BindableObjectlifecycle and async operations. Always cancel subscriptions to avoid memory cycles. DispatchQueue.main.asyncin the model's init creates timing conflicts with view updates, especially when combined with delayed deallocation from cycles.- Using Combine's
Futurewith proper subscription management is a more reliable way to handle async data loading in SwiftUI.
内容的提问来源于stack exchange,提问作者Congruent Tech. UG

