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.