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)")
}
}
Assume i have two flows:
1) I have data in database, then i use fetch entity with params from database and set and observer. After that i load data from server and observer's block fires successfully. That's fine.
2) I don't have data in database. Then i try to do the same, it looks like:
myObject = MyRealmService()
.fetchAll(MyRealmObject.self,
filter: "userID == \(someID)")?
.first
realmToken = myObject?.observe { [weak self] change in
guard let _self = self else { return }
switch change {
case .deleted:
_self.popCurrentViewController()
case .error(let error):
_self.show(error: error)
case .change:
_self.updateUI()
}
}
loadDataFromServer() { object in
object.saveToRealm()
}
Then myObject is nil, so the notification block don't setting.
What is the way to handle notifications in this way? I mean, maybe somehow we can set the block to the filter type (MyRealmObject.self, filter: "userID == \(someID)"), so if in realm has write the object that fits it, then the observe block fires?
Instead of observing individual object which is still not stored in Realm database, you could observe Realm Results
let realm = try! Realm()
var results = realm.objects(MyRealmObject.self).filter: "userID == \(someID)")
var notificationToken = results.observe { change in
switch change {
case .update:
DispatchQueue.main.async {
block()
}
default: ()
}
}
You can observe the result object instead of individual and do some action based on changes.
I've been struggling with this for days. I'll appreciate any help.
I have a Location NSManagedObject and an Image NSManagedObject, they have one-to-many relationship, i.e., one location has many images.
I have 2 screens, in the first one the user adds locations on the view context and they get added and retrieved without problems.
Now, in the second screen, I want to retrieve images based on the location selected in the first screen, then display the images in a Collection View. The images are first retrieved from flickr, then saved in the DB.
I want to save and retrieve images on a background context and this causes me a lot of problems.
When I try to save every image retrieved from flickr I get a warning stating that there is a dangling object and the relationship can' be established:
This is my saving code:
func saveImagesToDb () {
//Store the image in the DB along with its location on the background thread
if (doesImageExist()){
dataController.backgroundContext.perform {
for downloadedImage in self.downloadedImages {
print ("saving to context")
let imageOnMainContext = Image (context: self.dataController.viewContext)
let imageManagedObjectId = imageOnMainContext.objectID
let imageOnBackgroundContext = self.dataController.backgroundContext.object(with: imageManagedObjectId) as! Image
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
imageOnBackgroundContext.image = imageData as Data
imageOnBackgroundContext.location = locationOnBackgroundContext
try? self.dataController.backgroundContext.save ()
}
}
}
}
As you can see in the code above I'm building NSManagedObject on the background context based on the ID retrieved from those on the view context. Every time saveImagesToDb is called I get the warning, so what's the problem?
In spite of the warning above, when I retrieve the data through a FetchedResultsController (which works on the background context). The Collection View sometimes view the images just fine and sometimes I get this error:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (4) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
Here are some code snippets that are related to setting up the FetchedResultsController and updating the Collection View based on changes in the context or in the FetchedResultsController.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}
return imagesCount
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
print ("cell data")
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! ImageCell
//cell.placeImage.image = UIImage (named: "placeholder")
let imageObject = fetchedResultsController.object(at: indexPath)
let imageData = imageObject.image
let uiImage = UIImage (data: imageData!)
cell.placeImage.image = uiImage
return cell
}
func setUpFetchedResultsController () {
print ("setting up controller")
//Build a request for the Image ManagedObject
let fetchRequest : NSFetchRequest <Image> = Image.fetchRequest()
//Fetch the images only related to the images location
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let predicate = NSPredicate (format: "location == %#", locationOnBackgroundContext)
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "location", ascending: true)]
fetchedResultsController = NSFetchedResultsController (fetchRequest: fetchRequest, managedObjectContext: dataController.backgroundContext, sectionNameKeyPath: nil, cacheName: "\(latLongString) images")
fetchedResultsController.delegate = self
do {
try fetchedResultsController.performFetch ()
} catch {
fatalError("couldn't retrive images for the selected location")
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
print ("object info changed in fecthed controller")
switch type {
case .insert:
print ("insert")
DispatchQueue.main.async {
print ("calling section items")
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.insertItems(at: [newIndexPath!])
}
break
case .delete:
print ("delete")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.deleteItems(at: [indexPath!])
}
break
case .update:
print ("update")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.reloadItems(at: [indexPath!])
}
break
case .move:
print ("move")
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.moveItem(at: indexPath!, to: newIndexPath!)
}
}
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {
print ("section info changed in fecthed controller")
let indexSet = IndexSet(integer: sectionIndex)
switch type {
case .insert:
self.collectionView!.numberOfItems(inSection: 0)
collectionView.insertSections(indexSet)
break
case .delete:
self.collectionView!.numberOfItems(inSection: 0)
collectionView.deleteSections(indexSet)
case .update, .move:
fatalError("Invalid change type in controller(_:didChange:atSectionIndex:for:). Only .insert or .delete should be possible.")
}
}
func addSaveNotificationObserver() {
removeSaveNotificationObserver()
print ("context onbserver notified")
saveObserverToken = NotificationCenter.default.addObserver(forName: .NSManagedObjectContextObjectsDidChange, object: dataController?.backgroundContext, queue: nil, using: handleSaveNotification(notification:))
}
func removeSaveNotificationObserver() {
if let token = saveObserverToken {
NotificationCenter.default.removeObserver(token)
}
}
func handleSaveNotification(notification:Notification) {
DispatchQueue.main.async {
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.reloadData()
}
}
What am I doing wrong? I'll appreciate any help.
I cannot tell you what the problem is with 1), but I think 2) is not (just) a problem with the database.
The error your are getting usually happens when you add or remove items/sections to a collectionview, but when numberOfItemsInSection is called afterwards the numbers don't add up. Example: you have 5 items and add 2, but then numberOfItemsInSection is called and returns 6, which creates the inconsistency.
In your case my guess would be that you add items with collectionView.insertItems(), but this line returns 0 afterwards:
guard let imagesCount = fetchedResultsController.fetchedObjects?.count else {return 0}
What also confused me in your code are these parts:
DispatchQueue.main.async {
print ("calling section items")
self.collectionView!.numberOfItems(inSection: 0)
self.collectionView.insertItems(at: [newIndexPath!])
}
You are requesting the number of items there, but you don't actually do anything with the result of the function. Is there a reason for that?
Even though I don't know what the CoreData issue is I would advise you to not access the DB in the tableview delegate methods, but to have an array of items that is fetched once and is updated only when the db content changes. That is probably more performant and a lot easier to maintain.
You have a common problem with UICollectionView inconsistency during batching update.
If you perform deletion/adding new items in the incorrect order UICollectionView might crash.
This problem has 2 typical solutions:
use -reloadData() instead of batch updates.
use third party libraries with safe implementation of batch update. Smth like this https://github.com/badoo/ios-collection-batch-updates
The problem is NSFetchedResultsController should only use a main thread NSManagedObjectContext.
Solution: create two NSManagedObjectContext objects, one in the main thread for NSFetchedResultsController and one in the background thread for performing the data writing.
let writeContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
let readContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
let fetchedController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: readContext, sectionNameKeyPath: nil, cacheName: nil)
writeContext.parent = readContext
UICollectionView will be updated properly once the data is saved in the writeContext with the following chain:
writeContext(background thread ) -> readContext(main thread) -> NSFetchedResultsController (main thread) -> UICollectionView (main thread)
I would like to thank Robin Bork, Eugene El, and meim for their answers.
I could finally solve both issues.
For the CollectionView problem, I felt like I was updating it too many times, as you can see in the code, I used to update it in two FetchedResultsController delegate methods, and also through an observer that observes any changes on the context. So I removed all of that and just used this method:
func controllerWillChangeContent(_ controller:
NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
In addition to that, CollectionView has a bug in maintaining the items count in a section sometimes as Eugene El mentioned. So, I just used reloadData to update its items and that worked well, I removed the usage of any method that adjusts its items item by item like inserting an item at a specific IndexPath.
For the dangling object problem. As you can see from the code, I had a Location object and an Image object. My location object was already filled with a location and it was coming from view context, so I just needed to create a corresponding object from it using its ID (as you see in the code in the question).
The problem was in the image object, I was creating an object on the view context (which contains no data inserted), get its ID, then build a corresponding object on the background context. After reading about this error and thinking about my code, I thought that the reason maybe because the Image object on the view context didn't contain any data. So, I removed the code that creates that object on the view context and created a one directly on the background context and used it as in the code below, and it worked!
func saveImagesToDb () {
//Store the image in the DB along with its location on the background thread
dataController.backgroundContext.perform {
for downloadedImage in self.downloadedImages {
let imageOnBackgroundContext = Image (context: self.dataController.backgroundContext)
//imagesLocation is on the view context
let locationObjectId = self.imagesLocation.objectID
let locationOnBackgroundContext = self.dataController.backgroundContext.object(with: locationObjectId) as! Location
let imageData = NSData (data: downloadedImage.jpegData(compressionQuality: 0.5)!)
imageOnBackgroundContext.image = imageData as Data
imageOnBackgroundContext.location = locationOnBackgroundContext
guard (try? self.dataController.backgroundContext.save ()) != nil else {
self.showAlert("Saving Error", "Couldn't store images in Database")
return
}
}
}
}
If anyone has another thought different from what I said about why the first method that first creates an empty Image object on the view context, then creates a corresponding one on the background context didn't work, please let us know.
Introduction
Context:
I'm coding up one of my first apps in which I want to be able to add a new row on a button press at index 0, (see code below). I use realm database to store my data.
Issue
Pressing the add button I have triggers the following crash Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to insert row 0 into section 0, but there are only 0 rows in section 0 after the update'.
Also when I check print statements of my tableView data array eventArray its always nil. Which leads me to believe that I'm not actually succeeding in saving data to realm.
Question:
Why isn't the data saved to my realm database? And why can't my tableView insert a new row?
Code
I've commented the points where my issues happen
class EventsScreen: UIViewController {
let realm = try! Realm()
var eventArray : EventList?
#objc func addEvent() { //The function my add button calls when pressed
let newCell = Event()
print(realm.objects(EventList.self).first)
insertInRealmContainerAtIndexZero(newEvent: newCell)
print(realm.objects(EventList.self).first)
let ndxPath = IndexPath(row: 0, section: 0)
tableView.beginUpdates()
tableView.insertRows(at: [ndxPath], with: .bottom) //App terminates on this line
tableView.endUpdates()
}
override func viewDidLoad() {
super.viewDidLoad()
loadEvents()
setupBackgroundVisuals()
registerCellAndDelegateAndDataSource()
}
private func insertInRealmContainerAtIndexZero(newEvent: Event) { // I suspect this method is the problem since when I restart the app the tableview doesn't load up any new created events.
if eventArray?.events != nil {
do {
try realm.write {
eventArray?.events.insert(newEvent, at: 0)
}
} catch {
print(error)
}
} else {
do {
try realm.write {
realm.add(newEvent)
}
} catch {
print(error)
}
}
}
private func loadEvents() {
eventArray = realm.objects(EventList.self).first
tableView.beginUpdates()
tableView.endUpdates()
}
}
Data object:
class Event: Object {
#objc dynamic var eventTitle: String?
#objc dynamic var eventText: String?
}
class EventList: Object {
let events = List<Event>()
}
Thanks for reading my post.
First time you launch the app this line
eventArray = realm.objects(EventList.self).first
returns nil and when run this
if eventArray?.events != nil {
it goes to else as eventArray is nil , then in else you do
realm.add(newEvent)
despite you should
eventArray = EventList()
eventArray.events.append(newEvent)
then add
realm.add(eventArray)
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?