For a kind of podcast player I need to periodically update a swiftData object to keep track of the listening progress. (Happy to hear if there are better ways) I need to do this in many places in my app so I wanted to extract the modelContext into a Singleton so I can write a global function that starts the timer. In doing so I stumbled upon a problem: The memory used by my app is steadily increasing and the device is turning hot.
@Observable
class Helper {
static let shared = Helper()
var modelContext: ModelContext?
}
@main
struct SingletontestApp: App {
let modelContainer: ModelContainer
init() {
do {
modelContainer = try ModelContainer(
for: Item.self, Item.self
)
} catch {
fatalError("Could not initialize ModelContainer")
}
Helper.shared.modelContext = modelContainer.mainContext
}
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(modelContainer)
}
}
struct ContentView: View {
@Query private var items: [Item]
var body: some View {
NavigationSplitView {
List {
ForEach(items) { item in
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
ToolbarItem {
Button(action: updateItemPeriodically) {
Label("Change random", systemImage: "dice")
}
}
}
} detail: {
Text("Select an item")
}
}
func addItem() {
withAnimation {
let newItem = Item(timestamp: Date())
Helper.shared.modelContext!.insert(newItem)
}
}
@MainActor
func updateItemPeriodically() { // Doesn't matter if run as global or local func
let descriptor = FetchDescriptor<Item>(sortBy: [SortDescriptor(\.timestamp)])
let results = (try? Helper.shared.modelContext?.fetch(descriptor)) ?? []
let element = results.randomElement()
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { timer in // Smaller time intervals worsen the problem
element?.timestamp = Date.now
}
}
}
Calling save() manually or automatically in the timer does not have any effect. I am not sure about my general way of keeping track of listening process so if you think there is a better way, feel free to correct me.
Thanks for your help
SwiftData's default backing store (BackingData) is based on Core Data, which caches the fetched objects to achieve better performance. If Helper.shared.modelContext?.fetch
returns more objects over time, the cache will grow, which may explain why your app’s memory consumption increases.
ModelContext
doesn't provide an API to reset the cache. What I can suggest is that, instead of using the main context, try to create a new context (init(_:)) every time your timer is triggered. Since the context is released when your code exits the timer handler, the cache won’t grow.
The device getting hot is an indication that your code does too much. I am unclear why you need to fetch all the objects and set the time stamp for only a random one. If you can share your concrete use case and what you are trying to achieve, folks may be able to give you a better suggestion.