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.
Related
something goes wrong when trying to update rows of tableview after delete of Firebase data.
Below is method I use.
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let delete = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
let cell = self.messages[indexPath.row]
let b = cell.msgNo
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
self.messages.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
} else {
print("snapshot empty")
}}) }
...
return [delete, edit, preview]
}
Initially I checked whole logic without including line /*action.child(autoId as! String).removeValue()*/ then it works normally and removes rows as should be. But once I add this line it removes data from Firebase but tableview is updated in strange way by adding new rows below existing
My guess is that somewhere else in your application you have code like action .observe(.value, which shows the data in the table view. When you delete a node from the database, the code that populates the database gets triggered again, and it adds the same data (minus the node that you removed) to the table view again.
When working with Firebase it's best to follow the command query responsibility segregation principle, meaning that you keep the code that modifies the data completely separate from the flow that displays the data. That means that your code that deletes the data, should not try to update the table view. So something more like:
let action = MyGlobalVariables.refMessages.child(MyGlobalVariables.uidUser!)
action.queryOrdered(byChild: "msgNo").queryEqual(toValue: b).observe(.childAdded, with: { snapshot in
if snapshot.exists() { let a = snapshot.value as? [String: AnyObject]
let autoId = a?["autoID"]
action.child(autoId as! String).removeValue()
} else {
print("snapshot empty")
}}) }
All the above does is remove the selected message from the database.
Now you can focus on your observer, and ensuring it only shows the messages once. There are two options for this:
Always clear self.messages when your .value completion handler gets called before you add the messages from the database. This is by far the simplest method, but may cause some flicker if you're showing a lot of data.
Listen to the more granular messages like .childAdded and .childRemoved and update self.messages based on those. This is more work in your code, but will result in a smoother UI when there are many messages.
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.
I have a UITableView subclass that I am populating from a Firestore collection. I only grab 20 documents at a time, and use the UITableViewDataSourcePrefetching delegate to get the next "page" of 20 documents from Firestore when the user nears the end of the loaded array.
The following is my code that almost perfectly achieves this, omitting/obfuscating where appropriate.
class MyCustomViewController: UIViewController {
#IBOutlet weak var myCustomTableView: MyCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
myCustomTableView.query = Firestore.firestore().collection("MyCollection").whereField("foo", isEqualTo: "bar").order(by: "timestamp", descending: true)
myCustomTableView.fetchNextPage()
}
}
extension MyCustomViewController: UITableViewDataSourcePrefetching {
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
guard let tableView = tableView as? MyCustomTableView else {
return
}
tableView.prefetch(indexPaths: indexPaths)
}
}
class MyCustomTableView: UITableView {
var documents = [DocumentSnapshot]()
var query: Query?
let querySize = 20
private var fetchSemaphore = DispatchSemaphore(value: 1)
public func prefetch(indexPaths: [IndexPath]) {
for indexPath in indexPaths {
if indexPath.section >= documents.count - 1 {
fetchNextPage()
return
}
}
}
public func fetchNextPage() {
guard let query = query else {
return
}
guard documents.count % querySize == 0 else {
print("No further pages to fetch.")
return
}
guard fetchSemaphore.wait(timeout: .now()) == .success else { return }
if self.documents.isEmpty {
query.limit(to: querySize).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
self.documents.append(contentsOf: snapshot.documents)
self.reloadData()
self.fetchSemaphore.signal()
}
}
else {
// I think the problem is on this next line
query.limit(to: querySize).start(afterDocument: documents.last!).addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
return
}
for document in snapshot.documents {
if let index = self.documents.firstIndex(where: { $0.documentID == document.documentID }) {
self.documents[index] = document
}
else {
self.documents.append(document)
}
}
self.reloadData()
self.fetchSemaphore.signal()
}
}
}
}
I think I know what/where the problem is (see comment in fetchNextPage()), but I may be wrong. Even if I am correct, I have not been able to come up with a way to fix it.
My query is sorted, in descending order, by the documents' timestamp value which, as the name suggests, represents the time at which the document was created. This means that, in the table view, the newest documents will appear at the top, and the oldest at the bottom.
Everything works great, except for...
The problem: When a new document is created, every item in the table gets shunted down a row because the new document has the newest timestamp and gets placed at the top. Great, except that when this happens, all of the query snapshot listeners (except for the first one) are no longer using the correct document to start after. The document snapshot originally retrieved with documents.last! is no longer the correct document that the query should start after. I do not know how to change the startAfter parameter of an existing snapshot listener. I could not find any method belonging to the query that I could use to alter this.
Again, this might not actually be the reason I'm seeing things out of order when new documents are added, but I think it is. If anyone has advice for how to resolve this I'd greatly appreciate it.
References I have used to get here:
https://firebase.google.com/docs/firestore/query-data/query-cursors
https://www.raywenderlich.com/5786-uitableview-infinite-scrolling-tutorial
Additional notes:
Even though it'd be much easier and solve this problem, I do not want to use getDocuments on the query. Having the near-real-time updates is important in this case.
I cannot use FirebaseUI because my use-case requires functionality not yet available. For example, you may have noticed that I used indexPath.section instead of indexPath.row, which is because the table is made up of many single-rowed sections so that vertical padding can be put between cells.
I have a single view of SlackTextViewController which works as a UITableView. I'm switching between "states" using a public String allowing it to read 1 set of data in a certain state and another set of data in another state. The problem is when I switch back and forth to the original state it prints the data 2, 3, 4 times; as many as I go back and forth. I'm loading the data from a Firebase server and I think it may be a Firebase issue. Here is my code...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if chatState == "ALL"
{
self.globalChat()
}else if chatState == "LOCAL"
{
self.localChat()
}
}
override func viewDidDisappear(animated: Bool) {
self.messageModels.removeAll()
tableView.reloadData()
ref.removeAllObservers()
}
func chatACTN()
{
if chatState == "ALL"
{
self.viewWillAppear(true)
}else if chatState == "LOCAL"
{
self.viewWillAppear(true)
}
}
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
let globalRef = ref.child("messages")
globalRef.keepSynced(true)
globalRef.queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
The problem is that each time you call globalChat() you're creating another new observer which results in having multiple observers adding the same items to self.messageModels. Thats why you're seeing the data as many times as you switch to the global state.
Since you want to clear the chat and load the last 100 each time you switch to global, there's no point in keeping the observer active when you switch to "Local".
Just remove the observer when you switch to Local, that should fix your problem.
From firebase docs:
- (void) removeAllObservers
Removes all observers at the current reference, but does not remove any observers at child references.
removeAllObservers must be called again for each child reference where a listener was established to remove the observers.
So, ref.removeAllObservers() will not remove observers at ref.child("messages") level.
Using ref.child("messages").removeAllObservers at the beginning of localChat function to remove the observer you created in globalChat would be ok if you're only dealing with this one observer at this level but if you have more on the same level or you think you might add more in the future the best and safest way would be to remove the specific observer you created. To do that you should use the handle that is returned from starting an observer. Modify your code like this:
var globalChatHandle : FIRDatabaseHandle?
func globalChat()
{
self.messageModels.removeAll()
tableView.reloadData()
ref.child("messages").keepSynced(true)
globalChatHandle = ref.child("messages").queryLimitedToLast(100).observeEventType(.ChildAdded, withBlock: { (snapshot) -> Void in
if snapshot.exists()
{
let names = snapshot.value!["name"] as! String
let bodies = snapshot.value!["body"] as! String
let avatars = snapshot.value!["photo"] as! String
let time = snapshot.value!["time"] as! Int
let messageModel = MessageModel(name: names, body: bodies, avatar: avatars, date: time)
self.messageModels.append(messageModel)
self.messageModels.sortInPlace{ $0.date > $1.date }
}
self.tableView.reloadData()
})
}
func localChat()
{
if globalChatHandle != nil {
ref.child("messages").removeObserverWithHandle(globalChatHandle)
}
self.messageModels.removeAll()
tableView.reloadData()
print("LOCAL")
}
And in viewDidDisappear method replace this
ref.removeAllObservers()
with
ref.child("messages").removeAllObservers()
To my knowledge, the following code (or very close to it) would retrieve one cloudkit instance from the recordtype array...
let pred = NSPredicate(value: true)
let query = CKQuery(recordType: "Stores", predicate: pred)
publicDatabase.performQuery(query, inZoneWithID: nil) { (result, error) in
if error != nil
{
print("Error" + (error?.localizedDescription)!)
}
else
{
if result?.count > 0
{
let record = result![0]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.txtDesc.text = record.objectForKey("storeDesc") as? String
self.position = record.objectForKey("storeLocation") as! CLLocation
let img = record.objectForKey("storeImage") as! CKAsset
self.storeImage.image = UIImage(contentsOfFile: img.fileURL.path!)
....(& so on)
However, how and when (physical location in code?) would I query so that I could set each cell to the information of each instance in my DiningType record?
for instance, would I query inside the didreceivememory warning function? or in the cellforRowatIndexPath? or other!
If I am misunderstanding in my above code, please jot it down in the notes, all help at this point is valuable and extremely appreciated.
Without a little more information, I will make a few assumptions about the rest of the code not shown. I will assume:
You are using a UITableView to display your data
Your UITableView (tableView) is properly wired to your viewController, including a proper Outlet, and assigning the tableViewDataSource and tableViewDelegate to your view, and implementing the required methods for those protocols.
Your data (for each cell) is stored in some type of collection, like an Array (although there are many options).
When you call the code to retrieve records from the database (in this case CloudKit) the data should eventually be stored in your Array. When your Array changes (new or updated data), you would call tableView.reloadData() to tell the tableView that something has changed and to reload the cells.
The cells are wired up (manually) in tableView(:cellForRowAtIndexPath:). It calls this method for each item (provided you implemented the tableView(:numberOfRowsInSection:) and numberOfSectionsInTableView(_:)
If you are unfamiliar with using UITableView's, they can seem difficult at first. If you'd like to see a simple example of wiring up a UITableView just let me know!
First, I had to take care of the typical cloudkit requirements: setting up the container, publicdatabase, predicate, and query inputs. Then, I had the public database perform the query, in this case, recordtype of "DiningType". Through the first if statement of the program, if an error is discovered, the console will print "Error" and ending further action. However, if no run-time problem is discovered, each result found to be relatable to the query is appended to the categories array created above the viewdidload function.
var categories: Array<CKRecord> = []
override func viewDidLoad() {
super.viewDidLoad()
func fetchdiningtypes()
{
let container = CKContainer.defaultContainer()
let publicDatabase = container.publicCloudDatabase
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "DiningType", predicate: predicate)
publicDatabase.performQuery(query, inZoneWithID: nil) { (results, error) -> Void in
if error != nil
{
print("Error")
}
else
{
for result in results!
{
self.categories.append(result)
}
NSOperationQueue.mainQueue().addOperationWithBlock( { () -> Void in
self.tableView.reloadData()
})
}
}
}
fetchdiningtypes()