How to display CloudKit RecordType instances in a tableview controller - swift

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

Related

Understanding how to correctly execute CKQueryOperation

I'm working with CloudKit for the first time and am having trouble executing a CKQueryOperation to query all records of a given type. It doesn't help that Apple has deprecated most of the stuff I've found online and that their documentation for these things are completely blank besides the func declaration. I think I've got the "skeleton" of the code done but am unsure of what goes into the .recordMatchedBlock and the .queryResultsBlock.
I have a func queryAllNotes() which should query all records in the public database of type "Notes" and return an array of tuples of the note's title and its associated cloudID, which is just the unique recordName given to it when it is added to the database.
Here's the code for queryAllNotes() :
private func queryAllNotes() -> [(title: String, cloudID: String)] {
/*
query all notes in the cloud DB into an array to populate
the tableView
*/
var resultArray: [(title: String, cloudID: String)] = []
//set the cloud database to .publicCloudDatabase
let container = CKContainer.default()
let cloudDB = container.publicCloudDatabase
let pred = NSPredicate(value: true) //true -> return all records
let query = CKQuery(recordType: "Notes", predicate: pred)
let queryOperation = CKQueryOperation(query: query)
queryOperation.database = cloudDB
queryOperation.resultsLimit = 100
queryOperation.recordMatchedBlock = { (record: CKRecord) in
let noteTitle = record["Title"] as! String
let noteCloudID = record.recordID.recordName
resultArray.append((noteTitle, noteCloudID))
}
queryOperation.queryResultBlock = { (cursor, error) in
}
return resultArray
}
To my understanding the .recordMatchedBlock is called for every record returned by the query so I think it is complete but I could be very wrong. In regards to the .queryResultBlock, my understanding is that the query technically only return one record at a time and this block basically tells the query to run again for the next record for all records within the .resultLimit. How can I structure this query? I am keen to understand what each of these blocks do.
Also this is for a macOS app; I don't know if the code is different for macOS vs iOS but I thought I should include this just in case.
Also I'm getting an error saying "Type of expression is ambiguous without more context" which I'm assuming is because I haven't completed setting up my query. If it's for a different reason could also explain why this is happening.
Edit
I call this func inside of viewDidLoad() like so:
//array var for the array that is used to populate the tableView
var noteRecords: [(title: String, cloudID: String)] = []
override func viewDidLoad() {
super.viewDidLoad()
// do additional setup here
// set serachField delegate
searchField.delegate = self
// set tableView delegate and data source
tableView.delegate = self
tableView.dataSource = self
// load all NoteRecords in public cloud db into noteRecords
noteRecords = queryAllNotes()
}
With the new async pattern it has become much easier to fetch data from CloudKit.
Instead of CKQueryOperation you call records(matching:resultsLimit:) directly and map the result to whatever you like.
A possible error is handed over to the caller.
func queryAllNotes() async throws -> [(title: String, cloudID: String)] {
//set the cloud database to .publicCloudDatabase
let container = CKContainer.default()
let cloudDB = container.publicCloudDatabase
let pred = NSPredicate(value: true) //true -> return all records
let query = CKQuery(recordType: "Notes", predicate: pred)
let (notesResults, _) = try await cloudDB.records(matching: query,
resultsLimit: 100)
return notesResults
.compactMap { _, result in
guard let record = try? result.get(),
let noteTitle = record["Title"] as? String else { return nil }
return (title: noteTitle, cloudID: record.recordID.recordName)
}
}
And use it
override func viewDidLoad() {
super.viewDidLoad()
// do additional setup here
// set serachField delegate
searchField.delegate = self
// set tableView delegate and data source
tableView.delegate = self
tableView.dataSource = self
// load all NoteRecords in public cloud db into noteRecords
Task {
do {
noteRecords = try await queryAllNotes()
tableView.reloadData()
} catch {
print(error)
}
}
}
Please watch the related video from WWDC 2021 for detailed information about the async CloudKit APIs and also the Apple examples on GitHub.
Side note:
Rather than a tuple use a struct. Tuples as data source array are discouraged.

NSFetchRequestResult returns duplicate data from the database

I have a simple entity with identifier (constraint) and name fields. In ViewController I try to add data to the database without worrying about duplicates and there really is no duplicate data in the database, but when I try to get records, I get twice as many of them. As I found out, this only happens when I try to write something to the database, and regardless of whether the attempt was successful, the data goes to my NSFetchRequestResult. What is the reason for this behavior?
DB content now:
ViewController:
class ViewController: UIViewController {
let moc = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
override func viewDidLoad() {
super.viewDidLoad()
//if comment this loop I won't get duplicates
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
try? moc.save() //if the code is not executed for the first time, then the attempt is unsuccessful due to identifier constraint
}
let fetch = NSFetchRequest<NSFetchRequestResult>(entityName: "Entity")
let fetchedEntities = try? moc.fetch(fetch) as? [Entity]
print(fetchedEntities!.count) // Output: 12 (actually only 6 records in the db)
}
}
Change your code where you create the objects to
for i in 0...5 {
let ent = Entity(context: moc)
ent.identifier = Int16(i)
ent.name = "Username"
}
do {
try moc.save()
} catch {
moc.reset()
}
This way you will remove the faulty (duplicate) objects from the context

Core Data Attribute reset in Swift

In my application, I have set up a Core Data entity called "Finances". For now it has 2 attributes: money and net-worth.
I wish to write a function that each time it is called, it deletes all results for a specific entity. An example might be:
func resetAttribute(attribute: String) {
}
PS: I have found on the internet a function which was engineered to only delete a specific element of an attribute, which matched to a string. I have modified the code in the following way:
func resetTest() {
if let dataAppDelegatde = UIApplication.shared.delegate as? AppDelegate {
let mngdCntxt = dataAppDelegatde.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ApplicationFinances")
let predicate = NSPredicate(format: "money != %#", "vdavduabsdpansuryiubj")
fetchRequest.predicate = predicate
do {
let result = try mngdCntxt.fetch(fetchRequest)
print(result.count)
if result.count > 0 {
for object in result {
print(object)
mngdCntxt.delete(object as! NSManagedObject)
}
}
} catch{
}
}
}
Meaning that if money wouldn't have been equals to vdavduabsdpansuryiubj (Meaning never of course) it would have deleted the other values. But this didn't seem to work.
What happens when you run the function? Does it throw an error? Or does it print just fine and runs fine but nothing actually happens when you reload the core data? If this is the case, try adding:
do {
try mngdCntxt.save()
} catch {}
Right after the for loop is over

core data and relationship predicate

I've start swift & core data few month ago usually I've found my answer on this website but for the first time I'm really stuck with "Relationship" and "Predicates"
I've created a first view controller with a tableview which is populated by the user and this part is working like I wish.
The user can "tap" a cell and open a new view controller with a new tableview and I'd like populate this tableview with data that in relation with the cell the user tapped.
I'm using CoreData and I've set 2 entities : "Compte" and "Operation" they are in relationship by ONE TO MANY (ONE compte for MANY operation)
Here where I am :
when the user tap the cell i'm using segue to send the "Compte" to the second view controller :
//Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let guest = segue.destination as! OperationsViewController
let indexPath = tableView.indexPathForSelectedRow
let operation = fetchedResultController.object(at: indexPath!)
guest.compteTestRelation = operation
}
In the OperationsViewController i've set this variable :
var compteTestRelation: Compte!
for testing my data I've create a FOR LOOP like this and a FUNCTION:
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
the FUNCTION
func displayOperation() {
if let opList = compteTestRelation.relationCompte as? Set<Operation> {
sortedOperationArray = opList.sorted(by: { (operationA:Operation, operationB:Operation) -> Bool in
return operationA.date!.compare(operationB.date! as Date) == ComparisonResult.orderedAscending
})
print(sortedOperationArray)
}
}
In the console with "print" It work like I wish depend the cell is tapped the print(sortedOperationArray) appear or not
My problem now is how populate my tableview with this data, when I use predicates in my FetchResultController I've got error or an empty tableview but in the console everything seems to work so I'm thinking the relationship is OK ..
If I don't use PREDICATE I can populate my tableview with the data but I see always ALL the data
I've seen other similar problems and answers on stackoverflow.com but nothing work for the moment.
Thank You! :)
I've found an another way to predicate my data and it works for me now
I've create a new attribute in my OPERATION entity called "id" and when I create my data I attribute an ID like this :
for index in 1 ... 10 {
let newOp = Operation(context: context)
newOp.nom = "Test Compte \(index)"
newOp.date = NSDate()
newOp.moyenPaiement = "Test"
newOp.id = "id123\(compteTestRelation.nom!)"
compteTestRelation.addToRelationCompte(newOp) // RelationShip
}
do {
try context.save()
} catch {
print(error.localizedDescription)
}
then I predicate my data like this in my FetchResultController :
func setupFetchedResultController () {
let operationsRequest: NSFetchRequest<Operation> = Operation.fetchRequest()
let sortDescriptor = NSSortDescriptor(key: "nom", ascending: true)
let keyPath = "id"
let searchString = "id123\(compteTestRelation.nom!)"
let operationsPredicate = NSPredicate(format: "%K CONTAINS %#", keyPath, searchString)
operationsRequest.returnsObjectsAsFaults = false
operationsRequest.sortDescriptors = [sortDescriptor]
operationsRequest.predicate = operationsPredicate
fetchedResultController = NSFetchedResultsController(fetchRequest: operationsRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
do {
try fetchedResultController.performFetch()
} catch {
print(error.localizedDescription)
}
}

reload UITableView inside closure causes strange behaviour

I am using CloudKit to fetch CKRecords to populate a UITableView.
The operations Array acts as the datasource for the tableView
let predicate = NSPredicate(value: true)
let query = CKQuery(recordType: "Operation", predicate: predicate)
let queryOperation = CKQueryOperation(query: query)
operations = []
queryOperation.recordFetchedBlock = {
record in
self.operations.append(record)
}
queryOperation.queryCompletionBlock = {
cursor, error in
if error == nil {
dispatch_sync(dispatch_get_main_queue(), {self.tableView.reloadData()})
}
}
let database = CKContainer.defaultContainer().privateCloudDatabase
database.addOperation(queryOperation)
If i simply use self.tableView.reload() in the closure the tableView will update but there is a 5-10s delay unless I swipe the tableView where the cells will suddenly show. However when I dispatch on the main thread (as in the code above) the tableView cells will show but sometimes the top cell starts flashing or it renders very strange. I am wondering if anyone has any tips on how to implement the tableView.reloadData() inside this closure?
EDIT: This behaviour only seems to appear when i call the above code from viewDidLoad. If i call the above code after the tableview updates fine.