Weird issue with WatchKit and CoreData - ios

I am playing around with WatchKit and CoreData and the demo app I wrote is working but sometimes gives unexpected results. Basically, it's a toDo app. You enter the toDo items on the iPhone and it stores them using CoreData in an entity called ToDoItem with 2 attributes: name (string) and completed (boolean). The name of items entered on the iPhone are assigned item.completed = false in the CoreData entity.
On the Apple Watch it queries CoreData on the iPhone and lists the names of the items that are not completed (item.completed == 0) in a WatchKit table view. When an item on the watch is selected the font changes to red, the row is deleted from the table, and it changes the completed value in CoreData to true (item.completed = true). The next time the Watch app is launched, the completed items no longer are listed in the table.
This works fine most of the time, but sometimes when an item is selected on the watch, it does not update CoreData with the item as item.completed = true. And once this happens, it no longer updates the items in CoreData when selected. No errors are given. If I reset the simulator, it starts working again but after a while does the same thing.
My code is below:
class InterfaceController: WKInterfaceController {
#IBOutlet var table: WKInterfaceTable!
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Configure interface objects here.
var context = CoreDataStack.sharedInstance.managedObjectContext!
let request = NSFetchRequest(entityName: "ToDoItem") //CoreData entity ToDoItem contains two attributes - name (string) and completed (boolean)
let fetchItems: Array = context.executeFetchRequest(request, error: nil) as! [ToDoItem] //fetches the items in the entity
var counter = 0
for element in fetchItems{
counter++
println("The item name is \(element.name) and completed is \(element.completed)") //prints the items to the console
}
self.table.setNumberOfRows(fetchItems.count, withRowType: "ToDoRow")
var theCount = 0
for element in fetchItems {
if element.completed == 0 {
let row = self.table.rowControllerAtIndex(theCount) as? ToDoTableRowController
row?.nameLabel.setText(element.name) //populates the table with names of items that are not completed
theCount++
}
}
}
override func table(table: WKInterfaceTable, didSelectRowAtIndex rowIndex: Int) {
//self.pushControllerWithName("ToDoDetail", context: nil)
let row = table.rowControllerAtIndex(rowIndex) as! ToDoTableRowController
row.nameLabel.setTextColor(UIColor.redColor()) //change the color of font to red when row is selected
var myIndex = NSIndexSet(index: rowIndex)
self.table.removeRowsAtIndexes(myIndex) //removes selected row from table
var context = CoreDataStack.sharedInstance.managedObjectContext!
let request = NSFetchRequest(entityName: "ToDoItem")
let items = context.executeFetchRequest(request, error: nil) as! [ToDoItem]
let item = items[rowIndex]
item.setValue(true, forKey: "completed") //changes the selected item to completed and saves it in coredata
//item.completed = true
var error: NSError?
if !context.save(&error) {
println("Cannot save due to \(error) \(error?.localizedDescription)")
}
}

How are you coordinating data between the host app and the watch extension? I'm using a Core Data store in the app group container and I found that the watch, the today widget, and the host app didn't get updates to managed objects until a save forced a merge to happen. (The managed object context doesn't notice the objects in the store have changed.) So it might be that the watch's context has the completed item but the host app still has an uncompleted item in the context.
The fix for me was to use MMWormhole so all three binaries can update each other when they touch a NSManagedObject subclass. I pass the object ID in the message and when a binary receives the message a call to
context.refreshObject(managedObject, mergeChanges: true) gets everyone back in sync.

Related

Is there any pagination like code for saving JSON data to Realmdatabase

I have a button and below it is the table view. Table view cell has some random data.On button click I am calling the the api(function name is : api.urlRequest(userID: 80, businessUnitID: 2) ) .I have an API that has 35,0000 entries. What I want is to save that data in Realm database. The problem is that, when I am calling the save function, my UI freezes. I am appending the JSON data to Model and then saving it to database. I can get the start index and end index of the the JSON data.
What I tried was to call the API on background thread and when saving function is called, I am calling it on main thread. But this didn't worked.
class ViewController: UIViewController,getAdhocJSONDelegate{
let realm = try! Realm()
#IBOutlet weak var tableViewRef: UITableView!
var array = [NSDictionary]()
var adhocData : [AdhocModel] = []//for appending the JSON data to the model
var adhocDB : Results<AdhocDB>?// for accessing the database
let api = AdhocAPIParamteres()
var adhocJSONDatafromAPI : NSDictionary!
override func viewDidLoad() {
super.viewDidLoad()
adhocDB = realm.objects(AdhocDB.self)
}
#IBAction func buttonPressed(_ sender: Any) {
print("BUtton Tapped")
api.urlRequest(userID: 80, businessUnitID: 2)
api.delegate = self
}
func appTutorialData(json: NSDictionary) {
adhocJSONDatafromAPI = json
let apiData = adhocJSONDatafromAPI.value(forKey: "data") as! [NSDictionary]
print("Start Index of the data : ",apiData.startIndex)
print("End Index of the data : ",apiData.endIndex)
apiData.forEach { (abc) in
let model = AdhocModel()
model.site_id = abc.value(forKey: "site_id") as! Int
model.atm_id = abc.value(forKey: "atm_id") as! String
model.site_address = abc.value(forKey: "site_address") as! String
adhocData.append(model)
print("data appended")
DispatchQueue.main.async {
self.saveToDb(data:model)
}
}
func saveToDb(data: AdhocModel) {
let adhoc = AdhocDB()
try! realm.write {
adhoc.SiteId = data.site_id
adhoc.AtmId = data.atm_id
adhoc.SiteAdress = data.site_address
realm.add(adhoc)
}
}
}
I want to save data in such a way that my UI doesn't freeze.
There are a few issues with the code and writing data to Realm on a background thread is covered in the documentation so I won't address that. Following that design pattern will correct the UI lockup.
This is another issue
func saveToDb(data: AdhocModel) {
**let adhoc = AdhocDB()**
You want to write your populated model to realm, but AdhocDB is a Results object, not a Realm model object. Additionally the realm object created in appTutorialData which is model, is passed to saveToDb, then another object is created and then populated with data from the first object. There's no reason to do that (in this code)
Assuming AdHocModel is a Realm object, this is much cleaner
func appTutorialData(json: NSDictionary) {
adhocJSONDatafromAPI = json
let apiData = adhocJSONDatafromAPI.value(forKey: "data") as! [NSDictionary]
print("Start Index of the data : ",apiData.startIndex)
print("End Index of the data : ",apiData.endIndex)
apiData.forEach { (abc) in
let model = AdhocModel()
model.site_id = abc.value(forKey: "site_id") as! Int
model.atm_id = abc.value(forKey: "atm_id") as! String
model.site_address = abc.value(forKey: "site_address") as! String
try! realm.write {
realm.add(model)
}
}
}
You're going to want to wrap that write within a background thread (again, see the documentation) something like this
DispatchQueue(label: "background").async {
autoreleasepool {
.
.
.
try! realm.write {
realm.add(model)
}
}
}
You may ask about populating your array adhocData.append(model). We don't know what you're doing with it but if you're using it as perhaps a dataSource for a table view or some other UI element, you may want to consider using a Results object instead of an Array.
A significant advantage is, if you have 35,000 objects, that's a pretty sizable array and if you have more, it could overwhelm the device as ALL of that data is stored in memory. However, Results objects are lazily loaded so you could have a much larger dataset without overwhelming the device.
Additionally, when Realm objects are stored in an array, they 'Disconnect' from Realm and loose Realm functionality - they will not auto-update nor will changes to the actual object in Realm be reflected in that array nor can you just update the object - it doesn't appear to have a primary key.
However, if you populate a Results object with those models, they will be live updating - so if for example the atm_id changes in Realm, that object will automatically be updated. If you need to change a property you can change it directly on that object within a write transaction.
So the pattern would be to have a class var of Results and load your objects into those results within viewDidLoad. As you add more models, the results object will automatically be updated.
To keep your UI fresh, you would want to add observers (aka Notifications)to those Results so you can be notified when an object is updated so you can reload your tableView for example.

How can I stop UICollectionView from showing duplicate items after Firebase update

I have two UICollection views on a page that displays data about a Room. It includes photos of the room in one UICollection View and another UICollection View which contains a list of items in that room. There's a link to edit the Room. When a user clicks on the link, they then segue to another view that let's them update it including adding additional photos.
After adding a photo, and hitting submit, in the background the photo is uploaded to Firebase storage and in the Firebase database, the record is updated to include the name of the file that was just uploaded. Meanwhile, the user is segued back to the Room view.
There's a watched on the record of the room in Firebase and when it updates, then the view is refreshed with new data. This is where the problem occurs. It appears, based on a lot of debugging that I've been doing, that the Observe method fires twice and what ends up happening, is the UICollection view that holds the images of the room will show duplicates of the last photo added.
For example, if I add one photo to the room, that photo will appear in the collection 2x. I've attempted to clear the array before the array is updated with the images, and from my analysis, it appears that the array only contains two items, despite showing three in the view. I'm not sure what is happening that would cause this?
Here's a link to the entire file, because I think it might help.
Here's the loadData() method in case this is all that's important:
func loadData() {
self.ref = Database.database().reference()
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = true
guard let userID = Auth.auth().currentUser?.uid else { return }
let buildingRef = self.ref.child("buildings").child(userID)
buildingRef.keepSynced(true)
buildingRef.child(self.selected_building as String).observe(DataEventType.value, with: { (snapshot) in
let value = snapshot.value as? NSDictionary
if ((value) != nil) {
let building_id = value?["id"] as! String
let saved_image = value?["imageName"] as! String
let user_id = userID as! String
let destination = "/images/buildings/\(userID)/\(building_id)/"
let slideShowDictionary = value?["images"] as? NSDictionary
if ((slideShowDictionary) != nil) {
self.slideShowImages = [UIImage]()
self.slideShowCollection.reloadData()
var last_value = ""
slideShowDictionary?.forEach({ (_,value) in
print("are they different? \(last_value != (value as! String))")
if (last_value != value as! String) {
print("count: \(self.slideShowImages.count)")
print("last_value \(last_value)")
print("value \(value)")
last_value = value as! String
CloudStorage.instance.downloadImage(reference: destination, image_key: value as! String, completion: { (image) in
self.slideShowImages.append(image)
self.slideShowCollection.reloadData()
})
}
})
CloudData.instance.getBuildingById(userId: user_id, buildingId: building_id, completion: { (building) in
self.title = building.buildingName as String
self.roomsCollection.reloadData()
})
}
}
})
// User is signed in.
self.getRooms()
}
I am not completely familiar with the Firebase API but if you are having issues with the observation I would suspect the following:
#IBAction func unwindToRoomsVC(segue:UIStoryboardSegue) {
loadData()
}
Triggering loadData a second time looks like it would add a second observation block. As best I can tell the .observe method probably persists the block it is given and triggers it on all changes.

CoreData - fill array with entity attribute in swift

I'm pretty new to Swift/Xcode and I'm faced with a problem of trying to place CoreData attribute values into an array.
The app is a simple shopping list with an entity with 5 attributes:
entity is "Item"
image Binary data
name String
note String
qty String
status Boolean
The user creates a new shopping item which is displayed in a UITableVIewController (so far I'm fine)
I have to data loading into cells correctly.
I also want to display the total quantity(entity "qty") of items in a label.
I can't seem to put each qty into an array so that I can add them together to display them. There seems to be plenty of resources for Objective C but not much out there for swift.
These are what I've been looking at:
Filling an array with core data attributes
CoreData to Array in Swift
Swift: Fetch CoreData as Array
coredata - fetch one attribute into an array
UITableViewController Code:
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
var frc2 : NSFetchedResultsController = NSFetchedResultsController()
func fetchRequest2() -> NSFetchRequest {
let fetchRequest2 = NSFetchRequest(entityName: "Item")
let sortDescriptor2 = NSSortDescriptor(key: "name",ascending:true)
fetchRequest2.sortDescriptors = [sortDescriptor2]
return fetchRequest2
}
override func viewDidLoad() {
super.viewDidLoad()
//set frc
frc2 = getFRC()
frc2.delegate = self
do {
try frc2.performFetch()
} catch {
print("failed to perform initial fetch request")
return
}
self.tableView.reloadData()
}
Any help would be greatly appreciated.

Why is UICollectionView reloadData crashing my app?

I am using uicollectionview to display some photos and I have a button that allows the user to delete the selected photo.
It works perfectly unless I try to delete the last photo in the array of photos being used to populate the uicollectionview. Ie if there are 5 photos then there will be a problem if a user removes the 5th photo but not the 1st, 2nd, 3rd or 4th. When I try to delete the 5th photo it crashes on reloadData() with the following error
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 4 from section 0 which only contains 4 items before the update'
I don't understand why this would happen... The error even mentions "attempt to delete" but I never actually told it I was deleting anything. I just changed the source and then asked it to update. Also in the error message it says the section only contains 4 items before the update when their were actually 5 photos. Why?
A little bit more info about what I'm doing and how (Using Swift)...
I've got a ProgressPhotoSheetClass from which I have instantiated the object progressPhotoSheet
this progressPhotoSheet object has an array of photos and the photos can have priorities such as photo id, title etc
in my number of items in section I use
var numPics: Int = progressPhotoSheet.progressPhotos.count
return numPics
in my cellForItemAtIndexPath I use
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! PhotoHolder
cell.photoTitle.text = progressPhotoSheet.progressPhotos[indexPath.row].photoName
...etc..
When deleting the item I use
progressPhotoSheet.deletePhoto(progressPhotoSheet.progressPhotos[indexPath.row].photoId)
which deletes the photo from the device and from my core data and then reloads progressPhotoSheet.progressPhotos from using the modified core data so it is now exactly how it was before but without the deleted photo.
I then call
self.collectionView.reloadData()
Which should update the UICollectionView for the new data
I could understand if it felt there was a mismatch between what should be in the collection view and what is in the datasource if I were using
self.collectionView.deleteItemsAtIndexPaths(indexPaths)
because that would be saying ignored to get them to match we need to delete one item - here there is a possibility something could mismatch.. But surely using self.collectionView.reloadData() it doesn't matter what changes were made it should just look at what data is there now and update the UICollectionView accordingly....
So my question is... Why am I getting this error and what should I do to fix things so I don't get it?
Edit to include more info
Here is my telephoto Code
func deletePhoto(photoId: Int) {
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
// Fetch correct photo
let fetchRequest = NSFetchRequest(entityName: "CDProgressPhoto")
fetchRequest.predicate = NSPredicate(format: "photoId = %#", String(photoId))
// Save
if let fetchResults = managedContext.executeFetchRequest(fetchRequest, error: nil) as? [NSManagedObject] {
if fetchResults.count != 0{
// Will only be one photo with this photo id
var photo = fetchResults[0]
photo.setValue(true, forKey: "toDelete")
// Save the object
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
}
}
// Reload from core data
self.loadPhotoSheetFromCoreData()
}
self.loadPhotoSheetFromCoreData() then empties progressPhotoSheet.progressPhotos before getting the new data from core data... Code below...
private func loadPhotoSheetFromCoreData() {
if(self.hasPhotoSheet()) {
// Clear existing photos
self.progressPhotos = []
// Set up Core Data Managed Object Context
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let managedContext = appDelegate.managedObjectContext!
let request = NSFetchRequest(entityName: "CDProgressPhoto")
let predicate1 = NSPredicate(format: "photoSheetId == %#", String(self.sheetId))
let predicate2 = NSPredicate(format: "toDelete == %#", false)
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false) as NSSortDescriptor]
var predicatesArray: [NSPredicate] = [predicate1, predicate2]
//predicatesArray.append(predicate1)
request.predicate = NSCompoundPredicate.andPredicateWithSubpredicates(predicatesArray)
let existings = managedContext.executeFetchRequest(request, error: nil)
let existingPhotos: [CDProgressPhoto] = existings as! [CDProgressPhoto]
// for each photo make a ProgressPhoto object and add to progress photos array
for photo in existingPhotos {
var newPhoto: ProgressPhoto = ProgressPhoto()
newPhoto.photoSheetId = Int(photo.photoSheetId)
newPhoto.photoId = Int(photo.photoId)
newPhoto.photoName = photo.photoName
newPhoto.date = Int(photo.date)
newPhoto.url = photo.url
newPhoto.filename = photo.filename
newPhoto.height = Float(photo.height)
newPhoto.width = Float(photo.width)
newPhoto.selected = false
self.progressPhotos.append(newPhoto)
}
}
}
As you can see the photo isn't actually deleted at this point I just set a toDelete flag to true and then only re load items where toDelete is set to false. The photos are deleted later asynchronously depending on network connection etc because they are also stored on a server for use on the main website.
Have you tried calling invalidateLayout() on the collectionView? That might help incase your view is empty i.e. 0 elements are present.

swift, removing objects from core data, still appends empty objects?

I'm attempting to set up a UITableView, with all objects in an entity.
However, I'm loading the data from an api. So every time it loads, I'm deleting all the objects in the entity. And adding the new ones.
However, when I do this. It's showing 5 cells with the api data, and every time I load it. It adds 5 empty cells.
The reason it's adding empty cells is because I'm defining numberOfRowsInSection with objects count like so:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stocks.count
}
Where stocks is: var stocks = [NSManagedObject]()
So from that I assume it's because there's somehow empty objects in my entity?
here's how I'm trying to set up my data:
func loadSuggestions(Formula: Int) {
println("----- Loading Data -----")
// Check for an internet connection
if Reachability.isConnectedToNetwork() == false {
println("ERROR: -> No Internet Connection <-")
} else {
// Delete all the current objects in the entity
let fetchRequest = NSFetchRequest(entityName: formulaEntity)
let a = managedContext.executeFetchRequest(fetchRequest, error: nil) as! [NSManagedObject]
for mo in a {
managedContext.deleteObject(mo)
//println("removed \(mo)")
}
// save the context after removing objects.
managedContext.save(nil)
// Setting up a fetch request to get the api data.
let entity = NSEntityDescription.entityForName("\(formulaEntity)", inManagedObjectContext:managedContext)
var request = NSURLRequest(URL: formulaAPI!)
var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
var formula = JSON(data: data!)
for (index: String, actionable: JSON) in formula["actionable"] {
stockName = actionable["name"].stringValue
ticker = actionable["ticker"].stringValue
action = actionable["action"].stringValue
suggestedPrice = actionable["suggested_price"].floatValue
weight = actionable["percentage_weight"].floatValue
// Set the values in CoreData
let stock = NSManagedObject(entity: entity!,insertIntoManagedObjectContext:managedContext)
stock.setValue(stockName, forKey: "name")
stock.setValue(ticker, forKey: "ticker")
stock.setValue(action, forKey: "action")
stock.setValue(suggestedPrice, forKey: "suggestedPrice")
stock.setValue(weight, forKey: "weight")
// Set up second api fetch
var quoteAPI = NSURL(string: "http://dev.markitondemand.com/Api/v2/Quote/json?symbol=\(ticker)")
var quoteRequest = NSURLRequest(URL: quoteAPI!)
var quoteData = NSURLConnection.sendSynchronousRequest(quoteRequest, returningResponse: nil, error: nil)
if quoteData != nil {
var quote = JSON(data: quoteData!)
betterStockName = quote["Name"].stringValue
lastPrice = quote["LastPrice"].floatValue
// Setting the last values in CoreData
if betterStockName != "" {
stock.setValue(betterStockName, forKey: "name")
}
stock.setValue(lastPrice, forKey: "lastPrice")
} else {
println("ERROR - NO DATA")
}
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
// finally add the stockdata to the CoreData entity.
stocks.append(stock)
}
// Reload the tableview with the new data.
self.tableView.reloadData()
}
}
There isn't a lot of information on how to delete all of the objects in an entity. But I tried to print out the objects it was deleting, and that all seemed correct.
But I must be doing something wrong somewhere in the above function. Since it adds 5 more cells every time the above function is called.
Any help figuring out where the 5 empty cells are coming from and how to fix it would be greatly appreciated!
The simple reason is that you only ever add new entries to stocks, you never remove anything. If you never remove anything from the array, it will never get smaller.
You do delete the objects from Core Data, but that doesn't automatically remove them from the array. They don't disappear from memory and an array just because Core Data doesn't have them anymore. You need to do that separately by calling removeAll on the array after deleting the objects from Core Data.

Resources