SwiftData on iOS 18 extreme memory use

Hi,

I've run into two problems using SwiftData in iOS 18 that are total show-stoppers for my app, causing it to run out of memory and be killed by the system. The same app runs fine on iOS 17.

The two problems are inter-related in a way I can't exactly diagnose, but are easy to reproduce.

Problem #1: Calling .count on the array that represents a relationship or Query causes the whole array of objects to be loaded into memory.

Problem #2: When a @Model object is loaded, properties that are declared with .externalStorage are loaded unnecessarily, and use tons of memory.

It's possible that #1 is normal behavior, exacerbated by #2.

I've written a test app that demonstrates the extreme difference in memory usage between the OS Versions. It uses a typical navigation pattern, with content counts on the left-side view. Each item has one thumbnail and one large image in .externalStorge. GitHub Source

When populated with 80 items, each containing one thumbnail and one large image in .externalStorge, the app launches in 17.5 using 29mb of memory. On iOS 18, in the same conditions, 592 mb.

When the first folder is selected, causing a list of thumbnails to load, iOS 17 uses just 86mb. iOS 18 uses 599mb, implying that all image data has already been loaded.

So I'm asking for help from Apple or the Community in finding a workaround. I've been advised that finding a workaround may be necessary, as this may not be addressed in 18.0.

Thanks in advance for any insight.

Radars: FB14323833, FB14323934

(See attached images, or try it yourself)

(You may notice in the 18.0 screenshots that the item counts don't add up right. That's yet another 18.0-SwiftData anomaly regarding relationships that I haven't tackled yet, but is also demonstrated by the sample app.)

Answered by DTS Engineer in 799501022

I am unaware of how the team would address the issue. Also, we can't comment the future plan, as you might have known.

I think the major problem here is that the app loads the full image data when it doesn't need to, which consumes a lot of memory, and making the image data attribute a relationship is the way to avoid that. This has been a best practice in Core Data, as shown in the model of the Sharing Core Data objects between iCloud users sample. (Note that the sample goes even further to model thumbnail as a relationship.)

The default data store in SwiftData is based on Core Data, and so I'd suggest that folks follow the best practice. I understand that changing the model is pain though :-(.

Using fetchCount(_:) is another best practice. Yet if the full image data isn't loaded when you load an item, and your data set isn't quite large, counting the query result may not be an obvious issue for you.

The result returned by fetchCount(_:) is not updated when the context is changed, unless you do another fetch. It will be up to you to review your code if that impacts your SwiftUI views.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Related report by developer Gil here... https://developer.apple.com/forums/thread/759345

Yeah, what you described points to a SwiftData memory management issue on iOS 18. Thanks for filing the feedback reports.

Before the issue is fixed on the framework side, I am trying to explore a workaround. Did you even try the following, or can the following, if working, be an option in your case?

  1. Replacing the image data attribute (Item.image) with a relationship, which hopefully avoids loading the whole image when querying items.

  2. Using the other way, like fetchCount(_:), to retrieve the count of items.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Thanks Ziqiao, for these great ideas! I've tried both, separately, in my test project....

Replacing the data property with a relationship does indeed avoid loading the images until needed. A good workaround. I'll test it in my real project to see what happens with more complexity and scale.

Using fetchCount on a FetchDesciptor instead of count on SwiftData's Query array also improves things. It does not load the entire dataset to make the count.

So thanks for these workarounds. Used together they may allow a SwiftData app to run normally on iOS 18.

Now just thinking out loud, for myself and for fellow developers facing the same issues, strategizing about iOS 18 (and other fall 2024 os's)....

Changing the image properties to relationships has these costs and risks....

  • I'll probably have to write a migrator, as this change seems unlikely to be handled automatically by lightweight migration.
  • Will swift data reliably manage and discard the related items? Any different behavior from being properties?

Changing the queries to FetchDesciptor has these costs and risks....

  • In my large app, I use the convenience of counting SwiftData's pseudo arrays all over the place. Not just in Queries, but in relationships. How much work will it be to inject FetchDesciptors everywhere? Any performance difference?
  • Any unforeseen interactions or failures with SwiftUI views?

And the big question, do I just wait a few more Betas and hope that SwiftData's iOS-17-like behavior is restored? Is the old-way I was working closer to SwiftData best practices? When do I give up and make these changes?

Accepted Answer

I am unaware of how the team would address the issue. Also, we can't comment the future plan, as you might have known.

I think the major problem here is that the app loads the full image data when it doesn't need to, which consumes a lot of memory, and making the image data attribute a relationship is the way to avoid that. This has been a best practice in Core Data, as shown in the model of the Sharing Core Data objects between iCloud users sample. (Note that the sample goes even further to model thumbnail as a relationship.)

The default data store in SwiftData is based on Core Data, and so I'd suggest that folks follow the best practice. I understand that changing the model is pain though :-(.

Using fetchCount(_:) is another best practice. Yet if the full image data isn't loaded when you load an item, and your data set isn't quite large, counting the query result may not be an obvious issue for you.

The result returned by fetchCount(_:) is not updated when the context is changed, unless you do another fetch. It will be up to you to review your code if that impacts your SwiftUI views.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Ziqiao, thanks for the fetchCount workaround. Unfortunately I've found another case. Simply iterating over a @Query with ForEach causes the same problem. No call to count.

Your suggestion to make large items in externalStorage reference instead does help in this case too. But it points to a serious problem in SwiftData, where Query macros become useless in non-trivial apps.

(I'm pretty sure that the problem does not happen when iterating or counting an array representing a one-to-many relationship. Hard to be sure because I can't get the app running well enough to exercise in iOS 18)

I do hope that it's fixable, because this is not how SwiftData is supposed to work, and everything works fine on iOS 17.

Still not fixed in iOS18 Beta 7. I'm starting to be really anxious as final version of iOS 18 is approaching really fast and I don't know how to fix this in my app. This is a breaking change from iOS 17 ;(

I am concerned whether this issue will be resolved before the official release of iOS 18. It has a significant impact on the app's performance. If there are any alternative solutions, I would appreciate any suggestions. (I hope that, in the end, it will deliver performance similar to iOS 17.)

@Gil and @max_us I just gave up on SwiftData and reverted to Core Data. A pretty painful few days on a large app, but in the end it runs great on the fall OS's, and I have the convenience of FetchedResultsController for non-view code.

I have a gut feeling this won't be fixed in the fall releases.

@jhokit ok thanks for the insight on this.

I'm also considering reverting to Core Data. At least it seems less dangerous than making a Swift Data/cloudkit data model migration with the risk of loosing user data ;(

If this is not fixed for beta 8, I guess I will do the same as you although it's going to be a lot of work...

Still not fixed in beta 8 ;(

And not a single word about swift data in the release notes...

@jhokit thank you for this sample project, it was extremely helpful. I worked around SwiftData's Query memory issue using this workaround: https://gist.github.com/juanarzola/dca3c96f1122cb9710e443e40d75b01b

The problem is not just when counting, any query will load all the objects. It's a terrible regression, I don't know why it won't get fixed.

it's not perfect and just wrote it so it might not have the best naming. Thankfully "willSave" now works so we can write code like this.

SwiftData on iOS 18 extreme memory use
 
 
Q