I'm having an issue with Realm subscriptions observers. The state is not updating after receiving data from the server. I'm using this code below:
let realm = try! Realm(configuration: try configuration(url: realmURL))
let results: Results<Object> = realm!.objects(Object.self)
let subscription = results.subscribe(named: "objects")
subscription.observe(\.state, options: .initial) { state in
print("Sync State Objects: \(state)")}
The only state I get is ".creating" and after that nothing more is updated. I would like to get ".completed" to be able to track progress of the subscription getting data.
Important to mention that I already tried to remove the options but in this case even ".creating" is not triggered.
Thanks
I will answer with some partial code as it will provide some directon to get this working. Assume we have a PersonClass, a tableView and a tableView dataSource called personResults. This was typed here so don't just copy paste as I am sure there are a couple of build errors.
In our viewController...
class TestViewController: UIViewController {
let realm: Realm
let personResults: Results<Person>
var notificationToken: NotificationToken?
var subscriptionToken: NotificationToken?
var subscription: SyncSubscription<Project>!
then later when we want to start sync'ing our personResults
subscription = personResults.subscribe()
subscriptionToken = subscription.observe(\.state, options: .initial) { state in
if state == .complete {
print("Subscription Complete")
} else {
print("Subscription State: \(state)")
}
}
notificationToken = personResults.observe { [weak self] (changes) in
guard let tableView = self?.tableView else { return }
switch changes {
case .initial:
// Results are now populated and can be accessed without blocking the UI
print("observe: initial load complete")
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)")
}
}
Related
I am using realm swift for my messaging app. My base view for messaging is a custom UICollectionView and my i use realm swift for data storage. Unfortunately i couldn't find any official example from realm for updating collection view with realm notification. So i implemented like this when i get messages from realm getting notificationToken by adding an observer for that realm list so when update comes from that notification i perform batch updates and delete, insert and modify indexes that realm told me to do in this notification. my app works well in testing environment but in production i got some crashes that reported my by fabric that told me app crashing and it is caused by this batch updates. their messages are mostly like
invalid update: invalid number of sections. The number of sections contained in the collection view after the update (1) must be equal to the number of sections contained in the collection view before the update (1), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted).
or
invalid number of items in section after update and so on .
I am so confused now. any ideas?
My code for doing this updates
notificationToken = dbmsgs?.observe { [weak self] (changes: RealmCollectionChange) in
switch changes{
case .initial:
print("intial")
self?.collectionView.reloadData()
case .update(_,let deletions,let insertions,let modifications):
self?.collectionView.performBatchUpdates({
if deletions.count > 0{
print("deletions count\(deletions.count)")
if (self?.collectionView.numberOfItems(inSection: 0) ?? 0) - deletions.count > 0 {
self?.collectionView.deleteItems(at: deletions.map { IndexPath(row: $0, section: 0) })
}
else{
self?.collectionView.deleteSections(IndexSet(integer: 0))
}
}
if insertions.count > 0{
print("insertaions count\(insertions.count)")
for index in insertions{
guard let lastdbmsg = self?.dbmsgs?[index] else{
return
}
let sectionIndex = 0
let itemIndex = ( self?.dbmsgs != nil) ? (self?.dbmsgs?.count)! - 1 : 0
if itemIndex == 0{
self?.collectionView.insertSections([0])
}
self?.collectionView.insertItems(at: [IndexPath(item: itemIndex, section: sectionIndex)])
if itemIndex != 0{
self?.collectionView.reloadItems(at: [IndexPath(row: itemIndex-1, section: 0)])
}
}
}
if modifications.count > 0{
self?.collectionView.reloadItems(at: modifications.map({ IndexPath(row: $0, section: 0)}))
}
},completion: { (_) in
})
case .error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
}
}
I currently have a Realm database with three objects:
class Language: Object {
let lists = List<List>()
}
class List: Object {
let words = List<Word>()
}
class Word: Object {
dynamic var name : String = ""
}
So the object relation is: Language -> List -> Word
I display these objects in a UITableView, in a LanguageTableViewController, ListTableViewController and a WordTableViewController, respectively.
To update the TableView when changes occur, I use Realm Notifications in the LanguageTableViewController:
languageToken = languages.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
self?.updateLanguages(changes: changes)
}
With the current method to update the TableView (method is based on tutorials from the Realm site):
func updateLanguages<T>(changes: RealmCollectionChange<T>) {
switch changes {
case .initial:
self.tableView.reloadData()
case .update(_, let deletions, let insertions, let updates):
self.tableView.beginUpdates()
self.tableView.insertRows(at: insertions.map {IndexPath(row: $0, section: 0)}, with: .automatic)
self.tableView.reloadRows(at: updates.map {IndexPath(row: $0, section: 0)}, with: .automatic)
self.tableView.deleteRows(at: deletions.map {IndexPath(row: $0, section: 0)}, with: .automatic)
self.tableView.endUpdates()
default: break
}
}
Now to my issue: This method works when new "Language" objects are added or updated. However, when a new List or Word object is created, this method is called too - with an insertion update. This leads to a UITableView Exception, because no Language objects were created, but the updateLanguages method wants to insert new cells.
My question is: How can I only monitor changes to the count/number of objects? Or is there a better approach to this issue?
The lifecycle I am testing is: Start App Online -> Add item -> go offline -> kill the app -> open app -> add item -> go online. If I try adding a new item after going online I get 'NSInternalInconsistencyException', reason: 'attempt to insert row 9 into section 0, but there are only 0 rows in section 0 after the update' Not sure why it believes there are only 0 rows in section 0.
Relevant Code:
let realm = try! Realm()
let owners = try! Realm().objects(OwnerList.self).first?.list
// Notification token defined in viewDidLoad()
self.notificationToken = self.owners?.addNotificationBlock { [unowned self] changes in
switch changes {
case .initial:
self.tableView.reloadData()
case .update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
self.tableView.beginUpdates()
self.tableView.insertRows(at: insertions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
self.tableView.deleteRows(at: deletions.map { IndexPath(row: $0, section: 0) }, with: .automatic)
self.tableView.reloadRows(at: modifications.map { IndexPath(row: $0, section: 0) }, with: .none)
self.tableView.endUpdates()
case .error(let error):
print("Notification Error", error)
break
}
}
// Helper Function when inserting item
func insertItem() throws {
self.realm.beginWrite()
let owner = Owner().generate()
self.owners?.insert(owner, at: (owners?.count)!)
self.tableView.insertRows(at: [[0, (owners?.count)!-1]], with: .automatic)
try self.realm.commitWrite(withoutNotifying: [self.notificationToken!])
}
You shouldn't insert rows until write transaction is committed because write transactions could potentially fail.
Try to move
self.tableView.insertRows(at: [[0, (owners?.count)!-1]], with: .automatic)
after realm.commitWrite(..).
Also make sure your tableview datasource methods return correct values.
Figured it out, make sure you set your tableview delegate and datasource in viewDidLoad() not viewWillAppear()
Realm Collection Notifications works fine while mapping with UITableView rows using 'map'. How do i achieve the same by mapping it to UITableView sections.
For rows I follow the below code:
notificationToken = results.addNotificationBlock { [weak self] (changes: RealmCollectionChange) in
guard let tableView = self?.tableView else { return }
switch changes {
case .Initial:
tableView.reloadData()
break
case .Update(_, let deletions, let insertions, let modifications):
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.endUpdates()
break
case .Error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}
}
For sections, I work with:
tableview.beginUpdates()
for insertIndex in insertions {
tableview.insertSections(NSIndexSet(index: insertIndex), withRowAnimation: .Automatic)
}
for deleteIndex in deletions {
tableview.deleteSections(NSIndexSet(index: deleteIndex), withRowAnimation: .Automatic)
}
for reloadIndex in modifications {
tableview.reloadSections(NSIndexSet(index: reloadIndex), withRowAnimation: .Automatic)
}
tableview.endUpdates()
And this works.
But I want to know about 'map' and how to use it to map sections.
tableView.insertSections(insertions.map { NSIndexSet(index: $0) }, withRowAnimation: .Automatic)
And also,
tableview.insertSections(insertions.map({ (index) -> NSIndexSet in
NSIndexSet(index: index)
}), withRowAnimation: .Automatic)
But, both gives me the same error
'map' produces '[T]', not the expected contextual result type 'NSIndexSet'
map returns a new collection by replacing each of the original collection elements with a mapped version of that same element. In other words:
insertions.map { ...}
returns an array, while tableView.insertSections expects a single NSIndexSet argument.
The closest you're going to get is:
for indexSet in insertions.map { NSIndexSet(index: $0) } {
tableView.insertSections(indexSet, ...)
}
Alternatively, you can create a NSIndexSet that's a conjunction of the individual elements using reduce, something like:
tableView.insertSections(insertions.reduce(NSMutableIndexSet()) {
$0.addIndex($1)
return $0
}, withRowAnimation: .Automatic)
But that really seems to be obscuring the code rather than clarifying it.
When my user logs out, I clear my realm with realm.deleteAll(). After this, I get a lot of notifications resulting in reads from Results objects, which results in an exception realm::Results::InvalidatedException, "RLMResults has been invalidated". I can't find a way to
check a Results object for invalidation directly;
check a Results' realm object for invalidation; or
get the List the Results is derived from in order to check its invalidation state.
I can't think of anything else to look for. If there's a better way to clear the database that won't result in exceptions all over the place, I'd be happy to hear about that too.
Additional information: the exception is thrown even when calling count on a Results object, not just accessing its objects.
You can check if the first object exist, from Swift Docs:
public var first: T? { return rlmResults.firstObject() as! T? }
Returns the first object in the results, or nil if empty.
From Realm Documentation for Java (could not find the same wording in Swift Docs):
Notice that a RealmResults is never null not even in the case where it contains no objects. You should always use the size() method to check if a RealmResults is empty or not.
Long story short, check if the first object exist of or try to count the elements.
Sources:
Java - Class RealmResults,
Swift - Results Class Reference
EDIT: Here is a code sample, it is taken from Realm example and modified to my needs, they use notification token to detect if the array is empty
class Record: Object {
dynamic var workoutName = ""
dynamic var totalTime = ""
dynamic var date = ""
}
let realm = try! Realm()
let array = try! Realm().objects(Record).sorted("date")
var notificationToken: NotificationToken?
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
// Set results notification block
notificationToken = array.addNotificationBlock { [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()
break
case .Update(_, let deletions, let insertions, let modifications):
// Query results have changed, so apply them to the UITableView
tableView.beginUpdates()
tableView.insertRowsAtIndexPaths(insertions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.deleteRowsAtIndexPaths(deletions.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.reloadRowsAtIndexPaths(modifications.map { NSIndexPath(forRow: $0, inSection: 0) },
withRowAnimation: .Automatic)
tableView.endUpdates()
break
case .Error(let error):
// An error occurred while opening the Realm file on the background worker thread
fatalError("\(error)")
break
}
}
}
I also clear the table using deleteAll():
func clearTable() {
try! realm.write {
realm.deleteAll()
}
}
Results now has an invalidated property, as of 1.0.3.
Source: https://github.com/realm/realm-cocoa/blob/v0.103.0/CHANGELOG.md