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.
@Julius_Huizing My guess is they are not aware of details surrounding “immutable reactive state” issues. It would be great if a coding example of the trouble were to be posted to git so at least it’s on the board.
Perhaps we are overlooking something obvious but we are not seeing or experiencing that issue - and our project has 2Gb of data which is frequently shown in list and other UI elements. We are still somewhat new to SwiftUI so perhaps our expectation is not correct. Take, for example the example project posted above by @Jonathan_Czeck in 2023.
That project works as is and the output to console also is what is stated
Changing the state re-renders the view - and Realm is not involved.
Going back to Realm, if either @ObservedResults or @ObservedRealmObject is used:
The Swift SDK provides the @ObservedRealmObject property wrapper that invalidates a view when an observed object changes (true for @ObservedResults too). You can use this property wrapper to create a view that automatically updates itself when the observed object changes.
So those are essentially @State vars - if you change it, the view re-renders.
I think that’s where my confusion comes in on this topic - to me, it’s working as intended.
It’s been a while since I worked with this problem, so without me investing too much time and because all of the relevant details should be covered in past posts by me or by Jonathan_Czeck.
Problems:
Realm doesn’t play nice with any reactive state because it gives back new memory references every single time data is referenced. So it can cause nasty exploding re-renders. You may not see the issue on a tiny test project but you can just check for yourself and see new memory references every time. Fundamental problem for any immutable reactive state. I think this is a foundational design principle from realm originally, I think it’s meant for OOP and not functional programming.
I remember that realm has some kind of freeze feature that should fix this problem, but it just doesn’t work. You still get new memory references and the excessive re-renders.
I don’t remember the details on this, but ultimately proper permissions/auth doesn’t work in a reasonable way between realm & mongo. I wish I could remember the details but we reached out to devs from the team and confirmed our findings at the time. Ultimately users will have access to data they shouldn’t.
Other stuff:
firestore/firebase doesn’t have any of these issues. But Firestone in my opinion is much more limiting to work with than MongoDB. Mongo would be my first pick if it had a capable offline sdk solution. Firebase does work great though.
Powersync.com also works well and pairs with Postgres (Supabase for me). It does have a swift library that isn’t full release yet but my experience with their flutter sdk has been really good.
New & Unread Topics
Topic list, column headers with buttons are sortable.