Firebase child listener on child moved - ios

I am trying to rearrange items in list through firebase child moved in firebase child listener. But the item to be moved replaces the item at the new position. The sorting is however than properly when I scroll up and down
query.observe(.childMoved) { dataSnapshot in
if let value = dataSnapshot.value as? [String: Any],
let index = self.friends.index(where: {$0.fuid == dataSnapshot.key}) {
let indexPath = IndexPath(item: index, section: 0)
let movedPost = friendsModel(snapshot: dataSnapshot)
self.friends.remove(at: index)
self.friends.insert(movedPost, at: 0)
self.tableView.reloadRows(at: [IndexPath(item: 0, section: 0)], with: .none)
self.tableView.reloadRows(at: [indexPath], with: .none)
}
}

Your .childMoved listener gets called with the DataSnapshot that was moved and the previousSiblingKey, which is the key of the item after which you need to reinsert the snapshot in your friends list. Since your code doesn't use previousSiblingKey, it doesn't have enough information to process the move correctly.
See the Firebase documentation: https://firebase.google.com/docs/reference/swift/firebasedatabase/api/reference/Classes/DatabaseQuery#observe_:andprevioussiblingkeywith:withcancel:

Related

iOS: Realm subscription state never updated

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)")
}
}

Updating selected collectionview indexes after responding to PHPhotoLibraryChangeObserver event

I am making a multiple selection feature for my collection view which shows photos from the user's library. I keep track of the selected indexPaths in an array and I want to update them in case a photo library change observer event happens in the middle of selecting cells. for example, if a user has selected indexes 3 and 4 and a change observer event removes indexes 1 and 2 from the collection view, selected indexes should change to 1 and 2.
I am trying to do it manually using these functions:
fileprivate func removeIndicesFromSelections(indicesToRemove:IndexSet){
var itemToRemove: Int?
for (_, removeableIndex) in indicesToRemove.map({$0}).enumerated() {
itemToRemove = nil
for (itemIndex,indexPath) in selectedIndices.enumerated() {
//deduct 1 from indices after the deletion index
if (indexPath.item > removeableIndex) && (indexPath.item > 0) {
selectedIndices[itemIndex] = IndexPath(item: indexPath.item - 1, section: 0)
} else if indexPath.item == removeableIndex {
itemToRemove = itemIndex
}
}
if let remove = itemToRemove {
selectedIndices.remove(at: remove)
disableDeleteButtonIfNeeded()
}
}
}
fileprivate func moveSelectedIndicesAfterInsertion (insertedIndices:IndexSet){
for (_, insertedIndex) in insertedIndices.map({$0}).enumerated() {
for (itemIndex,indexPath) in selectedIndices.enumerated() {
//add 1 to indices after the insertion index
if (indexPath.item >= insertedIndex) {
selectedIndices[itemIndex] = IndexPath(item: indexPath.item + 1, section: 0)
}
}
}
}
However, these are getting more complicated than I expected and I keep finding bugs in them. Is there any better way to handle this situation (such as any built in collection view capabilities) or I just have to come up with my own functions like above?
You're on the right path, but you should store a reference to what object the user actually selected, not where they selected it (since that can change).
In this case, you should keep a reference to the selected photos' identifiers (see docs) and then you can determine what cell/index-path should be selected. You can compare your selection array against your image datasource to determine what the most up-to-date index path is.
There is a solution provided by Apple. You can find more information in official documentation page:
Bacically you want to adopt PHPhotoLibraryChangeObserver and implement the following function:
func photoLibraryDidChange(_ changeInstance: PHChange) {
guard let collectionView = self.collectionView else { return }
// Change notifications may be made on a background queue.
// Re-dispatch to the main queue to update the UI.
DispatchQueue.main.sync {
// Check for changes to the displayed album itself
// (its existence and metadata, not its member assets).
if let albumChanges = changeInstance.changeDetails(for: assetCollection) {
// Fetch the new album and update the UI accordingly.
assetCollection = albumChanges.objectAfterChanges! as! PHAssetCollection
navigationController?.navigationItem.title = assetCollection.localizedTitle
}
// Check for changes to the list of assets (insertions, deletions, moves, or updates).
if let changes = changeInstance.changeDetails(for: fetchResult) {
// Keep the new fetch result for future use.
fetchResult = changes.fetchResultAfterChanges
if changes.hasIncrementalChanges {
// If there are incremental diffs, animate them in the collection view.
collectionView.performBatchUpdates({
// For indexes to make sense, updates must be in this order:
// delete, insert, reload, move
if let removed = changes.removedIndexes where removed.count > 0 {
collectionView.deleteItems(at: removed.map { IndexPath(item: $0, section:0) })
}
if let inserted = changes.insertedIndexes where inserted.count > 0 {
collectionView.insertItems(at: inserted.map { IndexPath(item: $0, section:0) })
}
if let changed = changes.changedIndexes where changed.count > 0 {
collectionView.reloadItems(at: changed.map { IndexPath(item: $0, section:0) })
}
changes.enumerateMoves { fromIndex, toIndex in
collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
to: IndexPath(item: toIndex, section: 0))
}
})
} else {
// Reload the collection view if incremental diffs are not available.
collectionView.reloadData()
}
}
}
}

UICollectionView perform batch updates crash with realm notification

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)")
}
}

Issue with Realm Notifications and UITableView

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?

Realm NSInternalInconsistencyException When Starting App Offline

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()

Resources