Realm Sync + SwiftUI: Object is already managed by another Realm. Use create instead to copy it into this Realm

Hey all,

I’m having some issues with Realm + SwiftUI.

Models
My app has 4 models:

Household

final class Household: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: String = UUID().uuidString

    @Persisted var name: String
    
    @Persisted var users = RealmSwift.List<String>()
    
    @Persisted var lists = RealmSwift.List<List>()
    
    @Persisted var itemTypes = RealmSwift.List<ItemType>()
}

List

final class List: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: String = UUID().uuidString

    @Persisted var name: String
            
    @Persisted var listItems = RealmSwift.List<ListItem>()
}

ItemType

final class ItemType: Object, ObjectKeyIdentifiable {
    @Persisted(primaryKey: true) var _id: String = UUID().uuidString

    @Persisted var name: String
    
    @Persisted var categories = RealmSwift.List<String>()
}

ListItem

final class ListItem: EmbeddedObject, ObjectKeyIdentifiable {
    @Persisted var _id: String = UUID().uuidString

    @Persisted var itemType: ItemType?
    
    @Persisted var quantity: String?
    
    @Persisted var note: String?
}

Essentially I have set it up so that a user belongs to a household (the household ID is the partition key), each household has 1 or more lists and 1 or more itemTypes. A list contains a collection of listItems and each listItem is assigned an item type.

I’ve chosen this model so that when a user updates the name or categories associated with an itemType, all listItems assigned that itemType will also be updated.

My View
Here is a simplified version of my views:

ListsView

struct ListsView: View {
    @StateRealmObject var household: Household #This is passed in from a parent view where the user selects a household. At the moment it only shows 1 household (since household ID is the partition key, but I will fix this up later once the rest of the app is working as intended)

    var body: some View {
        VStack {
            SwiftUI.List {
                ForEach(household.lists) { list in
                    NavigationLink(destination: ListDetailView(household: household, list: list)) {
                        Text(list.name)
                    }.font(.body)
                }
            }
            .navigationBarTitle("Your Lists")
        }
    }
}

ListDetailView

struct ListDetailsView: View {

    @StateRealmObject var household: Household
    @StateRealmObject var list: List

    @State var itemName: String = ""

    var body: some View {
        VStack {
            SwiftUI.List {
                ForEach(list.listItems, id: \.id) { item in
                    if let itemType = item.itemType {
                        ListItemView(listItem: item, itemType: itemType)
                    }
                }.onDelete(perform: $list.listItems.remove)
            }
            Spacer()
            HStack {
                TextField("Enter an item name", text: $itemName)
                TsButton(
                    action: {
                        onInsertItem(name: itemName)
                    },
                    text: "add item"
                )
            }
        }
            .navigationTitle(list.name)
    }

    /**
        When a user inserts an item, check if an itemType already exists for the given name.
        If an itemType with this name already exists, assign that itemType to a new listItem
        and assign that listItem to the currently viewed list.

        If an itemType does not exist with a given name, create a new one, assign the new itemType
        to a new listItem and assign that listItem to the currently viewed list.
    */
    func onInsertItem(name: String) {
        if let itemType = household.itemTypes.first(where: { other in other.name == name }) {
            // Item type with this name already exists

            let listItem = ListItem()
            listItem.itemType = itemType

            $list.listItems.append(listItem)
        } else {
            // No item type exists for this name, so create a new one
            let itemType = ItemType();
            itemType.name = name
            $household.itemTypes.append(itemType)


            let listItem = ListItem()
            listItem.itemType = itemType
            $list.listItems.append(listItem)
        }
    }
}

Problem
I am getting the following error whenever I try to add a new listItem for an itemType that already exists:
Object is already managed by another Realm. Use create instead to copy it into this Realm.. If I add a listItem for a new itemType, it works as expected.

I have tried the following and the error becomes Cannot modify managed RLMArray outside of a write transaction.:

            let listItem = ListItem()
            listItem.itemType = itemType

            $list.listItems.wrappedValue.append(listItem)

I have also tried opening a write transaction against the realm referenced by the list, and I get an error about the primary key of the itemType not being unique.

When I inspect the itemType and list they both contain a reference to the same realm object. So I am not sure what may be going wrong.

Does anyone have any ideas?

The error is correct, because how our SwiftUI Helpers are implemented we use a different instance of Realm (not different realm file) to do any operation and in this case the error shows that you are trying to copy a managed object by other Realm (ItemType) which is been used by other realm and this is true.
I recommend to do everything on a realm write transaction.
I tested the following code and it is working.

func onInsertItem(name: String, listPrimaryKey: String) {
        let realm = try! Realm()
        guard let list = realm.object(ofType: List.self, forPrimaryKey: listPrimaryKey) else {
            return
        }

        let listItem = ListItem()
        let arrayItem = realm.objects(Household.self).compactMap { $0.itemTypes }.flatMap { $0 }
        if let itemType = arrayItem.first(where: { $0.name == name }) {
            listItem.itemType = itemType
        } else {
            // No item type exists for this name, so create a new one
            let itemType = ItemType()
            itemType.name = name
            listItem.itemType = itemType
        }

        try! realm.write({
            list.listItems.append(listItem)
        })
    }

Thanks Diana,

That solution only works for an on device Realm right? I am trying to use a synced realm.

I’ve tried accessing the realm via @Environment(\.realm) var realm: Realm but all my queries are returning zero results.

I am injecting the realm into the environment via the standard process:

        HouseholdsView()
            .environment(\.realm, realm) // Where realm is coming from autoOpen

I’ve also tried this:

func onInsertItem(name: String, listId: String) {
        guard let realm = list.realm?.thaw() else {
            return
        }

        guard let list = realm.object(ofType: List.self, forPrimaryKey: listId) else {
            print("No list found with PK \(listId)")
            return
        }

        let listItem = ListItem()
        let arrayItem = realm.objects(Household.self).compactMap { $0.itemTypes }.flatMap { $0 }
        if let itemType = arrayItem.first(where: { $0.name == name }) {
            listItem.itemType = itemType
        } else {
            // item type does not exist
            let itemType = ItemType();
            itemType.name = name
            listItem.itemType = itemType
        }

        try! realm.write {
            list.listItems.append(listItem)
        }
    }

and it almost works. I can insert as many items as I want into my list, but it always falls into the “item type does not exist branch”. I’m assuming this is because the Realm I’m using is attached to the list object, so it doesn’t contain the itemTypes which are in a different instance of Realm?

I’m experiencing this issue as well.

The guide here says to append a new item to the Observed results like:

$groups.append(Group())

The issue seems to be that if you want to initiate any values, there isn’t a functioning way to do that ( or at least the documentation / example is not complete).

I’ve tried to pass in params for initiation like:

$groups.append(Group(name: "Group Name"))

but that is not possible.

Alternatively I have tried to initiate the object first, set the initial parameters and then append, but that gives the error @Campbell_Bartlett has described here.

It’s as if initiating the object is automatically assigning it to some local Realm even when using a synced Realm, so then it can’t append it as demonstrated in the example.

Even if I pass in the correct environment in SwiftUI to the view with a ..environment(\.realmConfiguration, app.currentUser!.configuration(partitionValue: "user=\(app.currentUser!.id)")) and establish the correct default _partition in the Realm Model, it is still creating the object in some other Realm and throwing the error.

I know there has got to be a best practice for creating new objects in SwiftUI that allows for initial parameters to be passed in, it is just very unclear how to do this.

@Kurt_Libby1 I cannot reproduce your issue. Passing params to a constructor on an object should not involve a Realm unless there is one involved inside the constructor. Could you open a GitHub issue showing your usage?

Hey Lee.

I’m probably saying it wrong if you think it shouldn’t involve a Realm.

I’m talking about setting properties for a Realm object and then adding it in the prescribed way.

Your example is about adding a Group Realm object by appending Group() with the @ObservedResults property wrapper and the using .append()

Your example is extremely basic. For anything more complex, there is no example and that was my comment – either the example is incomplete or there isn’t a good way to do this.

In my app I am checking to see if there is a plan with a location like this:

@ObservedResults(Plan.self) var plans
@StateRealmObject var location: Location
...
if let plan = plans.filter("location._id = %@", location._id).first {
  // show plan
} else {
  // add location to plan like this:
  $plans.append(Plan(_id: UUID(), _partition: "user=\(app.currentUser!.id)",location: self.location))

  // because if I do it as prescribed like this:
  $plans.append(Plan())
  // it works, but there is no way to add the location, so there's just an empty Plan object added to the database
}

This doesn’t work.

I’ve tried to pass in the location as a var, as @ObservedRealmObject, and as @StateRealmObject as shown above. No matter what it always shows the error:

Object is already managed by another Realm. Use create…

I’m not super interested in opening a GitHub issue. I’m more interested in documentation and/or examples that goes beyond the most basic usage. Again, surely there is a way to do this, but either I’m misunderstanding how to do this in Realm, or I’m not saying it correctly. Either are entirely possible.

@Kurt_Libby1 I see what you mean, this is for sure something we can improve on. My first idea to resolve the issue would be to use the Realm.create(_, value:, update:) API. The usage would be like so:

let realm = plans.thaw()!.realm!
try! realm.write {
    $plans.append(realm.create(Plan.self, value: Plan(_id: UUID(), _partition: "user=\(app.currentUser!.id)",location: self.location), update: .modified)
}

1 Like

I solved my issue by adding the plan to the location when the location was created.

( I would love to know where in the documentation your approach is presented because I’ve never seen this update: .modified before. )

Also, I’m again running into the same error today in another view where I’m loading a bunch of tags and trying to add them to a list of tags on another object.

Simple example is like this:

@ObservedResults(Tag.self) var tags
@ObservedRealmObject var room: Room

// Room() has a property defined as @Persisted var tags = RealmSwift.List<Tag>()

ForEach(tags) { tag in
  Text(tag.name)
    .onTapGesture {
      $room.tags.append(tag)             
    }
}

I’m getting this same error:

Object is already managed by another realm.

What I don’t understand is this:

I only want to use one realm. It’s synced. It’s a partition set with the logged in user’s id as user=\(app.currentUser!.id). And every interaction with realm should be with this realm. Every part of my object model has this _partition set in the definition of the model.

So why are there other realms? How can I make it so that there are no other realms and every CRUD is always on this one realm with this one partition?

And again, I can’t find any documentation that helps explain this in a clear enough way. For the sake of presenting realm as simple, it seems like anything beyond the examples just break the functionality.

I know this probably isn’t the case, but that is how it seems.

Any insight and links to additional documentation would be super helpful.

Thanks.

Hi,

I think the issue you are having is caused by @ObservedResults(Tag.self) var tags realm instance being different from @ObservedRealmObject var room: Room realm. I had the same problem and something like this worked from me:

ForEach(tags) { tag in
  Text(tag.name)
    .onTapGesture {
      $room.tags.append(room.realm.objects(Tag.self).first(where: {$0._id == tag._id})             
    }
}

I hope this will help you.

Best regards,
Horatiu

Thanks Hortiu,

But that didn’t work.

I had to add some bangs for the optionals, but I got the same “Object is already managed by another Realm” error.

Here is the code with the bangs to make the complier happy:

$room.tags.append(room.realm!.objects(Tag.self).first(where: {$0._id == tag._id})!)

So this doesn’t work, AND it still doesn’t explain my main questions.

Why is there another realm? How can I make sure that there is only ever one realm?

I want my users to log in and always have the same realm. Always.

I have other apps where I go back and forth between multiple realm partitions, and I though this implementation would be easy, but it is definitely not.

Does your Tag type has other realm properties? Realm raises the same exception when you append a tag object that has other realm objects as its properties. It also sees those nested objects coming from a different realm.

Regarding the different realms, I don’t know why. I faced the same problems and I noticed that, in my case, realm instances were different.

About the realm query, you could let swift do the unwrapping:

$room.tags.append(room.realm?.objects(Tag.self).first(where: {$0._id == tag._id}) ?? tag)

Horatiu

My Tag type did have other Realm properties. So this morning I restructured the app so that it doesn’t, but that didn’t solve my issue. Still got the same Object is already managed by another Realm error.

Spent a couple of hours trying different things and stripping it down to a basic app that you can find here.

Ultimately I discovered that the object with the list as well as the object being added to the list both have to be thawed before it works:

  let thawedRoom = room.thaw()!
  let realm = thawedRoom.realm!
  let thawedTag = tag.thaw()!
                  
  do {
    try! realm.write {
      thawedRoom.tags.append(thawedTag)
    }
  }

Hopefully the Realm team can figure out a way for this to be clearer and for the implicit writes to work beyond extremely basic examples. And then also add to the documentation how to get this to work with only one Realm so we can stop running into these “another Realm” errors when 99% of the time we only ever want to deal with one Realm.

I agree with @Kurt_Libby1

It is a bit frustrating this confusion about “managed by another realm”.

The default behavior must be a “single realm”. I don’t want another Realm. Please “Mr. realm swift” , just use the single existing Realm.

“let realm = try! Realm()”

Why should I care about “another realms” if I just declared a single realm instance ?

Can someone from mongo staff give some guidance ?

2 Likes

@Robson_Tenorio

Do you have a specific use case? This:

let realm = try! Realm()

Doesn’t create “another realm” it accesses the same Realm that everything else on that thread accesses. If you’re on a different thread however, say a background thread, then it would (could) be “another realm”.

This is especially try if you’re using sync’ing and are writing on the UI thread as Sync writes on a background thread so there would be “another realm” in that case.

Got some a troublesome code example you can post?

While Robson may have some troublesome code to post, I will say that the continued frustration with using any synced Realm is just how hard it is to do exactly what the complaint/agreement is here.

Rather than posting “troublesome code”, it would be infinitely more beneficial for MDB to provide code examples of how to easily create one synced realm and always use that.

The back and forth on every screen in every way that I handle writing and reading from Realm seems to behave in completely unpredictible ways as I’m constantly thawing or needing to find clever places to add writes just to “make sure” the object isn’t managed by another Realm.

As I shared before and was not provided an answer, I will share again:

I want my users to log in and always have the same realm. Always.

There is no clean example of this that I know of in the documentation, nor as a response to my earlier post six months ago. Rather than seeing an answer to my or Robson’s troublesome code, I would sincerely love a straightforward answer about how the above quoted line should be achieved.

As SJ would have said, maybe we’re just “holding it wrong.”

That’s fine. I’ll admit to that. I just want to be shown how to hold it.

1 Like

@Kurt_Libby1

I think you missed my point, or perhaps it was too vague. The post referenced “a single realm” and the code presented

“let realm = try! Realm()”

does exactly that - a single realm. If you call that 900000 times in your app, you’re still going to have “a single realm”

When there isn’t a “single realm” is when a Realm is instantiated on different threads, say in the UI thread and then again in a background thread - and then objects are addressed from those “different realms”. I provided an example of that in my response above.

My query was in an effort to assist - requesting a short verifiable example to see what that user is experiencing to try to match it up with your initial code is not unreasonable.

If you review the TaskTracker Sync sample app, you’ll find that when a users logs in they always have the same Realm. Always.

The goal is to identify why your code behaves differently and IMO more data is needed.

1 Like

So the issue here is when we append an object with a nested managed object, for example for the following model from the initial post.

final class ListItem: Object, ObjectKeyIdentifiable {
  @Persisted var _id: String = UUID().uuidString
  @Persisted var itemType: ItemType?
  @Persisted var quantity: String?
  @Persisted var note: String?
}

we get an Object is already managed by another Realm. Use create instead to copy it into this Realm. error for the following operation

let listItem = ListItem() // Create unmanaged object
listItem.itemType = itemType // We are setting a managed object to a property of an unmanaged object
$list.listItems.append(listItem) // We append the unmanaged object

The reason for this, is that when the write is done, we only check if TopLevel object has a realm associated but because in this case it doesn’t (is unmanaged) we don’t thaw it to use the same realm as the write, we don’t check any nested if any nested properties has a managed object.
For this to work, we would have to verify not only if the TopLevel Object has an associated realm, but if any set property has it, and make sure they use the same realm.
In conclusion, we will have to do a recursive check for sub-objects when users try to add an unmanaged object.

There is a workaround for this, you can use create, like the following code.

let realm = list.thaw()!.realm!
try! realm.write {
   let listItem = realm.create(ListItem.self, value: ["itemType": itemType], update: .modified)
   $list.listItems.append(listItem)
}

This is something we clearly have been taking a look, and I just opened a GitHub issue to track any progress https://github.com/realm/realm-swift/issues/7942 on this.

1 Like

Thanks Diana.

This is how I do it as well. I have that thaw workaround all over my apps to account for this bug. I’ll watch the issue you shared for sure. I think the frustrating thing here is what you pointed out at the beginning of your post and still is the spirit of this discussion:

Once a user logs in, 99% of the time we only ever want to use their realm. So instead of creating “unmanaged” objects, I would love it if every newly created object was managed by that user’s realm unless explicitly stated otherwise. We’re adding the same bit of code all over the app for all different kinds of writes and it’s exactly the clunky boilerplate that is antithesis to the spirit of Realm.

Very pleased to know that this is being addressed and will be resolved soon. --Kurt

PS: I do have one app where I switch the Realm environments by passing it in the .environment() and I have always assumed that passing the Realm in the environment will make any objects created in that View managed by that Realm, but it does not seem to be the case. This has added to the confusion as well.

Hey Diana,

Thank you for getting back to me, I am glad the team is looking to simplify things so others don’t run into the same problem.

I appreciate that you have taken the time to update me about such an old issue!

Is this specifically an issue in SwiftUI only?

It seems to work in Swift without that error message (if I am duplicating the issue correctly, which is questionable lol)