Another Dev and I are building a SwiftUI app that utilizes Realm + MongoDB Atlas device sync.
We are experiencing some oddly slow page load times when we have a simple Vstack that contains maybe 20 items. With a very small amount of items the lag is barely noticeable but once we get higher it breaks down.
We had to stop using the SwiftUI wrapper / helper classes that Realm has available for a number of reasons, they seem to have not been fully thought through and don’t come with good examples. Removing these helped some of the issues.
It seems like SwiftUI probably doesn’t play nice with the way Realm is designed, the views are re-rendered a lot more than you would think and I’m currently assuming Realm is re-initializing data every time it’s called to re-render the view but I can’t confirm that.
Has anyone else had issues like this? Are there any tricks that you can share or some big fix that I haven’t found yet?
Does it work to convert realm objects into structs and wrap/unwrap them instead of calling the live objects every time?
Generally speaking, Realm is pretty darn fast; objects are stored locally and synced in the background so network lag isn’t an issue.
In our experience, performance issues are usually caused by bad code or incorrect implementation. Given the examples in the documentation could be better, they really do demonstrate the core of working with Realm.
I feel the question could be better addressed if you could provide a minimal example of the performance issues you’re experiencing. Keeping in mind a forum is not a good troubleshooting platform for long sections of code, if you could include some brief sample code that demonstrates those issues, maybe we’ll spot something.
I will continue to troubleshoot and will update this post here with what I discover.
But, I mostly posted the topic here in case anyone from the community has experience with SwiftUI + Realm and if they know of any common gotchas / tips / tricks for using the two together. I’m pretty sure someone else has run into similar behavior.
I don’t think there is anything wrong with Realm here, but there might be issues with SwiftUI not playing nice with it.
Understood. We use Realm, Swift & SwiftUI on macOS daily and don’t have any significant performance issues. That being said, our implementation could be way different than yours and I am sure our use cases are different as well.
I can tell you that in some cases, if you’re copying Realm objects to an array, it will hinder performance. But if you’re not doing that then it doesn’t apply.
Are you using @Observed objects? How is that implemented into a VStack? Did you try LazyVStack to see if there was any difference?
Seeing some code would be useful to track down the issue so if you have time, share some.
A few updates. I’m relaying some of the info from another dev on the team, sorry if I don’t explain some of the details well.
We are using @ObservedResults / @ObservedRealmObject property wrapper, but what we found is that even though realm freezes these (they are immutable results that get replaced from updates), but the binding still returns new memory references every time the UI requests data.
So the new memory references seem to cause SwiftUI to have a lot of extra needless re-renders. No idea why the freeze/immutable behavior was designed like this but I can see this behavior not playing nice with reactive frameworks like SwiftUI. The way this should work with the frozen/immutable results is that you get the same memory reference with observed/data bindings unless something changed, that would likely totally fix the problem.
The extra needless re-renders appear to cause our slow loading performance issues, but I can imagine a lot of projects use a very simple implementation of VStack/LazyVStacks that might not have as much of a negative impact. Ours is a bit more on the complicated side.
We decided to test converting all fetched data to structs to side step the issue with constantly new memory references causing re-renders. We set it up in a new test app to verify and so far it appears to have completely removed the needless re-renders that we were seeing.
We haven’t fully finished trouble shooting this but I wanted to add a quick update.
If others aren’t experiencing this issue with Realm + SwiftUI all I can think is that their use cases in SwiftUI for the realm data must be very simple compared to ours.
You actually don’t really need any example project, as the behavior is in the Realm SwiftUI quickstart with some minor modifications just to display that it’s happening.
All I’ve modified here is adding a simple counter state to ItemsView and some debug printing that shows when/why things rerender.
import RealmSwift
import SwiftUI
// MARK: Models
/// Random adjectives for more interesting demo item names
let randomAdjectives = [
"fluffy", "classy", "bumpy", "bizarre", "wiggly", "quick", "sudden",
"acoustic", "smiling", "dispensable", "foreign", "shaky", "purple", "keen",
"aberrant", "disastrous", "vague", "squealing", "ad hoc", "sweet"
]
/// Random noun for more interesting demo item names
let randomNouns = [
"floor", "monitor", "hair tie", "puddle", "hair brush", "bread",
"cinder block", "glass", "ring", "twister", "coasters", "fridge",
"toe ring", "bracelet", "cabinet", "nail file", "plate", "lace",
"cork", "mouse pad"
]
/// An individual item. Part of an `ItemGroup`.
final class Item: Object, ObjectKeyIdentifiable {
/// The unique ID of the Item. `primaryKey: true` declares the
/// _id member as the primary key to the realm.
@Persisted(primaryKey: true) var _id: ObjectId
/// The name of the Item, By default, a random name is generated.
@Persisted var name = "\(randomAdjectives.randomElement()!) \(randomNouns.randomElement()!)"
/// A flag indicating whether the user "favorited" the item.
@Persisted var isFavorite = false
/// Users can enter a description, which is an empty string by default
@Persisted var itemDescription = ""
/// The backlink to the `ItemGroup` this item is a part of.
@Persisted(originProperty: "items") var group: LinkingObjects<ItemGroup>
}
/// Represents a collection of items.
final class ItemGroup: Object, ObjectKeyIdentifiable {
/// The unique ID of the ItemGroup. `primaryKey: true` declares the
/// _id member as the primary key to the realm.
@Persisted(primaryKey: true) var _id: ObjectId
/// The collection of Items in this group.
@Persisted var items = RealmSwift.List<Item>()
}
extension Item {
static let item1 = Item(value: ["name": "fluffy coasters", "isFavorite": false, "ownerId": "previewRealm"])
static let item2 = Item(value: ["name": "sudden cinder block", "isFavorite": true, "ownerId": "previewRealm"])
static let item3 = Item(value: ["name": "classy mouse pad", "isFavorite": false, "ownerId": "previewRealm"])
}
extension ItemGroup {
static let itemGroup = ItemGroup(value: ["ownerId": "previewRealm"])
static var previewRealm: Realm {
var realm: Realm
let identifier = "previewRealm"
let config = Realm.Configuration(inMemoryIdentifier: identifier)
do {
realm = try Realm(configuration: config)
// Check to see whether the in-memory realm already contains an ItemGroup.
// If it does, we'll just return the existing realm.
// If it doesn't, we'll add an ItemGroup and append the Items.
let realmObjects = realm.objects(ItemGroup.self)
if realmObjects.count == 1 {
return realm
} else {
try realm.write {
realm.add(itemGroup)
itemGroup.items.append(objectsIn: [Item.item1, Item.item2, Item.item3])
}
return realm
}
} catch let error {
fatalError("Can't bootstrap item data: \(error.localizedDescription)")
}
}
}
// MARK: Views
// MARK: Main Views
/// The main screen that determines whether to present the SyncContentView or the LocalOnlyContentView.
/// For now, it always displays the LocalOnlyContentView.
@main
struct ContentView: SwiftUI.App {
var body: some Scene {
WindowGroup {
LocalOnlyContentView()
}
}
}
/// The main content view if not using Sync.
struct LocalOnlyContentView: View {
@State var searchFilter: String = ""
// Implicitly use the default realm's objects(ItemGroup.self)
@ObservedResults(ItemGroup.self) var itemGroups
var body: some View {
if let itemGroup = itemGroups.first {
// Pass the ItemGroup objects to a view further
// down the hierarchy
ItemsView(itemGroup: itemGroup)
} else {
// For this small app, we only want one itemGroup in the realm.
// You can expand this app to support multiple itemGroups.
// For now, if there is no itemGroup, add one here.
ProgressView().onAppear {
$itemGroups.append(ItemGroup())
}
}
}
}
// MARK: Item Views
/// The screen containing a list of items in an ItemGroup. Implements functionality for adding, rearranging,
/// and deleting items in the ItemGroup.
struct ItemsView: View {
@ObservedRealmObject var itemGroup: ItemGroup
@State var counter = 0
/// The button to be displayed on the top left.
var leadingBarButton: AnyView?
var body: some View {
let _ = Self._printChanges()
NavigationView {
VStack {
// The list shows the items in the realm.
List {
ForEach(itemGroup.items) { item in
ItemRow(item: item)
}.onDelete(perform: $itemGroup.items.remove)
.onMove(perform: $itemGroup.items.move)
}
.listStyle(GroupedListStyle())
.navigationBarTitle("Items", displayMode: .large)
.navigationBarBackButtonHidden(true)
.navigationBarItems(
leading: self.leadingBarButton,
// Edit button on the right to enable rearranging items
trailing: EditButton())
// Action bar at bottom contains Add button.
HStack {
Spacer()
Button(action: {
// The bound collection automatically
// handles write transactions, so we can
// append directly to it.
$itemGroup.items.append(Item())
}) { Image(systemName: "plus") }
}.padding()
Button("Increment counter (\(counter))") {
counter += 1
}
}
}
}
}
struct ItemsView_Previews: PreviewProvider {
static var previews: some View {
let realm = ItemGroup.previewRealm
let itemGroup = realm.objects(ItemGroup.self)
ItemsView(itemGroup: itemGroup.first!)
}
}
/// Represents an Item in a list.
struct ItemRow: View {
@ObservedRealmObject var item: Item
var body: some View {
let _ = Self._printChanges()
// You can click an item in the list to navigate to an edit details screen.
NavigationLink(destination: ItemDetailsView(item: item)) {
Text(item.name)
if item.isFavorite {
// If the user "favorited" the item, display a heart icon
Image(systemName: "heart.fill")
}
}
}
}
/// Represents a screen where you can edit the item's name.
struct ItemDetailsView: View {
@ObservedRealmObject var item: Item
var body: some View {
VStack(alignment: .leading) {
Text("Enter a new name:")
// Accept a new name
TextField("New name", text: $item.name)
.navigationBarTitle(item.name)
.navigationBarItems(trailing: Toggle(isOn: $item.isFavorite) {
Image(systemName: item.isFavorite ? "heart.fill" : "heart")
})
}.padding()
}
}
struct ItemDetailsView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ItemDetailsView(item: Item.item2)
}
}
}
To reproduce, run this, add an item, then click on the “Increment counter” button and check the log. You will see something like:
ItemRow gets rerendered even though it did not (functionally) change at all. But, in actuality it’s a brand new observable object getting passed in to ItemRow.
A dev on github tested the memory address theory and said the same thing happens if you cache the items into an array, so that disproves the theory.
My current theory is just that when you pass a Realm object to a view, you’re always implicitly creating an observable object, and that is always going to cause the view to rerender.
If you do some extra work and only pass equivalent data as a struct, the problem goes away. But, obviously you lose a lot of benefits with this approach.
I think for simple apps using List views that use navigation to drill down to detail views, this likely isn’t much of a problem because List manages how many items are “mounted” at any given time. But imagine something like a kanban board desktop app using a lot of custom stuff instead of List, and you might be able to imagine it becoming a major problem. Rerendering a component far up on the hierarchy causes a huge cascade of rerendering.
This is actually very accurate, I used to reproduce this behavior with the same means for a couple of years now, and avoid passing realm objects to a view, I started the struct approach years ago. This is very accurate and on point.
Also you can double check this by using TestFlight and comparing the re-renders that keep occurring on a page by page basis in the app. Readingminitial post, I’m glad I read through the rest of this as I was just about to suggest this.
Yes I find SwiftUI and RealmSwift don’t get along too well.
Just placing the cursor in a TextField that is bound to a Realm object property causes the whole user interface to lock up on macOS. For some reason it seems this is causing all UI objects to be re-rendered. Not sure this is a RealmSwift related issue or a SwiftUI issue.
I have two lists, one with around 30 realm objects and a second one with around 2000 objects (not realm objects).
The bound property in the TextField comes from one of the realm objects from the first list. Just placing the cursor in the TextField causes the colourful umbrella to show up for 20 or 30 seconds. Pretty strange.
EDIT:
LazyVStack {} seems to largely address the issue - which does not appear to be RealmSwift related. Seems SwiftUI will by default create all the list views unless LazyStack is used. !
That’s very concerning. One of the projects we use internally for testing is macOS, SwiftUI and displays a couple hundred realm objects in a list along with a few bound TextFields in the same UI.
We have not seen any slowdowns or other odd behavior. Not saying it isn’t there, we just dont notice them and are not experiencing a lock up.
Do you have some brief sample code that duplicates the behavior? Going forward, it would be helpful to know what to avoid doing!
We had the same issue and here’s what we’ve found. Sorry if this isn’t explained well.
SwiftUI does have a problem with re-rendering too much of the screen when any data changes. The new SwiftUI changes announced at WWDC should fix this problem moving forward with the new iOS version.
RealmSwift still has a fundamental problem that doesn’t play nice with SwiftUI. Realm returns new references/object references every time the code hits a realm object. And every re-render requests the realm data… This is a very fundamental flaw that needs to be fixed for realm to play nice with SwiftUI. If someone doesn’t see issues from this, they probably have a simple view/screen with little data being displayed. SwiftUI’s reactive behavior is not designed to work with data that changes every single time you ask for it. The issue compounds when SwiftUI is re-rendering way too much of the screen for small data changes.
It’s been a little while since I read about Realm’s freezing objects concept, which should remove this live object / new object issue, but it simply doesn’t do that when using Swift. I’m assuming this is a bug that hasn’t been addressed but it’s hard to say for sure.
We work around this realm new object issue by transforming all realm objects to structs before giving it to a view so it properly freezes the data when a view gets ahold of the data. And if the data changes, we replace the relevant struct.
Lastly, there is a problem with displaying a list of textfields in swiftUI. If 1 textfield grabs focus, SwiftUI re-renders the entire screen, basically. So that will cause very noticeable UI lag. We work around this by only displaying “text” objects in a list, and on tap we replace the text with a textfield so there is only 1 available at a time on a given list. I’m assuming this has been fixed in the new version of SwiftUI but I haven’t checked yet.
I’ve found this problem to be SwiftUI itself. Any view with @FocusState will rerender any time there is a change in focus, whether or not it’s relevant to that view. Hopefully iOS 17 and macOS Sonoma will fix it. I haven’t looked into the latest focus changes for that yet.
Thank you for your post. Is there any way you could post any code (actual or example) of your work around of ‘transforming realm objects to structs before giving it to a view’? I’m having the same issue in SwiftUI and would love to see how someone else figured it out.
The high level explanation is to take the data contained in a Realm object class, and put that data into a a struct so it’s static. Then pass those structs to the view for rendering. Because they are static, they don’t change and will not cause SwiftUI to refresh frequently. If the data in Realm changes on an object, only that object will be refreshed via the associated Realm object class.
The obvious downside to that is you loose a LOT of Realm functionality so I don’t feel it’s a good workaround.
At this time, we are not seeing an ongoing issues but definitely use LazyVStacks/LazyHStacks.
Any news on this issue / best practices that overcome abundant view invalidations?
Realm + Swift seems like a great way to rapidly prototype, but I haven’t managed to get the performance I want for a production-ready app, mainly because of @ObservedRealmObjects seeming to trigger a lot of abundant view invalidations.
The code below reproduces the issue: I would expect check marking a single item on the todo list with only invalidate that single TodoRow view; instead, it invalidates all rows.
import RealmSwift
import SwiftUI
// MARK: Models@mainstructshufflelistApp: SwiftUI.App {
var body: someScene {
WindowGroup {
LocalOnlyContentView()
}
}
}
/// An individual item. Part of an `ItemGroup`.finalclassTodo: Object, ObjectKeyIdentifiable {
/// The unique ID of the Item. `primaryKey: true` declares the/// _id member as the primary key to the realm.@Persisted(primaryKey: true) var _id: ObjectId@Persistedvar isChecked: Bool=false/// The backlink to the `ItemGroup` this item is a part of.@Persisted(originProperty: "items") var group: LinkingObjects<TodoList>
}
/// Represents a collection of items.finalclassTodoList: Object, ObjectKeyIdentifiable {
/// The unique ID of the ItemGroup. `primaryKey: true` declares the/// _id member as the primary key to the realm.@Persisted(primaryKey: true) var _id: ObjectId/// The collection of Items in this group.@Persistedvar items =RealmSwift.List<Todo>()
convenienceinit(n: Int) {
self.init()
for_in0..<n {
self.items.append(.init())
}
}
}
/// The main content view if not using Sync.structLocalOnlyContentView: View {
// Implicitly use the default realm's objects(ItemGroup.self)@ObservedResults(TodoList.self) var lists
var body: someView {
iflet todoList = lists.first(where: {$0.items.count >1 }) {
// Pass the ItemGroup objects to a view further// down the hierarchyToDoListView(todoList: todoList)
} else {
// For this small app, we only want one itemGroup in the realm.// You can expand this app to support multiple itemGroups.// For now, if there is no itemGroup, add one here.ProgressView()
.onAppear {
$lists.append(TodoList(n: 50))
}
}
}
}
//let realm = Realm()// MARK: Item Views/// The screen containing a list of items in an ItemGroup. Implements functionality for adding, rearranging,/// and deleting items in the ItemGroup.structToDoListView: View {
@ObservedRealmObjectvar todoList: TodoListvar body: someView {
let_=Self._printChanges()
NavigationStack {
SwiftUI.List(todoList.items) {
ToDoRow(item: $0)
}
}
}
}
structToDoRow: View {
@ObservedRealmObjectvar item: Todo@Environment(\.realm) var realm
var body: someView {
let_=Self._printChanges()
HStack {
Text(item._id.stringValue)
Spacer()
Button {
try? realm.write {
iflet thawed = item.thaw() {
thawed.isChecked.toggle()
}
}
} label: {
Image(systemName: item.isChecked ?"circle.fill" : "circle" )
.font(.title2)
}
}
}
}
Thanks for your reply Jay. Unfortunately putting the row in a LazyHstack still invalidates all rows when a single row is checkmarked.
Unless I’m missing something, this seems like a very problematic side-effect of using @ObservedRealmObject, and there Realm + Swift in general, for any project requiring a slightly more complex UI. As mentioned in the above posts, users that use e.g., a very simple List view to navigate to some detail view holding a single realm object might not notice much issues, but for slightly more complex projects showing a number of @ObservedRealmObjects on a single screen at the same time (e.g., a Kanban board) will very quickly experience significant lags in the UI as Swift will invalidate and rerender every single view containing an @ObservedRealmObject, instead of only the one that changed.
At least I would expect the simple two-view todo list example I provided to invalidate views more selectively, as it is as simple as a project can get and is even simpler than the sample code provided in the Realm SwiftUI docs.
Honestly, I migrated away from realm and don’t suggest using it. It has fundamental issues with immutable reactive state and permissions/security.
The best stack I’ve found is using Powersync.com + Supabase(postgres) as a great all-around stack for offline/local first development that seamlessly includes full permissions/security/user accounts/file storage/generous free tier/etc.
I wish there was a Powersync option for Mongo, if there was I would be using it. But Realm isn’t it.
@Richard_Anderson Thanks for the feedback - hopefully that was communicated to the Realm development team as well as they are important points.
Do the tools you suggest have current public support for iOS/macOS development or Swift SDK’s? I would like to look into that if they do - I don’t see anything on their site though.
Have not run into security issues yet, but unfortunately have to agree with the reactive part in my experience @Richard_Anderson.
@Jay FYI I am now also in the process of migrating away from Realm/MongoDB to Firebase Firestore storage, which offers similar real-time sync as Realm Swift – the initial reason I opted for Realm Swift a few years ago – but for which I could easily set up the above ToDoList example without the view invalidation issues. Hope the Realm development time will pick up this issue sometime in the future because, except for those view invalidations, I enjoyed using it a lot.
Do you know if they are aware? Otherwise I’m happy to open an issue.