Hy, I'm trying to couple RealmNotifications with updating a tableView but for some reason this keeps generating multiple crashes on the tableView because of inconsistency between the number of sections that exist and what the realm notification has sent. This is the code I have for observing any changes on the Results<T>:
do {
let realm = try Realm()
sections = realm.objects(AssetSections.self).filter("isEnabled = true AND (assets.#count > 0 OR isLoading = true OR banners.#count > 0 OR tag == 'My Tools')").sorted(byKeyPath: "sort_order", ascending: true)
guard let sections = sections else { return }
// Watch on the asset sections
notificationToken = sections.observe { [weak self] (change: RealmCollectionChange) in
switch change {
case .initial: break
case .error(let error):
self?.handle(error: error)
case .update(_, let deletions, let insertions, let modifications):
self?.updatedModel.onNext((insertions: insertions, modifications: modifications, deletions: deletions))
}
}
} catch { }
The above code occurs on a ViewModel and a ViewController is observing those changes like so:
vm.updatedModel
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.beginUpdates()
self.tableView.insertSections(insertions, animationStyle: .none)
self.tableView.deleteSections(deletions, animationStyle: .none)
self.tableView.reloadSections(modifications, animationStyle: .none)
self.tableView.endUpdates()
})
.disposed(by: disposeBag)
I'm working with sections instead of Rows because this is a tableView with multiple sections and just one row per section.
The crash I'm getting is if I do a pull to refresh which does multiple network calls which in turn makes multiple changes to the objects. The funny thing is I can always almost replicate the crash if I scroll down rapidly during a pull to refresh. The error is the following:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete section 14, but there are only 14 sections before the update'
The way I get the numberOfSections for the tableView is the following:
var numberOfSections: Int {
return sections?.count ?? 0
}
My suspicion is that since the notifications are getting delivered on the next runLoop of the Main Thread and since I'm making the thread busy by scrolling and messing with the UI by the time I get a notification and the tableView reacts to it, it's already out of sync. But I'm not exactly sure if this is the problem or if it is how to solve it.
Thank you
Edit
One way to avoid this is just .reloadData() on the tableView but it's a perfomance hit especially on big datasets, and I can't use the default tableView animations. To diminish the perfomance hit of calling .reloadData() multiple times I'm using debounce.
vm.updatedModel
.debounce(1.0, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] (insertions, modifications, deletions) in
guard let `self` = self else { return }
self.tableView.reloadData()
})
.disposed(by: disposeBag)
Great question, and it looks like the Realm documentation does exactly as you do (with rows rather than sections) but doesn't address this. I can't see an obvious answer, but best I can do is these possible workarounds (although tbh, neither is great).
Rework the update code to pull together results, then do a single commit when they are complete.
Make a static copy of sections to use locally, and update it within the subscription. i.e. copy the RealmCollection to an array, which then is not a dynamic view into the realm. This will stay synchronised to your updates.
Best I can do. Otherwise, I can't see how you can guarantee synchronisation between the dynamic query and the notification.
Related
I am trying to execute an asynchronous request as part of a search result updater in my app.
I wrote the following code
func updateSearchResults(for searchController: UISearchController) {
guard let text = searchController.searchBar.text else {return}
let threadingContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
threadingContext.parent = self.context
DispatchQueue.global(qos: .userInitiated).async {
let fetchRequest = MyObject.fetchRequest() as NSFetchRequest<MyObject>
fetchRequest.predicate = get_predicate(text)
do {
let objects = try threadingContext.fetch(fetchRequest).map({ object in
return object.objectID
})
}
catch {return}
DispatchQueue.main.async {
// Pass results to the search view controller
}
}
}
but the UI is still slow (even if I don't do any display update), and looking at the Time profiler, I see that my main thread is spending 80% of its time on the following:
So it seems that my request is still being dispatched onto the main thread, which I don't understand. Would anyone see my mistake?
(I tried a few various on the above e.g. using threadingContext.perform but for the same result)
Ok, I understood it, and I should have read Apple's documentation, but basically
If a context’s parent store is another managed object context, fetch and save operations are mediated by the parent context instead of a coordinator.
This is slightly subtle, but my construction would have been useful if the operations performed on the fetch request, rather than the fetch request itself, had been slow.
The solution is to set threadingContext.persistentStoreCoordinator instead.
I have a typical messaging app where messages are stored as Realm objects. I want to display messages of a conversation in a collection/table view in a safe way observing the results
let results = realm.objects(Message.self).filter(predicate)
// Observe Results Notifications
notificationToken = results.observe { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}),
with: .automatic)
tableView.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }),
with: .automatic)
tableView.endUpdates()
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
Now, this would work assuming that I display all messages. Since they can be a lot I need to load paginated.
How can I track changes then?
I'm searching for a method to get a sort of id of the changed message, but I couldn't find anything.
Realm objects are lazily loaded so they don't 'take up space' until they are accessed. In our case we have have 1000 objects in results but only display 10 at a time. Those are the 10 that 'take up space'. So it may not be an issue to have a large results dataset.
When you populate a results object from Realm, each object has an index. Think of a results as an array. The first object is index 0, the second object in index 1 etc.
When an object is modified in realm, that information is passed to your observer to which you can then update your UI.
Say we have a PersonClass Realm object that has a persons name and email
PersonClass: Object {
#objc dynamic var name = ""
#objc dynamic var email = ""
}
and we want to display a list of people, and if an email address changes we want to update the UI with that change.
When the app starts, we load all of the people into a Results class var.
override func viewDidLoad() {
super.viewDidLoad()
self.personResults = realm.objects(PersonClass.self)
Then we add an observer to those results so the app is notified of changes. .initial will run when the results have been loaded so it's a good place to populate your dataSource and refresh your tableView.
func doObserve() {
self.notificationToken = self.personResults!.observe { (changes: RealmCollectionChange) in
switch changes {
case .initial: // initial object load complete
if let results = self.personResults {
for p in results {
print(p.name, p.email) //populate datasource, refresh
}
}
break
case .update(_, let deletions, let insertions, let modifications):
for index in deletions {
print(" deleted object at index: \(index)")
}
for index in insertions {
print(" inserted object at index: \(index)")
}
for index in modifications {
print(" modified object at index: \(index)")
let person = self.personResults![index]
print("modified item: \(person.name) \(person.email)")
}
break
case .error(let error):
fatalError("\(error)")
break
}
}
}
In this example, when an person stored at index #2 changes his email, the observer responds to that and prints the name and the new email to console.
But...
Realm is live updating and if you refresh your tableView or even just that row so the cell re-loads from the object, the UI will be updated. I don't know what 'How can I track changes then?' means in your use case, but you could actually remove all of the for loops and just have a tableView.reloadData in the .update section and the UI will be updated as data changes. Or, you could use the index and just update that row.
Keep in mind Realm objects in Results are live and always stay fresh as data changes.
Another note is that many Realm objects will have a unique identifier for the object, defined like this
class PersonClass: Object {
#objc dynamic var person_id = UUID().uuidString
override static func primaryKey() -> String? {
return "person_id"
}
}
which can be used to access that specific object within Realm, but not directly related to your question.
I get this error:
assert(proxy === DelegateProxy.currentDelegate(for: object), "Proxy
changed from the time it was first set.
Original: (proxy)
Existing:
(String(describing: DelegateProxy.currentDelegate(for: object)))")
I have two observables that handles to xib storyboard on different states but once one of them has been loaded I receive the error above, I have tried to use the self.tableView.delegate = nil & self.tableView.dataSource = nil but yet it causes this on the .bind(to:) function. My problem is I don't know how to handle it before this error:
Assertion failed: Proxy changed from the time it was first set.
Original:
.asObservable().bind(to:(tableView?.rx.items(cellIdentifier:
aTableViewCell.Identifier, cellType: HaTableViewCell.self))!
func initTableView() {
// pull to refresh
aViewModel
.isLoading
.asObservable()
.subscribe({ (loading) in
if loading.element == false {
} else {
self.tableView!.delegate = self // loads a shimmering view
self.tableView!.dataSource = self
}
})
.disposed(by: disposeBag)
aViewModel
.datas
.asObservable()
.bind(to:(tableView?.rx.items(cellIdentifier: aTableViewCell.Identifier, cellType: HaTableViewCell.self))!) { (index, element, cell) in
// when the data is fired load this tableview cell
}.disposed(by: disposeBag)
}
How can set the the tableview to nil before the data Observables fires up?
This is a feature to warn you that there is already a delegate (or data
source) set somewhere previously. The action you are trying to perform ?
will clear that delegate (data source) and that means that some of your ?features that depend on that delegate (data source) being set will likely stop working. If you are ok with this, try to set delegate (data source) to nil in front of this operation.
It is not entirely clear to me what you are trying to achieve.
In general, you should not set 'tableView.dataSource' or 'tableView.delegate' when using RxDataSources with it.
If you want to bind different observables as datasource to the same tableView, '.switchLatest()' operator is what you need.
Every time you associate to a delegate via RxCocoa, you need to make sure you clean that disposable in order to reuse with a different one.
In swift you don't need to set to nil, pointing to other memory slot will do the work.
func refreshTableView() {
disposable = DisposeBag()
// pull to refresh
aViewModel
.datas
.asObservable()
.bind(to:(tableView?.rx.items(cellIdentifier: aTableViewCell.Identifier, cellType: HaTableViewCell.self))!) { (index, element, cell) in
// when the data is fired load this tableview cell
}.disposed(by: disposeBag)
}
I am implementing this drop down menu from cocoaPod. It is pretty easy to implement and I got it to work.
https://github.com/PhamBaTho/BTNavigationDropdownMenu
However, as per instruction, I have implemented the following functions in viewDidLoad
self.navigationItem.titleView = menuView
menuView.didSelectItemAtIndexHandler = {[weak self] (indexPath: Int) -> () in
print("Did select item at index: \(indexPath)")
if indexPath == 0 {
print("Closest")
self?.sortByDistance()
} else if indexPath == 1 {
print("Popular")
self?.sortByRatings()
} else if indexPath == 2 {
print("My Posts")
self?.myPosts()
} else {
}
I am abit concerned as Xcode is telling me to put a ? or a ! just after self which was never done in other places of my program. Could someone please advise if this is totally acceptable or is there a better way of doing it? It just seems odd force unwrapping or putting my VC as optional...?
The whole point of {[weak self] ... is that the controller may be released and you don't want this block to strongly capture it and keep it in memory if it has been released by whatever presented it. As such the reference to self may be nil.
So, you definitely don't want to use !, and you should either user ? or and if let check.
I’m using NSFetchedResultsController and DATAStack. My NSFetchedResultsController doesn’t update my table if I make any changes in another contexts.
I use dataStack.mainContext in my NSFetchedResultsController. If I do any updates from mainContext everything is okay. But if I write such code for example:
dataStack.performInNewBackgroundContext({ (backgroundContext) in
// Get again NSManagedObject in correct context,
// original self.task now in main context
let backTask = backgroundContext.objectWithID(self.task.objectID) as! CTask
backTask.completed = true
let _ = try? backgroundContext.save()
})
NSFetchedResultsController doesn’t update cells. I will see updated data if I reload application or whole view for example.
There is no error in data saving (I’ve checked).
I’ve tried to check NSManagedObjectContextDidSaveNotification is received, but if I do .performFetch() after notification is received I again get old data in table.
Finally I’ve got it, it’s not context problem.
I’ve used such type of code in didChangeObject func in NSFetchedResultsControllerDelegate if NSFetchedResultsChangeType is Move (that event was generated because of sorting I think):
guard let indexPath = indexPath, newIndexPath = newIndexPath else { return }
_tableView?.moveRowAtIndexPath(indexPath, toIndexPath: newIndexPath)
And that’s incorrect (I don’t know why NSFetchedController generates move event without «reload data event» if data was changed), I’ve replaced it with next code and everything now is working.
guard let indexPath = indexPath, newIndexPath = newIndexPath else { return }
_tableView?.deleteRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.Fade)
_tableView?.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: UITableViewRowAnimation.Fade)
It isn't documented terribly well what the different background context options should be used for and how. Likely worth raising an issue / submitting a PR.
Your issue is that performInNewBackgroundContext doesn't merge changes into the main context. This is because the context hierarchy isn't prescribed, so you can choose what you want to do.
So, you can either monitor the save and merge the changes yourself. Or you could set the parent context. Or you could use newBackgroundContext (which does merge changes automatically, even though that's a bit weird...).