Hmm… I believe I do now have an understanding how a State variable would tie the lifecycle of its value to the lifecycle of a View component… but I am still unclear how this helps me to understand how a State variable defined on an App component can be expected to behave:
https://developer.apple.com/documentation/swiftui/managing-user-interface-state
This article has many examples of using State in a View component… but I do not see examples of using State in our App component.
https://developer.apple.com/videos/play/wwdc2020/10040/?time=1852
This video does have an example of a StateObject being used as a global "source of truth" (which is going to be most similar to my use case)… but this video predates the release of State and Observable.
https://developer.apple.com/videos/play/wwdc2023/10149
I believe this video has a close example from FoodTruckModel to achieve a similar feature to my use case. What this video seems to be missing is where this FoodTruckModel should first be created. Similar to the previous talk… this "global source of truth" looks like it should belong in my root App component… but then would that imply that FoodTruckModel be better initialized as a State var or as a regular Swift let?
A side-question I would have here is how it would be possible for an engineer to stress-test their app in a way to cause the App component instance to be disposed by SwiftUI (and then rebuilt while the process is running). Is such a use case ever possible in an edge case? Is this something (recreating App component) that engineers should ever have to prepare for to defend against?
Post
Replies
Boosts
Views
Activity
This is a more detailed example that shows some extra work from my production app:
@main struct StepperDemoApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
@Observable final class Number {
var value: Int = 0
}
struct ContentView: View {
@State private var number = Number()
func onIncrement() {
number.value += 1
print(#function)
}
func onDecrement() {
number.value -= 1
print(#function)
}
var body: some View {
Stepper {
Text("Stepper \(number.value)")
} onIncrement: {
self.onIncrement()
} onDecrement: {
self.onDecrement()
}
.padding()
}
}
#Preview {
ContentView()
}
In this example, the Stepper text label value depends on state (the number class), but I see the same unexpected behavior (potential bug) from my original example. My Stepper text label is correctly updating when the Observable value changes… but the onIncrement and onDecrement closures are in some kind of "bad" state.
My original example showed how the Stepper behaves unexpectedly when no state is triggering a redraw. This new example behaves unexpectedly when state is triggering a redraw through an Observable object. Our previous example showed how Stepper behaves as expected when we trigger a redraw directly on state (without the indirection of an Observable object).
All these repros are from macOS 14.6.1… deploying to iPhone simulator behaves as expected on all examples (no unexpected behavior).
I've submitted a bug report myself and will see if I can find a workaround.
Thanks! I am actually kind of "unblocked" in a different way by building my own custom component:
@main struct StepperDemoApp: App {
func onIncrement() {
print(#function)
}
func onDecrement() {
print(#function)
}
var body: some Scene {
WindowGroup {
VStack {
Text("Stepper")
Button("Increment") {
self.onIncrement()
}
Button("Decrement") {
self.onDecrement()
}
}
.padding()
}
}
}
This unblocks my use case for my app… but I am still blocked from using the system Stepper component. If I did ship this custom component as a workaround it would be helpful for my repo to be able to explain to readers why this workaround is needed (because the production stepper component is potentially causing a bug on macOS).
could you explain what you're using this for that's not updating any UI
Thanks! The post here is an attempt at a MRE test case to show I am unable to see these two functions called how I expect on macOS (but iPhone simulator behaves as expected).
My production app does more complex work in the onIncrement and onDecrement functions and I am not seeing that work being called correctly. I seem to be running into the same problem in my production app I have here in the MRE test case… without an explicit state or id refreshing the stepper then the macOS stepper seems to get in some weird situation where the + and - buttons do not dispatch correctly until the next manual refresh computes a new body.
I advise filing a bug report at https://feedbackassistant.apple.com and mentioning it.
I actually haven't had very quick results from feedback assistant… the last bug I filed was Feb 12 and I still see no comments or any information from an Apple Engineer. This bug has been fixed but I did not see any comments or notifications for me to follow along with the progress in feedback assistant.
I'm actually blocked on this Stepper. :( If I used one of my DTS service request support tickets would you be available to help investigate to confirm if this is a legit bug in the framework? My last DTS request was Oct 27 (last year) and I still have heard no response back from anyone at Apple. Could I file a DTS and ask for you to be the engineer if you have time? Thanks!
Resetting the view id seems to lead to the correct values being printed on macOS:
import SwiftUI
@main struct StepperDemoApp: App {
@State var id = UUID()
func onIncrement() {
print(#function)
self.id = UUID()
}
func onDecrement() {
print(#function)
self.id = UUID()
}
var body: some Scene {
WindowGroup {
Stepper {
Text("Stepper")
} onIncrement: {
self.onIncrement()
} onDecrement: {
self.onDecrement()
}
.id(self.id)
}
}
}
Tapping + + + - - -:
onIncrement()
onIncrement()
onIncrement()
onDecrement()
onDecrement()
onDecrement()
This is a hack? Or a legit workaround for a known issue? Is some internal state in Stepper bad for some reason without this id hack?
This one also works:
var body: some Scene {
WindowGroup {
Stepper {
Text(self.id.uuidString)
} onIncrement: {
self.onIncrement()
} onDecrement: {
self.onDecrement()
}
}
}
Building the SPM plug in with disable-sandbox seems to work around the errors… but I'm still not clear why a model context specified as an in-memory context needs a valid URL on the system to begin work.
https://github.com/swiftlang/swift-package-manager/issues/6948
This might be another version of this error from SPM plug ins.
import Benchmark
import CoreData
let benchmarks = {
Benchmark("Benchmark") { benchmark in
let _ = NSPersistentContainer.defaultDirectoryURL
}
}
This crashes:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+[NSPersistentContainer defaultDirectoryURL] Could not conjure up a useful location for writing persistent stores.'
*** First throw call stack:
(
0 CoreFoundation 0x0000000189caa2ec __exceptionPreprocess + 176
1 libobjc.A.dylib 0x000000018978e158 objc_exception_throw + 60
2 CoreData 0x00000001904d9034 __44+[NSPersistentContainer defaultDirectoryURL]_block_invoke + 0
3 CoreData 0x00000001905c3f74 $sSo21NSPersistentContainerC8CoreDataE19defaultDirectoryURL10Foundation0G0VvgZ + 40
4 Benchmarks 0x00000001047530e4 $s10Benchmarks10benchmarks9BenchmarkACCSgycvpfiAEycfU_yADcfU_ + 92
5 Benchmarks 0x0000000104703608 $s9Benchmark0A8ExecutorV3runySayAA0A6ResultVGA2ACF + 2640
6 Benchmarks 0x0000000104728f34 $s9Benchmark0A6RunnerV3runyyYaKFTY0_ + 6768
7 Benchmarks 0x0000000104725cb5 $s9Benchmark0A11RunnerHooksPAAE4mainyyYaFZTQ1_ + 1
8 Benchmarks 0x00000001047535a5 $sIetH_yts5Error_pIegHrzo_TR10async_MainTf3npf_nTQ0_ + 1
9 libswift_Concurrency.dylib 0x00000002513d6149 _ZL22completeTaskAndReleasePN5swift12AsyncContextEPNS_10SwiftErrorE + 1
)
libc++abi: terminating due to uncaught exception of type NSException
CoreData: error: Failed to create directory file:///Users/rick/Library/Application%20Support/Benchmarks: NSCocoaErrorDomain (513)
CoreData: fault: Unhandled exception finding default Directory URL '+[NSPersistentContainer defaultDirectoryURL] Could not conjure up a useful location for writing persistent stores.'
https://github.com/vanvoorden/2024-08-26
I am running into these errors when attempting to run a benchmark on a SwiftData context from the 2024-08-26 repo. The errors seem to be harmless… the benchmark continues without crashing on those unhandled exceptions.
https://github.com/vanvoorden/2024-08-02/
I seem to have no problem running against SwiftData from a different Swift Package Executable (2024-08-02). This package seems to build and run with no errors. This package also creates the expected directory under ~/Library/Application%20Support/2024-08-02.
I don't yet completely understand what is happening… but there seems to be some reason why Benchmarks is failing to create that extra directory under Application%20Support.
I can also run Benchmarks with no errors after I add the Application%20Support/Benchmarks directory manually from mac Finder.
The benchmarks seem to be running correctly even after printing those errors… so I am not sure if there is anything important to fix in the Benchmarks package for now.
The reference of delete(model:where:includeSubclasses:) doesn't mention that behavior, and I believe that is because delete(model:where:includeSubclasses:) goes down directly to the store to delete the objects (for better performance), like what NSBatchDeleteRequest does, and doesn't discard the unsaved objects in the context.
Ahh… interesting! This is very valuable insight. Thanks! I am beginning to understand this behavior more.
A follow up question is I am still looking for one "single shot" function to delete all model instances (including staged and pending models prior to save). I can think of (at least) three options:
Explicitly save before attempting to delete all:
func testSaveAndDelete() throws {
let modelContext = ModelContext(container)
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 1)
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 2)
try modelContext.save()
try modelContext.delete(model: Item.self)
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 0)
}
Attempt to delete all and then follow up with an iteration through all remaining:
func testSaveAndDeleteAndIterate() throws {
let modelContext = ModelContext(container)
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 1)
try modelContext.save()
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 2)
try modelContext.delete(model: Item.self)
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 1)
for model in try modelContext.fetch(FetchDescriptor<Item>()) {
modelContext.delete(model)
}
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 0)
}
Iterate through all inserted models prior to delete all:
func testSaveAndIterateAndDelete() throws {
let modelContext = ModelContext(container)
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 1)
try modelContext.save()
modelContext.insert(Item())
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 2)
for model in modelContext.insertedModelsArray {
// TODO: filter only for `item` models! :D
modelContext.delete(model)
}
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 1)
try modelContext.delete(model: Item.self)
print(try modelContext.fetchCount(FetchDescriptor<Item>()) == 0)
}
All of these options seem to operate correctly and pass my tests. any opinion about which pattern might provide the right choice to optimize for memory and CPU? My intuition tells me the most efficient pattern might depend on the model schema and the state of this context at any point in time. Any more insight about what might be the right option here? Thanks!
If that doesn't help, I'd be interested in taking a closer look if you can provide a sample project to demo the issue.
https://github.com/vanvoorden/2024-08-02
Here is a repo to reproduce the behaviors. I am not seeing any change when I explicitly save my model context.
Did you try to save the model context after doing the deletion? When using a model context created with ModelContext(modelContainer), you need to save the changes explicitly because the auto-save doesn't come to play, which is different from when you use mainContext.
Hmm… I can give that a try. My question then would be is why do these two functions:
extension Store {
public func delete<T>(model: T.Type) throws where T : PersistentModel {
try self.modelContext.delete(model: model)
}
}
extension Store {
public func deleteWithIteration<T>(model: T.Type) throws where T : PersistentModel {
for model in try self.fetch(model) {
self.modelContext.delete(model)
}
}
}
Seem to show different behavior without an explicit save being called? Calling the delete function (with no explicit save) returns with no error thrown and no models have been deleted. Calling the deleteWithIteration function (with no explicit save) returns with no error thrown and all models have been deleted. Should those two functions not return with the same state (either both functions delete all models or both functions delete no models)?
I'm seeing similar behavior (ModelActor eagerly dispatching work to main) in Xcode_16_beta_3. Has anyone heard if this is supposed to be fixed… or should we plan to continue to ship with these workarounds going forward when the new OS is released?
With Xcode, you can use -com.apple.CoreData.ConcurrencyDebug 1 as a launch argument to do the check.
By any chance do we know if there are any known issues that might lead to false positive errors when using the ConcurrencyDebug argument with SwiftData? I am seeing some concurrency errors with a SwiftData stack… but I'm looking through how this stack is set up and I can't understand what could be leading to any kind of race condition.
Ahh… I don't know how to delete this comment and move it to a thread. Sorry about that!