NSPredicate for filtering array of primitives field in Realm 3 - swift

We have to store elements in Realm that have array of string field.
To do this we have to implement small workaround:
class RealmString: Object {
#objc dynamic var value = ""
override init(value: Any) {
super.init(value: [value])
}
required init(realm: RLMRealm, schema: RLMObjectSchema) {
super.init(realm: realm, schema: schema)
}
required init() {
super.init()
}
required init(value: Any, schema: RLMSchema) {
super.init(value: [value], schema: schema)
}
}
class Realm2Element: Object {
let tags = List<RealmString>()
}
As a result:
CONTAINS ALL OF condition looks like this:
ANY tags.value == "tag0" AND ANY tags.value == "tag1" AND ANY tags.value == "tag2"
CONTAINS ANY OF condition looks like this:
ANY tags.value IN {"tag0", "tag1", "tag2"}
Realm3 suppors array of primitives. We removed .value keyPath from condition. But we have got error when applied these filters for Realm3Element:
class Realm3Element: Object {
let tags = List<String>()
}
For CONTAINS ALL OF condition:
'Invalid value', reason: 'Expected object of type (null) for property 'tags' on object of type 'Realm3Element', but received: tag0'
For CONTAINS ANY OF condition:
'Expected object of type (null) in IN clause for property 'tags' on object of type 'Realm3Element', but received: tag0'
We run through the all possible variations, but couldn't find any solution.
Does Realm3 support filtering by array of primitives field?

You can't achieve your goals using predicate with Realm because Realm have a lot of limitations using Predicates but you can use this way as a workaround
let filterArray = ["tag0","tag1"]
Array(realm.objects(Realm3Element.self)).filter({$0.tags.sorted().joined().contains(filterArray.sorted().joined())})
If you want a perfect solution tracking this issue #5334

Related

Instance method 'drive' requires the types 'NotificationItem' and '[NotificationItem]' be equivalent

I have create a class called notification Item and parsing the data from model class RTVNotification
import Foundation
import RTVModel
public class NotificationItem: NSObject {
public var id: String
public var title: String
public var comment: String
public var publishStartDateString: String
init(id: String,
title: String,
comment: String,
publishStartDateString: String) {
self.id = id
self.title = title
self.comment = comment
self.publishStartDateString = publishStartDateString
super.init()
}
}
extension NotificationItem {
static func instantiate(with notification: RTVNotification) -> NotificationItem? {
return NotificationItem(
id: notification.id,
title: notification.title,
comment: notification.comment,
publishStartDateString: notification.publishStartDateString)
}
}
ViewModel
public class SettingsViewModel: ViewModel {
var item = [NotificationItem]()
public var fetchedNotifications: Driver<NotificationItem> = .empty()
public var apiErrorEvents: Driver<RTVAPIError> = .empty()
public var notificationCount: Driver<Int> = .empty()
public func bindNotificationEvents(with trigger: Driver<Void>) {
let webService: Driver<RTVInformationListWebService> = trigger
.map { RTVInformationListParameters() }
.webService()
let result = webService.request()
apiErrorEvents = Driver.merge(apiErrorEvents, result.error())
notificationCount = result.success().map {$0.informationList.maxCount }
fetchedNotifications =
result.success()
.map {$0.informationList.notifications}
-----> .map {NotificationItem.instantiate(with: $0)}
}
}
Getting an Error saying that Cannot convert value of type '[RTVNotification]' to expected argument type 'RTVNotification'
What can i do to solve this.
The purpose of the map() function is to iterate over the elements of an input array and apply a transform function to each of those elements. The transformed elements are added to a new output array that is returned by map(). It's important to understand that the length of the output array is the same length as the input array.
For example:
let inputArray = ["red", "white", "blue"]
let outputArray = inputArray.map { $0.count } // outputArray is [3, 5, 4]
In your code, you are calling:
result.success().map { $0.informationList.notifications }
I don't know RxSwift at all, so I'm going to go into wild speculation here.
First, I don't know exactly what result.success() returns, but the fact you can call map() on it implies result.success() returns an array (which is weird, but ok we'll go with it).
Second, we know the array returned by result.success() contains elements that have an informationList property, and the informationList property has a property called notifications. My guess is that notifications, being plural, means the notifications property type is an array, probably [RTVNotification].
So this code:
result.success().map { $0.informationList.notifications }
Transforms the success() array into a new array. Based on my assumption that notifications is of type [RTVNotification], and further assuming the success() array contains only one element, I would expect the result of
result.success().map { $0.informationList.notifications }
To be an array of type [[RTVNotification]], i.e. an array with one element, where that element is an array of RTVNotifications.
You then feed that [[RTVNotification]] array into another map() function:
.map { NotificationItem.instantiate(with: $0) }
Recall from the start of this answer that map() iterates over the elements of arrays. Since the input array to this map is [[RTVNotification]], its elements will be of type [RTVNotification]. That's what the $0 is in your call - [RTVNotification]. But the instantiate(with:) function takes an RTVNotification, not an array of RTVNotification, thus you get the error:
Cannot convert value of type '[RTVNotification]' to expected argument type 'RTVNotification'
So what can you do to fix it?
I would probably do something like this (you'll have to tailor it to your use case):
guard let successResponse = webService.request().success().first else {
print("no success response received")
return nil // basically report up an error here if not successful
}
// get the list of notifications, this will be type [RTVNotification]
let notifications = successResponse.informationList.notifications
// Now you can create an array of `[NotificationItem]` like you want:
let notificationItems = notifications.map { NotificationItem.instantiate(with: $0) }
// do something with notificationItems...
The caveat to the above is if you need to iterate over each element in the success() array, then you could do that like this:
let successResponses = webService.result().success()
// successNotifications is of type [[RTVNotification]]
let successNotifications = successResponses.map { $0.informationList.notifications }
// successItems will be of type [[NotificationItem]]
let successItems = successNotifications.map { notifications in
notifications.map { NotificationItem.instantiate(with: $0) }
}
In other words, in this last case, you get back an array that contains arrays of NotificationItem.
Your problem is here:
fetchedNotifications: Driver<NotificationItem> should be fetchedNotifications: Driver<[NotificationItem]> and the line .map {NotificationItem.instantiate(with: $0)} needs another map You are dealing with an Observable<Array<RTVNotification>>. You have a container type within a container type, so you need a map within a map:
.map { $0.map { NotificationItem.instantiate(with: $0) } }
When your types don't match, you need to change the types.
Other issues with your code...
Drivers, Observables, Subjects and Relays should never be defined with var, they should always be lets. Objects that subscribe to your properties before the bind is called will connect to the .empty() observables and never get any values. This is functional reactive programming, after all.
Your NotificationItem type should either be a struct or all it's properties should be `let's.
Be sure to read and understand #par's answer to this question. He wrote a really good explanation and it would be a shame to waste that knowledge transfer.

Swift generic codable struct is hard to work with

I have this simple codable struct that represents a JSON response for many different API index endpoints:
struct JsonDocument<Resource: Mappable> : Codable {
var data: [Resource]
var cursor: String? // pagination cursor
}
Mappable looks something like this:
protocol Mappable : Codable {
static var dependencies: [MappableType] { get }
var id: Int64 { get set }
var deleted: Bool? { get }
var created_at: Date { get set }
var updated_at: Date { get set }
}
// this enum lets me associate core data entities to mappable structs (more info in comments)
#objc enum MappableType : Int16, CaseIterable {
case event = 1
case venue = 2
....
func getType() -> Mappable.Type {
switch self
... return EventResource.self
}
}
And a concrete type might look something like this:
struct EventResource : Mappable {
... id, deleted, etc
var name: String
static var dependencies: [SyncableType] { return [.venue] }
}
The problem is I'm now finding this JsonDocument structure hard to work with. For example, I need to create DownloadOperation's (subclass of Operation) for each entity type (EventResource, etc) and configure dependencies. I found I had to declare the DownloadOperation with a generic parameter like so DownloadOperation<Resource: Mappable> : Operation in order to pass the Mappable type to the JsonDocument. I.e., the mappable type can't be an init() argument because you can't do var type: Mappable.Type ...and then: JsonDocument<type>.
With DownloadOperation now taking a generic parameter, I can't for example loop over all my MappableType (that I define in an enum), create operations, and configure dependencies:
let operations = [DownloadOperation<???>]
I want to be able to do something like:
// pseudo-code
var operations = [DownloadOperation<???>]()
for mappable in MappableType.allCases {
operations.append(mappable.downloadOperation)
}
for operation in operations {
operation.Resource.configureDependencies(operations)
}
// add operations to queue
Eventually these structs get mapped to Core data objects in a separate MappingOperation subclass. The reason for dependencies is that the JSON responses are flat objects and only return foreign keys for relationships. For example, the /events endpoint would return venue_id's. Therefore, I need to run the DownloadOperation<VenueResource> operation before the DownloadOperation<EventResource> operation.
Any tips? I'm new to Swift. Thanks

Querying for a matching array property in Realm

Consider the following, using Realm Swift:
class Shelf : Object {
let products = List<Product>()
}
and
class Product : Object {
let string: String = ""
let Shelves = LinkingObjects(fromType: Shelf.self, property: "products")
}
Now the question is, is it possible to perform a query like:
"What are all the shelves that match EXACTLY a specific product list?" (no more, no less).
I could not find a way to compare an array.
EDIT:
If I try doing this:
let results = realm.objects(Shelf.self).filter("%# == products",products)
I get the error:
Invalid predicate:
Key paths that include an array property must use aggregate operations
Thank you.
Each Realm model class inheriting from Object conforms to the Equatable protocol by default due to the fact that Object inherits from NSObject, which conforms to Equatable. If the elements of a List conform to the Equatable protocol, the List itself conforms to it as well.
Hence, you should be able to compare List<Product> instances by using the == function and you can also directly compare Shelf objects.
However, be aware that Realm overrides the == function, see Realm object's Equatable is implementation.
If you want a custom comparison, you can override the == function yourself.
Edit: I don't think this can be done using Realms filter method. However, if you don't mind getting an Array back as a result and not a Result collection, the below method can help you.
let searchedProducts = List([product1, product2, product3]) //these are Product instances
let results = Array(realm.objects(Shelf.self)).filter{
if $0.products.count != searchedProducts.count {
return false
}
for product in $0.products {
if searchedProducts.index(of: product) == nil {
return false
}
}
return true
}
The above code has been tested in Playground with a similar class and returned correct results.

RealmSwift: Casting Object.subscript AnyObject? to List<>

I’m writing a custom update method to allow control over how values are represented in Realm. The approach taken is to match incoming key: values (from JSON) against objects’ objectSchema.properties and convert the values accordingly. Everything is general-purpose with values stored using Object.subscript.
For to-one relations, where property type is .Object the system can recurse and create or update the appropriate nested object of type determined by Property.objectClassName.
For to-many relations, where property type is .Array we must modify a List of objects of type objectClassName. List however is a generic so that’s List< concrete type of objectClassName >.
As Object.subscript returns values of type AnyObject? we need to cast this into something that can be treated as a list irrespective of its contained type. How can this be achieved?
(I've replaced the example below to better illustrate the problem)
e.g.
typealias ValueDictionary = Dictionary<String, AnyObject>
func update(object: Object, values: ValueDictionary) {
for property in object.objectSchema.properties {
if let value = values[property.name] {
var newValue: AnyObject?
switch property.type {
case .Object:
let objectType = NSClassFromString(
"mymodule" + property.objectClassName) as! Object.Type
newValue = relatedObjectWithType(objectType,
values: value as! ValueDictionary)
case .Array:
let objectType = NSClassFromString(
"mymodule" + property.objectClassName) as! Object.Type
newValue = listOfRelatedObjectsWithType(objectType,
values: value as! [ValueDictionary])
default:
// Convert primitive values, date strings, etc.
newValue = coerceValue(value, toPropertyType:property.type)
}
}
}
func relatedObjectWithClassName(type: Object.Type, values: ValueDictionary) -> Object {
// To-one relations can be instantiated using Object.Type.
var object = type.init()
update(object, values)
return object
}
// To-many relation are generic so there's no way to treat them dynamically.
func listOfRelatedObjectsWithType(type: Object.Type, values: [ValueDictionary]) -> List {
}

How to store optionals

As Realm doesn't support optionals, which are not Object subclasses, I'm trying to wrap a string into StringObject:
final class StringObject: Object {
dynamic var value: String = ""
convenience init?(_ value: String?) {
self.init()
if let value = value {
self.value = value
} else {
return nil
}
}
}
And use it like this:
final class Person: Object {
dynamic var firstName: String = ""
dynamic var lastName: StringObject? // can be optional
}
But this solution has a nasty side effect: as StringOptional values will be stored in their own table within the database, there will be countless duplicate values every time a StringObject is created. I tried making StringObject's value a primary key, but upon Person object's creation I receive an error:
Can't set primary key property 'lastName' to existing value 'Doe'
Which means that internally Realm does not upsert relationships.
Is there a better way to store optionals?
We actually released a beta of Realm that had support for optional strings and data properties, and it will hopefully be released more widely soon! In the meantime, you can try out the beta at https://github.com/realm/realm-cocoa/issues/628#issuecomment-106952727.