I have set an observer for a live result object.
let token = realm.objects(RealmObject.self).observe(on: realmQueue) { changeset in
print(changeset)
}
It works as expected for the most part, it triggers when I add/delete or edit RealmObject type objects in/to the realm db.
My issue happens when I try to replace the whole array of objects with a new array. Basically what I do is I get all objects of type RealmObjects delete them and then I add the new array of RealmObjects. Now the issues is if I do the delete and add in one write transaction the notification mechanism seems to break, it doesn't trigger for this operation. On the other hand if I separate the delete and add into different write transactions then everything works with notifications (except I get two notifications as expected in this case, but it's not what I want).
Am I missing something and doing something wrong ?
// triggers notifications
let objectsToDelete = realm.objects(RealmObject.self)
try realm.write {
if !objectsToDelete.isEmpty {
realm.delete(objectsToDelete)
}
}
try realm.write {
realm.add(objects)
}
// doesn't trigger notification
let objectsToDelete = realm.objects(RealmObject.self)
try realm.write {
if !objectsToDelete.isEmpty {
realm.delete(objectsToDelete)
}
realm.add(objects)
}
edit: After further investigation it seems that this only happens in a specific case. When replacing the items with the same number of items and same primary key (even though some other properties ore different).
class RealmObject: Object {
#objc dynamic var primary: String!
#objc dynamic var summary: String!
override class func primaryKey() -> String? {
return "primary"
}
}
extension RealmObject {
convenience init(summary: Int, uuid: String = UUID().uuidString) {
self.init()
self.primary = uuid
self.summary = "Object nr. \(summary)"
}
}
let sharedKeys = [UUID().uuidString, UUID().uuidString, UUID().uuidString]
let initialObjects = [RealmObject(summary: 0, uuid: sharedKeys[0]),RealmObject(summary: 1, uuid: sharedKeys[1]),RealmObject(summary: 2, uuid: sharedKeys[2])]
let replaceObjects = [RealmObject(summary: 3, uuid: sharedKeys[0]),RealmObject(summary: 4, uuid: sharedKeys[1]),RealmObject(summary: 5, uuid: sharedKeys[2])]
This rings bells (I worked on the Realm C# team 2015-2017).
I'm fairly certain it is because of optimisations around primary keys and their use within a transaction, combined with how the notification structure is built. Strictly speaking, it is a bug. You might expect these would be coming out as Delete+Add or as a Change record in the notification.
You are fighting a deep core assumption about how primary keys are used and optimisations that are intended to keep them working incredibly fast. I doubt strongly it will get fixed.
Switching to non-primary keys but indexed fields would probably get around this but complicate the rest of your code, sorry.
It's also possible you don't need these to be indexed at all, depending on your data volumes. Realm does some incredibly fast searching so I always recommend people try a non-indexed search first to see if that's enough.
Related
Say I am creating an object that takes two strings and acts like a dictionary.
class WordInDictionary: Object {
#objc dynamic var word: String = ""
#objc dynamic var meaning: String = ""
What should I do if I wanted to have some initial objects that get added to the database just once upon installation/update of the app?
Also, is there a way to make it so that just those initial objects can't be deleted?
"What should I do if I wanted to have some initial objects that get added to the database just once upon installation/update of the app?"
One option would be to have some code near the realm initialisation that checks if there are any WordInDictionary objects already in the realm - if not then add the required default objects.
E.g.
let realm = try! Realm()
if realm.objects(WordInDictionary.self).isEmpty
{
// Add required words here
}
"Also, is there a way to make it so that just those initial objects can't be deleted?"
I don't know of a way to make realm objects read-only. You'd have to implement this in code in some way, e.g. have a isDeletable boolean member which is true for every user-created object and false for your default members, then only delete those from realm.
E.g. for your deletion code:
func deleteWords(wordsToDelete: Results<WordInDictionary>)
{
try! realm.write
{
realm.delete(wordsToDelete.filter("isDeletable = true")
}
}
I have a lot of data stored in Core Data without any trouble. Now I need to add some additional data. Instead of creating an entity for all this new data, I have decided to use the fact that the object to be stored(and all its children) implement NSCoding, and rather store the object as a Transformable (NSObject). I have done this before (in Obj-c), but for some reason I can't get it to work this time.
Let's say I have a huge class named Event:NSObject,NSCoding which contains name, date, and a metric ton of additional variables. Then imagine you'd want the ability to let the user receive a notification a given number of days before the event starts. Like, let the user "watch" the event. I want to keep track of which events are being watched, and how long before I should send the notification. With this, I can get a list of all the "watched" events, and know how many days before the event they want a notification. This is just a poor example of the real situation, just bear with me. Don't think about the "notification" part, just storing the data.
I have my Event-object, now I have created a WatchEvent-entity in my CoreData-database which has two attributes: event:Transformable and days:Integer. Neither are optional.
My entire Event-class and all its children now implement NSCoding, so to store this in the database, I simply set it to the transformable attribute. Here you can see how I create the WatchEvent-object and put it in the database, and a function to get all WatchEvents from the DB, and a function to print out the contents of each WatchEvent.
func storeWatchEvent(someEvent:Event, numberOfDays:Int){
let watchEvent = WatchEvent(entity: NSEntityDescription.entity(forEntityName: "WatchEvent", in: managedObjectContext)!, insertInto: managedObjectContext)
watchEvent.days = numberOfDays //e.g 3
watchEvent.event = someEvent
saveContext()
}
func saveContext(){
if managedObjectContext.hasChanges {
do {
try managedObjectContext.save()
} catch {
let nserror = error as NSError
NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
func getWatchedEvents()->[WatchEvent]?{
return managedObjectContext.fetch(WatchEvent.fetchRequest())
}
func printOutAllWatchedEvents(){
if let watchedEvents = getWatchedEvents(){
watchedEvents.foreach{ (w) in
print("numberOfDays: ", w.days)
print("event: ", w.event)
}
}
}
func saveButtonClicked(){
storeWatchEvent(someEvent, numberOfDays:3)
// For testing, attempt to get all my events immediately, several times
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
}
func verifyButtonClicked(){
printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and `nil` for event.
}
Let's say I have two buttons. One "Save" and another "Verify". If I click "Save", I save the valid objects, and as you can see in the code, I immediately query the database for all stored WatchEvents, and print them out. At that time, everything looks good. If I click the "verify"-button, it should've printed out the same thing, but it has deleted my event. It manages to keep the days-attribute stored, but the event is deleted. Why?:(
It doesn't matter how long I wait to click the verify-button. If I click them nearly at the same time, this still happens.
I call printOutAllWatchedEvents in saveButtonClick, three times, and since it manages to return a valid event-object every time, I assume that the storing/NSCoding-part of this was successful?
But if I click the verify-button, which I assume will happen at least a few "run loops" later, the transformable event-object has been deleted..
I have no idea what's happening.
Why does it manage to return my valid events if I request them immediately after inserting them, but not if I request them later? Why does this only affect the transformable object? The integer days is correctly retained for all WatchEvents. And since I have marked event as not optional, how can it return nil and never give me any errors? There are no errors when I call save on the context, I have checked.
I figured it out.. It had nothing to do with CoreData, really. It was a faulty implementation of NSCoding. I never got any errors of any kind, so it was hard to figure out, especially with so many variables, but the problem was essentially this:
class Event:NSObject,NSCoding{
let hasBeer:Bool
func encode(with aCoder: NSCoder) {
aCoder.encode(hasBeer, forKey: "hasBeer")
}
required init?(coder aDecoder: NSCoder) {
//This line:
self.hasBeer = aDecoder.decodeObject(forKey: "hasBeer") as? Bool ?? false
//Should be like this:
self.hasBeer = aDecoder.decodeBool(forKey: "hasBeer")
//Because it is a primitive type Bool, and not an Object of any kind.
super.init()
}
}
I'm still not entirely sure why I experienced it like this though..
The encoding always succeeded, so the Event was stored in the database (even though it looked empty when inspecting the blob-field in the sqlite-file using Liya). It succeeded encoding because the functions are named the same for all types (Bool, Int, Double, and NSObject). They all work with aCoder.encode(whatever, forKey: "whatever"). However, when DEcoding them, you have to pick the right decoding-function, e.g decodeBool, decodeInt32, decodeObject etc.
As I stated, I have done this in the past, in Objective-C, where I had no trouble at all, because both the encoding and decoding-functions are named for the type ([aDecoder decodeBoolForKey:#"key"]; and [aCoder encodeBool:hasBeer, forKey:#"hasBeer"];), and threw compile-time-errors when using the wrong one.
I guess I would've picked up on this if I had some fatalError("something") in my required init? instead of just using return nil when something didn't go as planned.
I am trying to make a dictionary with the properties of a class of mine.
class SomeClass() {
var someString = "Hello, stackoverflow"
var someInt = 42 // The answer to life, the universe and everything
var someBool = true
func objectToDict() -> [String: String] {
var dict = [String: String]()
let reflection = Mirror(reflecting: self)
for child in reflection.children {
if let key = child.label {
dict[key] = child.value as? AnyObject
}
return dict
}
}
but objectToDict() is very slow. Is there a way to speed this up, or may be another approach to add the property values to a Dictionary?
I do not agree with most other users. Using reflection results less code, which means less time to develop, maintain, and test your product. With a well written library like EVReflection you don't need to worry about things behind the scene too much.
However, if performance is going to be a concern, do NOT use reflection based approaches at all. I'd say it's never really a problem in front-end development for me, especially in iOS, but it cannot be ignored in back-end development.
To see how slow it can be, I ran some test in Xcode. I'll write a blog about it, but generally speaking, getting Mirror is not the worst part (plus it may be possible to catch property list in memory), so replacing it with objc runtime wouldn't change the situation too much. In the other hand, setValue(_, forKey) is surprisingly slow. Considering that in real life you also need to perform tasks like checking dynamicType and so on, using the dynamic approach surely will make it 100+ times slower, which won't be acceptable for server development.
- Looping 1,000,000 times for a class with 1 `Int` property
- Getting mirror: 1.52
- Looping throw mirror and set `child.value`: 3.3
- Looping throw mirror and set `42`: 3.27
- Setting value `42`: 0.05
Again, for iOS I'll keep using it to save my time. Hopefully end customers won't care about whether it's 0.005 seconds or 0.0005 seconds.
Not only is that slow, it's also not a good idea: mirroring is for debug introspection only. You should instead construct the dictionary yourself. This ensures that you have the flexibility to store all the data in exactly the right way, and also decouples your Swift property names from the keys of the dictionary you're generating.
class SomeClass {
var someString = "Hello, stackoverflow"
var someInt = 42 // The answer to life, the universe and everything
var someBool = true
func objectToDict() -> [String: AnyObject] {
return ["someString": someString, "someInt": someInt, "someBool": someBool]
}
}
I'll make it short as possible.
I have an API request that I fetch data from (i.e. Parse).
When I'm getting the results I'm writing it to Realm and then adding them to a UICollectionView's data source.
There are requests that take a bit more time, which run asynchronous. I'm getting the needed results after the data source and collection view was already reloaded.
I'm writing the needed update from the results to my Realm database.
I have read that it's possible to use Realm's Results. But I honestly didn't understood it. I guess there is a dynamic and safe way working with collection views and Realm. Here is my approach for now.
This is how I populate the collection view's data source at the moment:
Declaration
var dataSource = [Realm_item]()
where Realm_item is a Realm Object type.
Looping and Writing
override func viewDidLoad() {
super.viewDidLoad()
for nowResult in FetchedResultsFromAPI
{
let item = Realm_item()
item.item_Title = nowResult["Title"] as! String
item.item_Price = nowResult["Price"] as! String
// Example - Will write it later after the collectionView Done - Async request
GetFileFromImageAndThanWriteRealm(x.image)
// Example - Will write it later after the collectionView Done - Async request
dataSource.append(item)
}
//After finish running over the results *Before writing the image data*
try! self.realm.write {
self.realm.add(self.dataSource)
}
myCollectionView.reloadData()
}
After I write the image to Realm to an already created "object". Will the same Realm Object (with the same primary key) automatically update over in the data source?
What is the right way to update the object from the data source after I wrote the update to same object from the Realm DB?
Update
Model class
class Realm_item: Object {
dynamic var item_ID : String!
dynamic var item_Title : String!
dynamic var item_Price : String!
dynamic var imgPath : String?
override class func primaryKey() -> String {
return "item_ID"
}
}
First I'm checking whether the "object id" exists in the Realm. If it does, I fetch the object from Realm and append it to the data source. If it doesn't exist, I create a new Realm object, write it and than appending it.
Fetching the data from Parse
This happens in the viewDidLoad method and prepares the data source:
var query = PFQuery(className:"Realm_item")
query.limit = 100
query.findObjectsInBackgroundWithBlock { (respond, error) -> Void in
if error == nil
{
for x in respond!
{
if let FetchedItem = self.realm.objectForPrimaryKey(Realm_item.self, key: x.objectId!)
{
self.dataSource.append(FetchedItem)
}
else
{
let item = Realm_item()
item.item_ID = x.objectId
item.item_Title = x["Title"] as! String
item.item_Price = x["Price"] as! String
let file = x["Images"] as! PFFile
RealmHelper().getAndSaveImageFromPFFile(file, named: x.objectId!)
self.dataSource.append(item)
}
}
try! self.realm.write {
self.realm.add(self.dataSource)
}
self.myCollectionView.reloadData()
print(respond?.count)
}
}
Thank you!
You seem to have a few questions and problems here, so I'll do my best.
I suggest you use the Results type as your data source, something like:
var dataSource: Results<Realm_item>?
Then, in your viewDidLoad():
dataSource = realm.objects(Realm_item).
Be sure to use the relevant error checking before using dataSource. We use an optional Results<Realm_item> because the Realm object you're using it from needs to be initialised first. I.e., you'll get something like "Instance member * cannot be used on type *" if you try declaring the results like let dataSource = realm.objects(Realm_item).
The Realm documentation (a very well-written and useful reference to have when you're using Realm as beginner like myself), has this to say about Results...
Results are live, auto-updating views into the underlying data, which means results never have to be re-fetched. Modifying objects that affect the query will be reflected in the results immediately.
Your mileage may vary depending on how you have everything set up. You could try posting your Realm models and Parse-related code for review and comment.
Your last question:
What is the right way to update the "object" from the Data Source after i wrote the update to same object from the Realm DB?
I gather you're asking the best way to update your UI (CollectionView) when the underlying data has been updated? If so...
You can subscribe to Realm notifications to know when Realm data is updated, indicating when your app’s UI should be refreshed for example, without having to re-fetch your Results.
The code is from a book. In terms of overall app architecture (MVC), it's part of the Model. The model has two main components:
An array of tags called tags
A dictionary of tag - query called searches
The app saves these pieces of data in the NSUserDefaults (iOS defaults system) and on iCloud. The following method is called when a change in iCloud is signaled. The parameter is an instance of NSNotification.userInfo
// add, update, or delete searches based on iCloud changes
func performUpdates(userInfo: [NSObject: AnyObject?]) {
// get changed keys NSArray; convert to [String]
let changedKeysObject = userInfo[NSUbiquitousKeyValueStoreChangedKeysKey]
let changedKeys = changedKeysObject as! [String]
// get NSUbiquitousKeyValueStore for updating
let keyValueStore = NSUbiquitousKeyValueStore.defaultStore()
// update searches based on iCloud changes
for key in changedKeys {
if let query = keyValueStore.stringForKey(key) {
saveQuery(query, forTag: key, saveToCloud: false)
} else {
searches.removeValueForKey(key)
tags = tags.filter{$0 != key}
updateUserDefaults(updateTags: true, updateSearches: true)
}
delegate.modelDataChanged() // update the view
}
}
My question is on the if - else inside the for loop. The for loop iterates over keys that where changed; either the user adds a new search, updates an existing search, or deletes a search. But, I don't understand the logic behind the if-else. Some clarifying thoughts would be appreciated. I've read it over and over but it doesn't tick with me.
if let query = keyValueStore.stringForKey(key)
means that if keyValueStore contains a string corresponding to key, then this string will be assigned to the constant query.
This is called "safe unwrapping":
inside the if let ... condition, the query is safely saved with saveQuery because using if let ... guarantees that the value of keyValueStore.stringForKey(key) won't be nil.
If the value is nil, then in the else branch, the filter method is used to update the tags array without the key we just processed: tags.filter{$0 != key} means "return all items in tags that are different from key" (the $0 represents the current item from the array processed by filter).