Realm Notification is not triggered when NSPredicate changes - swift

I have a simple ticked off list in Realm, where I am using NotificationToken to check for updates in the datasource and hereafter updates the tableView with the following items:
class Item : Object {
dynamic var name = ""
dynamic var isTickedOff = false
dynamic var timeStamp : Date?
}
The model:
var items: Results<Item> {
get {
let results = self.realm.objects(Item.self)
let alphabetic = SortDescriptor(property: "name", ascending: true)
let tickedOff = SortDescriptor(property: "isTickedOff", ascending: true)
let timestamp = SortDescriptor(property: "timeStamp", ascending: false)
return results.sorted(by: [tickedOff, timestamp, alphabetic]).filter("isTickedOff = %# || isTickedOff = %#", false, self.includeAll)
}
}
I have a switch in my tableview, where the user can change the self.includeAll property.
When inserting, deleting items or selecting them (resulting in setting them to isTickedOff or !isTickedOff) triggers the notification and updates the tableView. However, changing the self.includeAll property does not trigger the notification even though the items property is modified. I could include self.tableView.reloadData() when the user triggers the switch, but I would like the more smooth animations through the notification.
Is it me, who understands notifications wrong or is it a bug?
Thanks in advance!

Realm doesn't observe external properties, so it can't know when a property that is being used in a predicate query has changed, and to then subsequently generate a change notification off that.
When you access items the next time, that will give it a sufficient event to recalculate the contents, but by that point, it won't trigger a notification.
Obviously, like you said, the easiest solution would be to simply call tableView.reloadData() since that will force a refetch on items but there'll be no animation. Or conversely, like this SO question says, you can call tableView.reloadSections to actually enable a 're-shuffling' animation.
Finally, it might be a rather dirty hack, but if you still want it to trigger a Realm-based change notification, you could potentially just open a Realm write notification, change the includeAll property, and then close the write transaction to try and 'trick' Realm into performing an update.
One final point of note. I might not have enough information on this, so this might be incorrect, but if you're registering a Realm notification block off the items property, be aware that the way you've implemented that getter means that a new object is generated each time you call it. It might be more appropriate to mark that property as lazy so it's saved after the first call.

Related

Swift Combine Pipeline to compare draft model

I have a VielModel in SwiftUI handling my person model. To be able to store draft persons in the editor in the View(s), I have two objects:
#Published var person: Person
#Published var draftPerson: Person
In the UI, I am only changing the draftPersons until the user clicks on "Save", which stores the draftPerson as the person. In the onAppear method of the editor, I reset the draftPerson to the person.
Now I want to disable the "Save" button of the Editor and therefor introduced a bool "modified" in the VM. Using a pipeline, I want to set the modified to true, if and as long as the draftPerson is not equal to person, by doing the following:
$draftPerson.map { draftPerson in
return draftPerson != self.person
}
.assign(to: \.modified, on: self)
.store(in: &cancellables)
It looks like it is working on first glance, but if I change something in a textField, the value of modified is only set to true after the second change in the field. Vice versa, if I delete the typed values, it is only set back to false after I delete one more character as were originally there.
Question 1:
Is there another "best practice" to handle changes in draft objects and deactivating the "Save" button in SwiftUI?
Question 2:
Why is the pipeline "one change behind"?
Thanks a lot for your input.
Edit: I created a separate part of the App focusing only on the pipeline and realized that it is indeed working as intended if I remove my other pipelines. I have to check now in detail. Nevertheless, I will stick with my first question:
Is there anything I can do better?
Please find the code here on Github
You could declare another #Published property and combine the two person and draftPerson publishers and publish whether they are the same, like this:
#Published var person: Person
#Published var draftPerson: Person
#Published var saveDisabled: Bool = true
public init() {
// all init code
Publishers.CombineLatest($person, $draftPerson)
.map { $0 == $1 }
.assign(to: &$saveDisabled)
}
But essentially it is not needed and a computed property will do the same job:
var saveDisabled: Bool {
person == draftPerson
}
Because both person and draftPerson are marked #Published each time one of them changes the View will be notified of the change so it will also pick new value of saveDisabled.

How to tell NSPredicate skip entities before context.save()

How to prevent firing controller(:didChangeContentWith: diff) before viewContext was saved?
After creating an NSManagedObject object i'm getting controller(:didChangeContentWith: diff) fired, and after saving context too, so it fires twice for the same object.
First of all, I create NSFetchedResultsController and set fetchRequest.predicate
fetchedResultsController.fetchRequest.predicate = nil
Then when I add item:
let newItem = Item(context: context) //controller fires right after this #1
...
...
context.save() // Fires here too #2
First time objectID is wrong yet, second time it's correct and set up according to the context. So, is there any way to tell predicate to skip entities, that was created before context.save()?
The only way I've found for now is filtering results. It works for me because I have own custom logic of UI updating, not sure it will work for everyone.
lists = fetchLists().filter { !$0.objectID.isTemporaryID }
Where lists is [NSManagedObject] (subclass). So when CoreData was changed, I simply fetch with needed predicate and filter out all temporary IDs. When item gets correct ID, controller(:didChangeContentWith: diff) fires again and everything works fine.
Still looking for the way to fetch with skipping isTemporaryID objects.

RxSwift - change specific object in PublishSubject on event

struct Contact : Codable, Hashable {
var id : String
...
}
I use PublishSubject to feed the data to the UITableView
let contacts : PublishSubject<[Contact]> = PublishSubject()
And when the value is changed on the other view controller, I want to change the specific value in the array.
I want to change the Contact object with the specific id.
contacts.filter {$0.id == contactId}[0].someKey = someValue
How can I do this with RxSwift?
Understand that a PublishSubject doesn't contain any state so there is nothing in it that you can change. Instead, you emit a new array from the publish subject with a new contact that has the new value.
Somewhere in your code, you are calling onNext(_:) on the subject (or connecting it to an Observable that is doing that. We would need to see that code to help you solve your problem.

Add or remove item in datagrid does not trigger WhenAnyPropertyChanged

I am using dynamic data with reactiveui,
` _propList.Connect()
.WhenAnyPropertyChanged()
.Subscribe(t =>
{
}`
the code will be trigger if I just edit any item in the grid. However, when I try to add or remove an item, it is not triggered.
In my view model I have something like this
private SourceList<Decision> _myList { get; set; } = new SourceList<Decision>();
private readonly IObservableCollection<Decision> _targetCollection = new ObservableCollectionExtended<Decision>();
public IObservableCollection<Decision> TargetCollection => _targetCollection;
in my view, I simply
this.OneWayBind(VM, vm => vm.TargetCollection, v => v.DataGrid1.DataSource);
If I remove or Add item in the grid, and press Save
_myList.Count() didn't change, but
_TargetCollection.Count() will increase or decrease by number of items I delete
In my ViewModel
OKCmd = ReactiveCommand.Create(() =>
{
//// _myList.Connect()
////.Subscribe(t =>
//// {
//// ;
//// }
//// );
t.Items.count() and it is the initial load items, but I couldn't seem to know what items have been added or removed. Am I missing something.
Of course, I can keep track of what items are added or removed in the UI, but I am hoping I don't have to do that.
Thanks.
To help me answer your question, I need to better understand what you are trying to achieve but first I will explain what the default behaviour of DD is.
If you want add / remove events you need _propList.Connect().Subscribe(changes => ...). These are the collection changes and you will receive all collection change events including the initial load, but no inline changes.
By default, no property changes are wire in. This is because to monitor property changes is expensive and is opt in only. Also WhenAnyPropertyChanged() never tiggers for the initial load. This is because the item is already loaded and no properties have changed between Connect being called and the property changed observable being subscribed to.
Following on from 2, you will never receive a property changed when an item is removed from the underlying source. This is because when an item it removed, any inline subscriptions are disposed of. Otherwise there would be memory leaks.
Another option for monitoring inline changes is to make use of 'MergeMany' which allows you to craft any observable on a specific item, and in your case you can create an observable to return the initial value as well as as subsequent changes.
It is possible using standard rx to listen to collection changes and inline changes in a single observable, which you would have to compose yourself. For example
var myCollectionChanges = _propList.Connect();
var myPropertyChanges = _propList.Connect().WhenAnyPropertyChanged();
var allMyChanges = myCollectionChanges.Select(_ => Unit.Default)
.Merge(myPropertyChanges.Select(_ => Unit.Default));
In the this example, I have used Select(_ => Unit.Default) to enable the merge operator as it requires the same signature. However what signature is returned is up to you, the key point being that the signatures must match.

How can I undo a NSManagedObject deletion without using the built-in undo manager?

I'm building an OSX document-based app using CoreData and NSUndoManager.
I have a global NSUndoManager that handles the Undoing and Redoing, instead of using the default one that's built into each NSManagedDocument. This means that I register the undos and redos manually, instead of relying on them to be automatically registered when the Managed Object Context changes.
When I delete the NSManagedObject from the NSManagedObjectContext, I want to register an undo that restores the deleted object. I know this is possible, because the NSManagedDocument's built-in NSUndoManager would do it by default, but trying to use the object after it's been deleted throws an error. How can I restore the actual object during the undo?
func removeAnObject(object: NSManagedObject) {
self.managedObjectContext?.deleteObject(object)
self.project?.undoManager?.registerUndoWithTarget(self, handler: { (object) in
// how to undelete object???
self.project?.undoManager?.prepareWithInvocationTarget(self).removeAnObject(object)
})
}
Got it figured out. The easy answer:
self.project.undoManager?.insert(object)
The tricky part is, if you've saved the managed object context to a persistent store, all of the properties of "object" are blanked. If you save the values of those properties beforehand, though, you can reassign them after the new insert.
func removeAnObject(object: NSManagedObject) {
let property1 = object.property1
let property2 = object.property2
self.managedObjectContext?.deleteObject(object)
self.project?.undoManager?.registerUndoWithTarget(self, handler: { (object) in
self.managedObjectContext?.insertObject(object)
object.property1 = property1
object.property2 = property2
self.project?.undoManager?.prepareWithInvocationTarget(self).removeAnObject(object)
})
}