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.
Related
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
I am using Swift 4 to build a single view iOS 11 application that has a UITableViewController that is also defined as a delegate for a NSFetchedResultsController.
class MyTVC: UITableViewController, NSFetchedResultsControllerDeleagate {
var container:NSPersistentContainer? =
(UIApplication.shared.delegate as? AppDelegate)?.persistentContainer
var frc : NSFetchedResultsController<Student>?
override func viewDidLoad() {
container?.performBackgroundTask { context in
// adds 100 dummy records in background
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
} // end of background inserting.
// now defining frc:
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch() // this works and I get no errors
tableView.reloadData()
frc.delegate = self
} // end of frc definition
}
}
If I add one row of Student using the viewContext, the frc will fire the required methods to show it in the tableView. However, the 100 dummy rows are not shown. In fact, If I try to tell the tableview to reload after the insertion is done, my app starts to behave weirdly and becomes buggy, and does not do what it should do (i.e: does not delete rows, does not edit, etc).
But If I restart my app, without calling the dummy insertion, I can see the 100 rows inserted from the previous run.
The only problem is that I can't call tableView.reloadData() from the background thread, so I tried to do this:
// after printing the count, I did this:
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData() // causes UI to behave weirdly
}
then I tried to call viewContext.perform to reload the table view in the proper thread
func viewDidLoad() {
// code for inserting 100 dummy rows in background thread
// code for defining frc and setting self as delegate
if let context = container?.viewContext {
context.perform { [weak self] in
self?.tableView.reloadData() // that also causes UI to behave weirdly
}
}
}
How can tell my tableview to reload and display the 100 dummy rows in a thread-safe manner?
override func viewDidLoad() {
super.viewDidLoad()
//Always need your delegate for the UI to be set before calling the UI's delegate functions.
frc.delegate = self
//First we can grab any already stored values.
goFetch()
//This chunk just saves. I would consider putting it into a separate function such as "goSave()" and then call that from an event handler.
container?.performBackgroundTask { context in
//We are in a different queue than the main queue, hence "backgroundTask".
for i in 1...100 {
let student = Student(context: context)
student.name = "student \(i)"
}
try? context.save() // this works because count is printed below
if let count = try? context.count(for: Student.fetchRequest()) {
print("Number of students in core data: \(count)") // prints 100
}
//Now that we are done saving its ok to fetch again.
goFetch()
}
//goFetch(); Your other code was running here would start executing before the backgroundTask is done. bad idea.
//The reason it works if you restart the app because that data you didn't let finish saving is persisted
//So the second time Even though its saving another 100 in another queue there were still at least 100 records to fetch at time of fetch.
}
func goFetch() {
if let context = container?.viewContext {
let request:NSFetchRequest<Student> = Student.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
frc = NSFetchedResultsController<Student> (
fetchRequest: request,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil )
try? frc?.performFetch()
//Now that records are both stored and fetched its safe for our delegate to access the data on the main thread.
//To me it would make sense to do a tableView reload everytime data is fetched so I placed this inside o `goFetch()`
DispatchQueue.main.async { [weak self] in
self?.tableView.reloadData()
}
}
}
After a lot of reading about the NSFetchedResultsController and the NSPersistentContainer and finally finding an important piece of information here at SO I think I have a working example.
My code is slightly different since I used a project I had for this. Anyway here is what I did:
In my view controller I had a property for my container
private var persistentContainer = NSPersistentContainer(name: coreDataModelName)
And in viewDidLoad I loaded the persistent store and created my 100 records.
persistentContainer.loadPersistentStores { persistentStoreDescription, error in
if let error = error {
print("Unable to add Persistent Store [\(error)][\(error.localizedDescription)]")
} else {
self.createFakeNotes() // Here 100 elements get created
DispatchQueue.main.async {
self.setupView() // other stuff, not relevant
self.fetchNotes() // fetch using fetch result controller
self.tableView.reloadData()
}
}
}
Below is createFakeNotes() where I use a separate context for inserting the elements in a background thread, this code is pretty much taken from Apple's Core Data programming guide but to make the UI being updated I needed to set automaticallyMergesChangesFromParent to true which I found out in this SO answer
I also delete old notes first to make the testing easier.
private func createFakeNotes() {
let deleteRequest = NSBatchDeleteRequest(fetchRequest: Note.fetchRequest())
do {
try persistentContainer.persistentStoreCoordinator.execute(deleteRequest, with: persistentContainer.viewContext)
} catch {
print("Delete error [\(error)]")
return
}
let privateContext = persistentContainer.newBackgroundContext()
privateContext.automaticallyMergesChangesFromParent = true //Important!!!
privateContext.perform {
let createDate = Date()
for i in 1...100 {
let note = Note(context: privateContext)
note.title = String(format: "Title %2d", i)
note.contents = "Content"
note.createdAt = createDate
note.updatedAt = createDate
}
do {
try privateContext.save()
do {
try self.persistentContainer.viewContext.save()
} catch {
print("Fail saving main context [\(error.localizedDescription)")
}
} catch {
print("Fail saving private context [\(error.localizedDescription)")
}
}
}
You should fetch your data by calling it from viewwillappear and then try to reload your tableview.
override func viewWillAppear(_ animated: Bool) {
getdata()
tableView.reloadData()
}
func getdata() {
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
do{
persons = try context.fetch(Person.fetchRequest())
}
catch {
print("fetching failed")
}
}
Here is what I am trying to do. I have a simple journaling app with two views: a tableView that lists the titles of the entries and a viewController that has a text field for a title, and a textView for the text body (and a save button to save to cloudKit). On the viewController, I hit save and the record is saved to cloudKit and also added to the tableView successfully. This is all good.
I want to be able to edit/update the journal entry. But when I go back into the journal entry, change it in any way, then hit save again, the app returns to the tableView controller with an updated entry, but cloudKit creates a NEW entry separate from the one I wanted to edit. Then when I reload the app, my fetchRecords function fetches any extra records cloudKit has created.
Question: How do I edit/update an existing journal entry without creating a new entry in cloudKit?
Let me know if you need something else to further clarify my question.
Thanks!
Here are my cloudKit functions:
import Foundation
import CloudKit
class CloudKitManager {
let privateDB = CKContainer.default().publicCloudDatabase //Since this is a journaling app, we'll make it private.
func fetchRecordsWith(type: String, completion: #escaping ((_ records: [CKRecord]?, _ error: Error?) -> Void)) {
let predicate = NSPredicate(value: true) // Like saying I want everything returned to me with the recordType: type. This isn't a good idea if you have a massive app like instagram because you don't want all posts ever made to be loaded, just some from that day and from your friends or something.
let query = CKQuery(recordType: type, predicate: predicate)
privateDB.perform(query, inZoneWith: nil, completionHandler: completion) //Allows us to handle the completion in the EntryController to maintain proper MVC.
}
func save(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
modify(records: records, perRecordCompletion: perRecordCompletion, completion: completion )
}
func modify(records: [CKRecord], perRecordCompletion: ((_ record: CKRecord?, _ error: Error?) -> Void)?, completion: ((_ records: [CKRecord]?, _ error: Error?) -> Void)?) {
let operation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
operation.savePolicy = .ifServerRecordUnchanged //This is what updates certain changes within a record.
operation.queuePriority = .high
operation.qualityOfService = .userInteractive
operation.perRecordCompletionBlock = perRecordCompletion
operation.modifyRecordsCompletionBlock = { (records, _, error) in
completion?(records, error)
}
privateDB.add(operation) //This is what actually saves your data to the database on cloudkit. When there is an operation, you need to add it.
}
}
This is my model controller where my cloudKit functions are being used:
import Foundation
import CloudKit
let entriesWereSetNotification = Notification.Name("entriesWereSet")
class EntryController {
private static let EntriesKey = "entries"
static let shared = EntryController()
let cloudKitManager = CloudKitManager()
init() {
loadFromPersistentStorage()
}
func addEntryWith(title: String, text: String) {
let entry = Entry(title: title, text: text)
entries.append(entry)
saveToPersistentStorage()
}
func remove(entry: Entry) {
if let entryIndex = entries.index(of: entry) {
entries.remove(at: entryIndex)
}
saveToPersistentStorage()
}
func update(entry: Entry, with title: String, text: String) {
entry.title = title
entry.text = text
saveToPersistentStorage()
}
// MARK: Private
private func loadFromPersistentStorage() {
cloudKitManager.fetchRecordsWith(type: Entry.TypeKey) { (records, error) in
if let error = error {
print(error.localizedDescription)
}
guard let records = records else { return } //Make sure there are records.
let entries = records.flatMap({Entry(cloudKitRecord: $0)})
self.entries = entries //This is connected to the private(set) property "entries"
}
}
private func saveToPersistentStorage() {
let entryRecords = self.entries.map({$0.cloudKitRecord})
cloudKitManager.save(records: entryRecords, perRecordCompletion: nil) { (records, error) in
if error != nil {
print(error?.localizedDescription as Any)
return
} else {
print("Successfully saved records to cloudKit")
}
}
}
// MARK: Properties
private(set) var entries = [Entry]() {
didSet {
DispatchQueue.main.async {
NotificationCenter.default.post(name: entriesWereSetNotification, object: nil)
}
}
}
}
Here's a couple threads that might be helpful.
If you were caching the data locally you would use the encodesystemfields method to create a new CKRecord that will update an existing one on the server.
How (and when) do I use iCloud's encodeSystemFields method on CKRecord?
It doesn't appear you are caching locally. I don't have experience doing it without using encodesystemfields, but it looks like you have to pull the record down and save it back in the completion handler of the convenience method:
Trying to modify ckrecord in swift
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)
}
}
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()