Sorting Realm results collection on computed property? - ios

I'm using Realm in my Swift iOS app and I have a model that I'm trying to add a computed property to. Here's what the class looks like:
class Conversation: Object {
#objc dynamic var conversationId: String = generateConversationId()
#objc dynamic var createdTime = Date()
#objc dynamic var title: String = ""
let messages = List<Message>()
var lastUpdated: Date {
if let newestMessage = self.messages.sorted(byKeyPath: "timestamp", ascending: false).first {
return newestMessage.timestamp
} else {
return Date(timeIntervalSince1970: 0)
}
}
}
As you can see, this object represents a conversation which has several properties one of which is a list of messages that belong to the conversation. The computed property that I've added (lastUpdated) should just return the timestamp of the most recent message in the messages list or, when the list is empty, it would return a default date value.
In one of my view controllers, I'm creating a results collection to access the Conversation objects like this:
var conversations: Results<Conversation> = realm.objects(Conversation.self)
This works and I can access the lastUpdated property on each Conversation as expected. However, I'd like to have this collection sorted by the lastUpdated property so I tried modifying my results collection like this:
var conversations: Results<Conversation> = realm.objects(Conversation.self).sorted(byKeyPath: "lastUpdated", ascending: true)
When I do this, the app throws the following exception:
Terminating app due to uncaught exception 'RLMException', reason: 'Cannot sort on key path 'lastUpdated': property 'Conversation.lastUpdated' does not exist.'
I think this might be happening because the lastUpdated computed property isn't persisted on the Conversation object (I would use Realm Studio to quickly verify this but my Realm is encrypted). I'm not explicitly ignoring this property but I assume this is the case because lastUpdated is not decorated with #objc dynamic like all of my other properties. I tried adding that, wiping my app install, recompiling and testing again but I still end up with the same exception.
Maybe it's not possible to persist a computed property (because, well, it's computed!) but is there another way to specify that my Conversation results collection should be sorted by this property?

As you suspect, you cannot persist a computed property and Realm's sorted(byKeyPath:,ascending:) method only works with persisted properties. There is no way to sort a Results instance based on a computed property while keeping the auto-updating Results.
However, there is a workaround if you don't need the auto-updating nature of Results. You can use Swift's sorted method, which takes a closure as its sorting parameter, which will result in a return value of type [Conversation]:
let conversations = realm.objects(Conversation.self).sorted(by: {$0.lastUpdated < $1.lastUpdated})

Related

Filtering Realm with nested subqueries

My app has data that looks like this.
class ShelfCollection: Object {
let shelves: List<Shelf>
}
class Shelf: Object {
let items: List<Item>
}
class Item: Object {
var name: String
let infos: List<String>
}
I'm trying to get all shelves in a shelf collection where any items match the query either by name or by an element in their infos list. From my understanding this predicate should be correct, but it crashes.
let wildQuery = "*" + query + "*"
shelfResults = shelfCollection.shelves.filter(
"SUBQUERY(items, $item, $item.name LIKE[c] %# OR SUBQUERY($item.infos, $info, info LIKE[c] %#).#count > 0).#count > 0",
wildQuery, wildQuery
)
It complies as a NSPredicate, but crashes when Realm is attempting to parse it, throwing me
'RLMException', reason: 'Object type '(null)' not managed by the Realm'
I suspect the nested subquery might be what fails, but I don't know enough about NSPredicate to be sure. Is this an acceptable query, and how can I make it.. work?
This is an answer and a solution but there's going to be a number of issues with the way the objects are structured which may cause other problems. It was difficult to create a matching dataset since many objects appear within other objects.
The issue:
Realm cannot currently filter on a List of primitives
EDIT: Release 10.7 added support for filters/queries as well as aggregate functions on primitives so the below info is no longer completely valid. However, it's still something to be aware of.
so this Item property will not work for filtering:
let infos: List<String>
However, you can create another object that has a String property and filter on that object
class InfoClass: Object {
#objc dynamic var info_name = ""
}
and then the Item class looks like this
class Item: Object {
var name: String
let infos = List<InfoClass>()
}
and then you filter based on the InfoClass object, not it's string property. So you would have some objects
let info0 = InfoClass()
info0.info_name = "Info 0 name"
let info1 = InfoClass()
info1.info_name = "Info 1 name"
let info2 = InfoClass()
info2.info_name = "Info 2 name"
which are stored in the Item->infos list. Then the question
I'm trying to get all shelves in a shelf collection...
states you want to filter for a collection, c0 in this case, shelves whose items contain a particular info in their list. Lets say we want to get those shelves whose items have info2 in their list
//first get the info2 object that we want to filter for
guard let info2 = realm.objects(InfoClass.self).filter("info_name == 'Info 2 name'").first else {
print("info2 not found")
return
}
print("info2 found, continuing")
//get the c0 collection that we want to get the shelves for
if let c0 = realm.objects(ShelfCollection.self).filter("collection_name == 'c0'").first {
let shelfResults = c0.shelves.filter("ANY items.infoList == %#", info2)
for shelf in shelfResults {
print(shelf.shelf_name)
}
} else {
print("c0 not found")
}
I omitted filtering for the name property since you know how to do that already.
The issue here is the infos could appear in many items, and those items could appear in many shelf lists. So because of the depth of data, with my test data, it was hard to have the filter return discreet data - it would probably make more sense (to me) if I had example data to work with.
Either way, the answer works for this use case, but I am thinking another structure may be better but I don't know the full use case so hard to suggest that.

Retrieving object properties with relation to their parent categories

I am new to realm and iOS development so I apologize in advance if something isn’t explained properly or is just incorrect.
I have 2 Realm Object classes:
class Category: Object {
#objc dynamic var name: String = ""
#objc dynamic var color: String = ""
let trackers = List<Tracker>()
}
and
class Tracker: Object {
#objc dynamic var timeSegment: Int = 0
var parentCategory = LinkingObjects(fromType: Category.self, property:
"trackers")
}
I’m able to store new timeSegment properties consistently; however, the issue is that I cannot retrieve & display a collection of timeSegment values relating to their parentCategory. setting
var entries : Results<Tracker>?
produces all results for every category, which is the only result i'm able to pull so far after testing.
Any help is appreciated, and can follow up with any additional details. Thanks
You need to call objects on your Realm object with a filter for fetching only results that match a predicate. The realm object in this code is an instance of the Realm class.
func getTrackersWithName(_ name: String) -> Results<Tracker> {
return realm.objects(Tracker.self).filter("name = \"\(name)\"")
}
This tells Realm to fetch all objects that match the filter predicate. In this case, the filter predicate matches any object where the value of the "name" property matches the string that is passed into the method.

Realm is creating multiple entries for nested objects on update

I'm experiencing trouble understanding how updating objects works in Realm. I'd appreciate help in helping me to understand how updating nested objects work and why it doesn't work the way I expect it.
I started using Realm just recently, and here's what I want to use it for: I have a set of key value pairs stored on my server, that serve as localized values for strings used in my iOS app. On app launch every now and then I want to update my strings, so I pull them from the server and store them locally in realm on my iOS device. I want to have only ONE instance of those strings on my device.
Here are the classes:
import RealmSwift
public class LocalizedStrings: Object {
dynamic var id = 1
dynamic var version: String = ""
let assets = List<LocalizedString>()
override public static func primaryKey() -> String? {
return "id"
}
}
public class LocalizedString: Object {
dynamic var key: String = ""
dynamic var value: String = ""
}
Here's how I update the LocalizedStrings object:
realm.add(localizedStrings, update: true)
Here's how I access my strings:
func getLocalizedString(forKey key: String) -> String {
var result = key
try! realm.write {
let queryResult = realm.objects(LocalizedString.self).filter("key == %#", key)
// print(queryResult)
if queryResult.count == 1 {
result = queryResult[0].value(forKey: "value") as! String
}
}
return result
}
Now, I would expect, that whenever I update my LocalizedStrings, that the localizedStrings.assets list would get updated with new values. But instead, the assets are not updated, the list reference gets updated and I end up having multiple instances of the same string, which is not what I would expect from an update function. When I try to access a particular LocalizedString, it turns out there's multiple instances:
(...)
[19] LocalizedString {
key = update;
value = Update;
},
[20] LocalizedString {
key = update;
value = Update;
}
Perhaps I'm missing something obvious and I would really appreciate if someone could point me in the right direction, so I'd be able to achieve the behavior I'm looking for (which would be having the nested object actually updated, rather than having unnecessary duplicates of my objects).
Thanks!
Ok, so this answer helped me figure out, what was wrong with my setup. I was missing primaryKey in LocalizedString class.
From the answer above on how realm.add(object, update: true) works:
Documentation :
parameter object: The object to be added to this Realm.
parameter update: If true, the Realm will try to find an existing copy of the object (with the same primary
key), and update it. Otherwise, the object will be added.
So the same thing happens with nested objects. They can not be updated unless they have primaryKey.

Filtering on RealmOptional removes values of nil, while CoreData predicate does not

I've ported code from CoreData to Realm and everything is working great aside from 1 final issue.
Filtering when using a RealmOptional is removing all objects that have a value equal to nil.
For instance, .filter("price <= 10.0") is removing from the results set, all objects whose price is nil. This behaviour did not happen in CoreData when using NSFetchedResultsController and NSPredicates, so I'm wondering if this is expected behaviour for Realm?
The Object is as follow a RealmOptional<Double> in the example below:
class Product : Object, Mapper
{
var price = RealmOptional<Double>() {
// Using ObjectMapper library to map JSON to Realm hence willSet
willSet {
self.price = newValue
}
}
}
I would expect results to return all Products that have price < 10.0 including those with nil values.
Is this expected behaviour or simply a bug?
That objects with null values are not included if you filter by a numeric comparison operator is the expected behavior. You can add OR price = nil if you'd like to include the objects where the price is nil. Like the following:
let free_or_cheap_products = realm.objects(Product)
.filter("product <= 10 || product = nil")

Realm: sorting by property in child object

My Show object looks like this:
class Show: RLMObject {
dynamic var venue: Venue?
}
and my Venue object:
class Venue: RLMObject {
dynamic var title = ""
}
I need to be able to sort my Show objects by their Venue object's titles. I tried the following but got an error:
allShowsByLocation = Show.allObjects().sortedResultsUsingProperty("venue.title", ascending: true)
The error is: Invalid sort column', reason: 'Column named '(null)' not found.
Realm doesn't yet support sorting RLMResults by a sub-property. As a work-around, you could query for Venues and return its linking object for each index:
allVenues = Venue.allObjects().sortedResultsUsingProperty("title", ascending: true)
func showAtIndex(index: UInt) -> Show {
return (allVenues[index] as Venue).linkingObjectsOfClass("Show", forProperty: "venue")
}
Or you could simply add a venueTitle property to your Show model which would then allow your query to work:
allShowsByLocation = Show.allObjects().sortedResultsUsingProperty("venueTitle", ascending: true)
You can also subscribe to GitHub issue #1199 to follow our progress on supporting sub-property sorting.
I'm very sad when Realm not support this feature.
I try another solution for this issue and it work well
copy all object in RLMResults to an array
Sort sub property of object in Array using compare

Resources