Simone Chelo
Simone Chelo

Reputation: 746

What to do when object on realm gets invalidated

In my iOS app I am using the Realm library for storing data. It works great, until for one reason or the other, object get invalidated. (reasons in the specific case could be that I sometimes implement background jobs on the same data that are getting visualized on view, or similar scenarios).

I know why it happens, and I understand is correct behavior but from there on: what is the correct behavior I should implement?

I would love to pull the data from Realm again and continue my job, but when an object gets invalidated, every field is inaccessible and crashes the environment (so, I don't know what is the unique+immutable id of the object).

How can I safely pull the current data from that object again, without storing the id in the ViewController?

Here is some code. I had to edit it heavily to since the structure of the app is different, but the issue is still exemplified. Assume that the table view delegate's is the view and all the technicalities are there.

// A class variable
var events: RLMResults<RLMMyEvent>
// A table view that shows all "MyEvents"
var tableview: UITableView

func initialiseView(_ predicate: NSPredicate) {
  // Get the events with a predicate from Realm
  events = RLMMyEvent.objects(with: predicate)
  tableview.reloadData()
}

// All tableView delegates, and in particular

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath) as! MyCell

   let event = self.events[UInt(indexPath.row)]

   if !event.isInvalidated {
   } else {

   /***** HERE is the problem. What to do here?
          HOW to get those data again, since I cannot 
          get that event's unique ID?  ****/
   }
   cell.initialize(event)
   return cell
}

Upvotes: 5

Views: 2078

Answers (3)

trungduc
trungduc

Reputation: 12144

Problem

As I understand, you want to access object's properties when the object is invalidated. Correct me if I'm wrong :)

First of all, let take a look at isInvalidated property

Indicates if the object can no longer be accessed because it is now invalid.

An object can no longer be accessed if the object has been deleted from the Realm that manages it, or if invalidate() is called on that Realm.

It means that an object can be invalidated only when it's managed by a Realm.

My solution

Detach object from the Realm which managers it. If object isn't managed by any Realm, of course it will never be invalidated and you can access properties as you want.

How to do

Whenever you fetch an object from Realm, detach it (create a cloned object).

  1. Add below code to your project. It's used to detach every object in Result after fetching from Realm. I found here

    protocol DetachableObject: AnyObject {
        func detached() -> Self
    }
    
    extension Object: DetachableObject {
    
        func detached() -> Self {
            let detached = type(of: self).init()
            for property in objectSchema.properties {
                guard let value = value(forKey: property.name) else { continue }
    
                if property.isArray == true {
                    //Realm List property support
                    let detachable = value as? DetachableObject
                    detached.setValue(detachable?.detached(), forKey: property.name)
                } else if property.type == .object {
                    //Realm Object property support
                    let detachable = value as? DetachableObject
                    detached.setValue(detachable?.detached(), forKey: property.name)
                } else {
                    detached.setValue(value, forKey: property.name)
                }
            }
            return detached
        }
    }
    
    extension List: DetachableObject {
        func detached() -> List<Element> {
            let result = List<Element>()
    
            forEach {
                if let detachable = $0 as? DetachableObject {
                    let detached = detachable.detached() as! Element
                    result.append(detached)
                } else {
                    result.append($0) //Primtives are pass by value; don't need to recreate
                }
            }
    
            return result
        }
    
        func toArray() -> [Element] {
            return Array(self.detached())
        }
    }
    
    extension Results {
        func toArray() -> [Element] {
            let result = List<Element>()
    
            forEach {
                result.append($0)
            }
    
            return Array(result.detached())
        }
    }
    
  2. Instead of keep events as RLMResults<RLMMyEvent>, keep it as [RLMMyEvent].

  3. After fetching result, detach objects and convert to an array

    events = RLMMyEvent.objects(with: predicate).toArray()
    

Now you can access properties without being afraid of invalidated objects and crash.

Note that detached objects will not be updated if the original objects or their values inside Realm are changed.

Upvotes: 4

arturdev
arturdev

Reputation: 11039

As a solution, you can observe for changes in the events array, and update/delete your cells accordingly, like

    eventsObserveToken = events.observe { [weak tableView] changes in
        guard let tableView = tableView else { return }

        switch changes {
        case .initial:
            tableView.reloadData()
        case .update(_, let deletions, let insertions, let updates):
            tableView.applyChanges(deletions: deletions, insertions: insertions, updates: updates)
        case .error: break
        }
    }

And an extension

extension IndexPath {
  static func fromRow(_ row: Int) -> IndexPath {
    return IndexPath(row: row, section: 0)
  }
}

extension UITableView {
  func applyChanges(deletions: [Int], insertions: [Int], updates: [Int]) {
    beginUpdates()
    deleteRows(at: deletions.map(IndexPath.fromRow), with: .automatic)
    insertRows(at: insertions.map(IndexPath.fromRow), with: .automatic)
    reloadRows(at: updates.map(IndexPath.fromRow), with: .automatic)
    endUpdates()
  }
}

And I believe that when an object is being invalidated it is being removed from the results array, so your app will not crash.

Or, you can use Unrealm. Unrealm enables you to easily store Swift native Classes, Structs and Enums into Realm without pain. And you don't need to worry about invalidated objects at all, because your app will not crash even if you try to access an invalidated object. Also, your app will not crash if you try to access an object from a different thread.

enter image description here

Upvotes: 1

Brandon Stillitano
Brandon Stillitano

Reputation: 1638

Have you tried using the RealmObject.isInvalidated variable to check if the object is still valid before you access it? Using this key won't cause the crash you're experiencing.

Current Scenario

realmObject.stringArray.append("")

Suggested Approach

if !realmObject.isInvalidated {
    realmObject.stringArray.append("")
}

Upvotes: 1

Related Questions