Realm object update with SwiftUI

I am trying to perform updates to a realm object in a SwiftUI application but get an error when performing the update.

What special magic is required to be able to perform and update to a realm object in response to a Gesture. In the example below from here (https://docs.mongodb.com/realm/sdk/ios/integrations/swiftui/) the Toggle() is bound directly to the items property.

I need to be able to call a method to perform multiple updates in response to a gesture - the example below does not show any examples of how to perform a custom update on an observed object. Is there an example that shows how you are supposed to do that with SwiftUI ?

Would it be possible to include a simple example of doing that in the example below - just for completeness.

Thanks

/// 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()
    }
}

What is the error that you’re getting? Could you give us an example of what you’re trying to do?

The error is - can’t access frozen Realm. The observable wrappers obviously take care of the updates via bindings but an example of how to do updates to the objects directly in code would be useful.

Also any discussion on whether to do updates on the main thread or not and any side effects of doing so. it seems that creating a new Realm for updates like the one below may be inefficient for very small updates so should be performed on the main thread but what is the risk of blocking the main thread.

Pretty cool though what you have done to make Realm integrate well with SwiftUI. We might have to do a rewrite sometime since it is a lot more efficient!! ouch.

Something like the following. Or if there is a more efficient way that way.

struct CheckBoxSelectionRealm: View {
    @ObservedRealmObject var item: RealmObject
    var size: CBSize
    
    var sizes: [CBSize : CGFloat] = [.small: 14, .medium: 17, .large: 21]
    
    var width: CGFloat {
        return sizes[size] ?? 17
    }
    var height: CGFloat {
        return sizes[size] ?? 17
    }
    
    var imageName: String {
        return item.isSelected ? "checkmark.square" : item.isChildSelected ? "minus.square" : "square"
    }
    var imageColor: Color {
        return item.isSelected ? Color.accentColor.opacity(0.6) : item.isChildSelected ? Color.accentColor.opacity(0.6) : Color.secondary
    }
    
    var body: some View {
        HStack {
            Text(item.name.count > 0 ? item.name : "Unlabelled")
                .foregroundColor(imageColor)
            Spacer()
            Image(systemName: imageName)
                .resizable()
                .frame(width: width, height: height)
                .foregroundColor(imageColor)
        }
        .onTapGesture {
                item.toggleSelection()
        }
    }
}

extension RealmObject {
  func toggleSelection(){
    
    /// Get object ID so we can access on another thread
    let id = self._id
    
    /// Do long running work - whatever that might be on a background thread
    DispatchQueue.global().async {
        if let realm = Realm.IAMRealm, let selfItem = realm.object(ofType: RealmObject.self, forPrimaryKey: id) {
            
            do {
                try realm.write({
                    
                    selfItem.isSelected.toggle()
                    
                    selfItem.parent?.setIsChildSelected()
                    
                })
            } catch {
                os_log(.error, "Error toggling selection for RealmObject \(self.name)")
            }
        }
    }
}
}

There’s really nothing wrong with your code. A couple of thoughts

It’s not clear if your using Sync or not but if you’re using Realm locally, non sync, then avoid opening a write on both the UI and background threads.

If you are using Sync, generally speaking, doing that write on a background thread is best practice. However, you’re updating one property of one object so the chances if it tying up your UI is very very small.

A second thing is really a design decision; it looks like you’ve loaded and Item and if the user toggles a checkbox, you want to update one of it’s properties. Then you get the key of that object, load it in and update the property.

It doesn’t look like your using objects on different threads - maybe you are? If not, since you already loaded the object, it’s loaded again just to update that property. Why not just update the already loaded item?

So instead of this

Why not

.onTapGesture {
   if let realm = Realm.IAMRealm {
     do {
        try realm.write {
          //item.isSelected.toggle() nope!
          $item.isSelected.wrappedValue = !item.isSelected
        }
     }
}

The last thought may go back to the actual issue: your question about the error

Are you freezing objects somewhere? Are the items being frozen and passed around? A bit more info may lead to a (better) answer.

Note: @ObservedRealmObject freezes an object.

Oh… why isSelected.toggle()? Why not item.isSelected = !item.isSelected or item.isSelected = checkbox state? That allows you to use the code in the guide as is without the need for toggle()

Thanks, yes I though the background thread was unnecessary for these small changes - and yes these items are already loaded into a list using the following - I am not doing any freeze of anything in my code. I assumed that maybe SwiftUI must be doing something. See the snippet below.

EDIT: Ah - I was using let realm = item.realm rather than your example getting a new realm = not sure if that could be the reason.

...
@ObservedResults(Taxonomies.self ) var taxonomies
...
    ScrollView {
                            VStack(alignment: .leading, spacing: 0) {
                               
                                ForEach(tax.taxonomies.sorted(byKeyPath: "name")) { group in
                                        
                                        TaxonomyGroup(group: group, spacing: 0)
                                        
                                    }
                                
                            }.padding(.leading, 4)
                            .padding(.trailing, 6)
                            //.onDelete(perform: $taxonomies.items.remove)
                            //.onMove(perform: $taxonomies.items.move)
                        }.listStyle(SidebarListStyle())

...

I just checked that when I use the items realm I get the " Can’t perform transactions on a frozen Realm" error.

If I use the following

       if let realm = Realm.IAMRealm {
             do {
                try realm.write {
                    let new = Taxonomy(name: "New Group", parent: item, isLeaf: false)
                    $item.children.append(new)
                }
             } catch {
                os_log(.error, "Error")
            }
        }

Then I get the following error:
" Cannot modify managed RLMArray outside of a write transaction."

BTW my Realm extension looks like this

extension Realm {
    static var IAMRealm: Realm? {
        let configuration = Realm.Configuration(schemaVersion: 2)
        do {
            let realm = try Realm(configuration: configuration)
            return realm
        } catch {
            os_log(.error, "Error opening realm: \(error.localizedDescription)")
            return nil
        }
    }
}

And if I try using this as per your suggestion

func toggle(){
        if let realm = Realm.IAMRealm {
             do {
                try realm.write {
                    item.isSelected = !item.isSelected
                }
             } catch {
                os_log(.error, "Error")
            }
        }
    }

I get the following error

**Attempting to modify object outside of a write transaction - call beginWriteTransaction on an RLMRealm instance first.**

And similarly this fails with the same error

func toggle2(){
        if let realm = Realm.IAMRealm {
            realm.beginWrite()
            
            item.isSelected = !item.isSelected
            
             do {
                try realm.commitWrite()
             } catch {
                os_log(.error, "Error")
            }
        }
    }

Oh and this is the top level view where it is implicitly using the default realm’s objects(Taxonomies.self). Could it be this is a problem since the system is I am using Realm.IAMRealm which uses a configuration elsewhere.

Can I get the default realm to use the same configuration somehow ? Overriding Realm.realm in an extension perhaps?

struct TaxonomyBrowserView: View {
        @ObservedResults(Taxonomies.self ) var taxonomies

....
     var body: some View {

          if let tax = taxonomies.first {
                VStack(alignment: .leading) {
                    ScrollView {
                        VStack(alignment: .leading, spacing: 0) {
                           
                            ForEach(tax.taxonomies.sorted(byKeyPath: "name")) { group in
                                    
                                    TaxonomyGroup(group: group, spacing: 0)
                                    
                                }
                            
                        }
                    }
                }
            } else {
                AnyView(Text("Setting up taxonomies..."))
    ...

@Duncan_Groenewald there are a couple of ways to solve your issue. The “why” of the issue is that when you are calling on the @ObservedRealmObject, we freeze it– this gives it temporary immutability so that SwiftUI can use it appropriately. This does make custom mutations slightly more complex for the time being. However, to work with your example, there are two ways to accomplish what you’re trying to do:

func toggle2() {
    // this is the simple way
    $item.isSelected.wrappedValue = !item.isSelected
    // OR this is the complex way
    guard let thawed = item.thaw(), let realm = thawed.realm() else {
        os_log(.error, "Error")
        return
    }
    try! realm.write {
        thawed.isSelected =  !item.isSelected
    }
}
2 Likes

So if I need to do a few things, like creating a new item and adding it then I need to use the complex way.

let new = Taxonomy(name: "New Group", parent: item, isLeaf: false)
$item.children.append(new)

What I don’t understand is why your example shows adding children to a list without going through any hoops. What am I doing differently that this does not work. Is it because my items are in a list already and if I want to add an item to their children I can’t. It seems counterintuitive that at the same time you can modify the item directly when bound to a detail view (as per example). Is the RealmSwift taking care of thawing the object under the covers ?

I never did watch Ice Age 2 ! :slight_smile:

struct ItemsView: View {
    /// The group is a container for a list of items. Using a group instead of all items
    /// directly allows us to maintain a list order that can be updated in the UI.
    @ObservedRealmObject var group: Group

    /// The button to be displayed on the top left.
    var leadingBarButton: AnyView?
    var body: some View {
        NavigationView {
            VStack {
                // The list shows the items in the realm.
                List {
                    ForEach(group.items) { item in
                        ItemRow(item: item)
                    }.onDelete(perform: $group.items.remove)
                    .onMove(perform: $group.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.**
**                            $group.items.append(Item())**
                        }) { Image(systemName: "plus") }
                    }.padding()
                }
            }
        }
    }

Great these work just fine - a lot less complex than unnecessary background tasks ! But using Sync mainly I am used to doing that for most things anyway.

Awesome! It would be great is wrappedValue was used somewhere in the docs in this context.

Does $item.isSelected.wrappedValue = !item.isSelected need to be within a write?

Also, I think that goes hand-in-hand with

Which would be great to have in the docs as well as it’s something that would probably come up frequently when attempting to update an observed object.