Reputation: 746
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
Reputation: 12144
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.
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.
Whenever you fetch an object from Realm, detach it (create a cloned object).
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())
}
}
Instead of keep events
as RLMResults<RLMMyEvent>
, keep it as [RLMMyEvent]
.
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
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.
Upvotes: 1
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