How to Utilize Fetched Record Attributes with CloudKit? - ios

so I'm trying to query my public database for a single record using CloudKit, and save an attribute value from the record to an external array(foodArrayLunch). I'm using the property RecordFetchedBlock in a closure to process my record. However, when the closure executes successfully, I find that my array failed to save the attribute value that was queried from the database, and it can be accessed and managed only from inside the closure. So my question is: How can I save this attribute value from outside of the RecordFetchedBlock? Any help would be greatly appreciated, Thanks!
class ViewControllerNewcomb: UIViewController, UITableViewDelegate, UITableViewDataSource {
let database = CKContainer.defaultContainer().publicCloudDatabase
#IBOutlet weak var LunchView: UITableView!
var foodArrayLunch: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
let predicate = NSPredicate(format: "TRUEPREDICATE", argumentArray: nil)
let query = CKQuery(recordType: "FoodData", predicate: predicate)
// get just one value only
let operation = CKQueryOperation(query: query)
operation.desiredKeys = ["FoodList"]
// get query
operation.recordFetchedBlock = { (record : CKRecord) in
let menu = record.objectForKey("FoodList") as! [String]
for item in menu {
self.foodArrayLunch.append(item)
}
}
// operation completed
operation.queryCompletionBlock = {(cursor, error) in
dispatch_async(dispatch_get_main_queue()) {
if error == nil {
print("no errors")
} else {
print("error description = \(error?.description)")
}
}
}
database.addOperation(operation)

Related

How to use a predicate for one-to-many managed objects in Core Data

I currently have two managed objects for Core Data that has one-to-many relationship.
Goal
extension Goal {
#nonobjc public class func createFetchRequest() -> NSFetchRequest<Goal> {
return NSFetchRequest<Goal>(entityName: "Goal")
}
#NSManaged public var title: String
#NSManaged public var date: Date
#NSManaged public var progress: NSSet?
}
Progress
extension Progress {
#nonobjc public class func createFetchRequest() -> NSFetchRequest<Progress> {
return NSFetchRequest<Progress>(entityName: "Progress")
}
#NSManaged public var date: Date
#NSManaged public var comment: String?
#NSManaged public var goal: Goal
}
For every goal, you can have multiple Progress objects. The problem is when I request a fetch for Progress with a particular Goal as the predicate, nothing is being returned. I have a suspicion that I'm not using the predicate properly.
This is how I request them.
First, I fetch Goal for a table view controller:
var fetchedResultsController: NSFetchedResultsController<Goal>!
if fetchedResultsController == nil {
let request = Goal.createFetchRequest()
let sort = NSSortDescriptor(key: "date", ascending: false)
request.sortDescriptors = [sort]
request.fetchBatchSize = 20
fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: self.context, sectionNameKeyPath: "title", cacheName: nil)
fetchedResultsController.delegate = self
}
fetchedResultsController.fetchRequest.predicate = goalPredicate
do {
try fetchedResultsController.performFetch()
} catch {
print("Fetch failed")
}
And pass the result to the next screen, Detail view controller:
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
vc.goal = fetchedResultsController.object(at: indexPath)
navigationController?.pushViewController(vc, animated: true)
}
Finally, I fetch Progress using the Goal as the predicate from Detail view controller:
var goal: Goal!
let progressRequest = Progress.createFetchRequest()
progressRequest.predicate = NSPredicate(format: "goal == %#", goal)
if let progress = try? self.context.fetch(progressRequest) {
print("progress: \(progress)")
if progress.count > 0 {
fetchedResult = progress[0]
print("fetchedResult: \(fetchedResult)")
}
}
Goal is being returned properly, but I get nothing back for Progress. I've tried:
progressRequest.predicate = NSPredicate(format: "goal.title == %#", goal.title)
or
progressRequest.predicate = NSPredicate(format: "ANY goal == %#", goal)
but still the same result.
Following is how I set up the relationship:
// input for Progress from the user
let progress = Progress(context: self.context)
progress.date = Date()
progress.comment = commentTextView.text
// fetch the related Goal
var goalForProgress: Goal!
let goalRequest = Goal.createFetchRequest()
goalRequest.predicate = NSPredicate(format: "title == %#", titleLabel.text!)
if let goal = try? self.context.fetch(goalRequest) {
if goal.count > 0 {
goalForProgress = goal[0]
}
}
// establish the relationship between Goal and Progress
goalForProgress.progress.insert(progress)
// save
if self.context.hasChanges {
do {
try self.context.save()
} catch {
print("An error occurred while saving: \(error.localizedDescription)")
}
}
Actually you don't need to refetch the data. You can get the progress from the relationship
Declare progress as native Set
#NSManaged public var progress: Set<Progress>
In DetailViewController delete the fetch code in viewDidLoad and declare
var progress: Progress!
In the first view controller filter the progress
let goal = fetchedResultsController.object(at: indexPath)
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
let progress = goal.progress.first(where: {$0.goal.title == goal.title}) {
vc.progress = progress
navigationController?.pushViewController(vc, animated: true)
}
And consider to name the to-many relationship in plural form (progresses)
I figured out that it's due to Core Data Fault where Core Data lazy loads the data and unless you explicitly access the data, the value will not be displayed.
You can either do something like the following:
let goal = fetchedResultsController.object(at: indexPath)
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController,
let progress = goal.progress.first(where: {$0.goal.title == goal.title}) {
vc.goalTitle = goal.title
vc.date = progress.date
if let comment = progress.comment {
vc.comment = comment
}
navigationController?.pushViewController(vc, animated: true)
}
or setreturnsObjectsAsFaults to false.
Here's a good article on the topic.

Object added in Parse several times after checking for all non similar objects:

I am querying objects from Parse, that I then append into an array. I check if this array contains a particular object, if not, I wish to add this particular object to Parse.
However, for each different object in this array, and therefore in Parse, the particular object gets added.
For instance, if I have four different objects, the object in question will be added four times.
See my code below:
#IBAction func nextWeek(sender: AnyObject) {
self.view.layoutIfNeeded()
UIView.animateWithDuration(0.5, animations: {
self.viewHeight.constant = 93
self.view.layoutIfNeeded() }, completion: nil)
var array2 = [String]()
var query1 = PFQuery(className: "sendMessage")
query1.whereKey("messageSent", equalTo:PFUser.currentUser()!.username!)
query1.whereKey("messageReceived", equalTo:self.nameLabel.text!)
query1.findObjectsInBackgroundWithBlock { (objects1, NSError) -> Void in
if let objects1 = objects1 as? [PFObject] {
for object1 in objects1 {
if object1["message"] != nil {
var textMessage3: String = object1["message"] as! String
var message3 = Message(sender: PFUser.currentUser()!.username!, message: textMessage3, time: NSDate())
array2.append(message3.message)
if contains(array2, self.nextWeekButtonL.titleLabel!.text!) {
} else {
println(array2)
println("it does not contain")
var heyMessage: PFObject = PFObject(className: "sendMessage")
heyMessage["messageSent"] = PFUser.currentUser()?.username
heyMessage["messageReceived"] = self.nameLabel.text
heyMessage.setObject(self.nextWeekButtonL.titleLabel!.text!, forKey: "message")
heyMessage.save()
var message = Message(sender: PFUser.currentUser()!.username!, message: self.nextWeekButtonL.titleLabel!.text!, time: NSDate())
self.array.append(message)
self.tableView.reloadData()
} } } } } }
I have tried to use a dispatch async with main queue, as well as different solutions to add the object to Parse, but the "else" after the check if the array contains, is executed as many times as the array does not contain.
Any idea ?
Thanks a lot,

How can i set a limit to the number of results given in cloudkit

Here is my code for retrieving data from icloud. How do i limit the number of data it retrieves to 25? I saw this question answered before, but i'm not sure how to apply it to my code? can you help?
Here is where i saw the question answered before. Please tell me exactly how to add it to my code or just give an answer with it in the code.
CKQuery from private zone returns only first 100 CKRecords from in CloudKit
CloudKit Batch Fetches?
import UIKit
import CloudKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionView: UICollectionView!
var EventsArray:[String] = [String]()
var container:CKContainer?
var privateDatabase:CKDatabase?
var publicDatabase:CKDatabase?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.container = CKContainer.defaultContainer()
self.privateDatabase = self.container?.privateCloudDatabase
self.publicDatabase = self.container?.publicCloudDatabase
self.collectionView.dataSource = self
self.collectionView.delegate = self
self.LoadEvents()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func LoadEvents() {
let predicate:NSPredicate = NSPredicate(value: true)
let query:CKQuery = CKQuery(recordType: "Data", predicate: predicate)
query.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
if let database = self.publicDatabase {
database.performQuery(query, inZoneWithID: nil, completionHandler: { (records:[AnyObject]!, error:NSError!) in
if error != nil {
self.alert("Error: \(error.localizedDescription)", Message: "Make sure iCloud is turned on and you are connected to the internet")
self.loading.hidden = true
}
else {
dispatch_async(dispatch_get_main_queue()) {
self.EventsArray.removeAll(keepCapacity: false)
for record in records {
let usernameRecord:CKRecord = record as CKRecord
if let event = usernameRecord.objectForKey("Events") as? String{
self.EventsArray.insert(event, atIndex: 0)
}
}
//update data
self.collectionView.reloadData()
self.loading.hidden = true
}
}
})
}}
Instead of performQuery you should use a CKQueryOperation.
Then you can set the result limit like this:
operation.resultsLimit = 25
In this question you can see how to use a CKQueryOperation: CKQuery from private zone returns only first 100 CKRecords from in CloudKit

Deleting all records CloudKit every day from a particular record type

So I want to wipe every record for a particular record type every day. So basically, I want the data to be wiped at 12:00 AM so that it will be fresh for the next day. How would I go about doing this? Is this something that I could set up in the CloudKit dashboard or will I have to set this up programmatically?
Deleting records from the dashboard is a lot of work if you need to delete multiple records.
The best workaround is by creating a separate recordType that will contain one record for every day. Then in the records that you want deleted for that day set up a CKReference to that particular day record and set its action to CKReferenceAction.DeleteSelf
After that you only have to remove the day record and all related records will be removed. Removing that one record could easily be done from the dashboard or you could create functionality in your app or you could create a 2nd app for administrative actions.
func deleteAllRecords()
{
let publicDatabase: CKDatabase = CKContainer.defaultContainer().publicCloudDatabase
// fetch records from iCloud, get their recordID and then delete them
var recordIDsArray: [CKRecordID] = []
let operation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDsArray)
operation.modifyRecordsCompletionBlock = {
(savedRecords: [CKRecord]?, deletedRecordIDs: [CKRecordID]?, error: NSError?) in
print("deleted all records")
}
publicDatabase.add(operation)
}
Try something like this:
let publicDb = CKContainer.defaultContainer().publicCloudDatabase
let query = CKQuery(recordType: "RECORD TYPE", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicDb.performQuery(query, inZoneWithID: nil) { (records, error) in
if error == nil {
for record in records! {
publicDb.deleteRecordWithID(record.recordID, completionHandler: { (recordId, error) in
if error == nil {
//Record deleted
}
})
}
}
}
"RECORD TYPE" should be your record type. Hope this helps.
this code able to delete any amount of records
import CloudKit
class iCloudDelete {
private let cloudDB: CKDatabase
private var recordIDsToDelete = [CKRecordID]()
private var onAllQueriesCompleted : (()->())?
public var resultsLimit = 10 // default is 100
init(cloudDB: CKDatabase){
self.cloudDB = cloudDB
}
func delete(query: CKQuery, onComplete: #escaping ()->Void) {
onAllQueriesCompleted = onComplete
add(queryOperation: CKQueryOperation(query: query))
}
private func add(queryOperation: CKQueryOperation) {
queryOperation.resultsLimit = resultsLimit
queryOperation.queryCompletionBlock = queryDeleteCompletionBlock
queryOperation.recordFetchedBlock = recordFetched
cloudDB.add(queryOperation)
}
private func queryDeleteCompletionBlock(cursor: CKQueryCursor?, error: Error?) {
print("-----------------------")
delete(ids: recordIDsToDelete) {
self.recordIDsToDelete.removeAll()
if let cursor = cursor {
self.add(queryOperation: CKQueryOperation(cursor: cursor))
} else {
self.onAllQueriesCompleted?()
}
}
}
private func recordFetched(record: CKRecord) {
print("RECORD fetched: \(record.recordID.recordName)")
recordIDsToDelete.append(record.recordID)
}
private func delete(ids: [CKRecordID], onComplete: #escaping ()->Void) {
let delete = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: ids)
delete.completionBlock = {
onComplete()
}
cloudDB.add(delete)
}
}

Fetch Record Assets in CloudKit Using Swift

I am new to CloudKit and I am having trouble connecting the assets in my database to the ImageView and TextView in my app. In my CloudKit database, under record types, I created a record named "Companies". I then went to Public Data->Default Zone and then created a new instance of the record called "myCompany". In "myCompany" I have two assets, an image and a .txt file. I want to connect those assets to my app. I am not sure if I need to use CKQuery or what is the best approach. Any advice would be much appreciated. Below is what I have so far. Feel free to give feedback of what I have or if there's a better way, I would love to know. Thanks.
import UIKit
import CloudKit
class LearnViewController: UIViewController {
#IBOutlet weak var theImage: UIImageView!
#IBOutlet weak var theText: UITextView!
let myRecord = CKRecord(recordType: "Companies" )
var image: UIImage!
let database = CKContainer.defaultContainer().publicCloudDatabase
func loadCoverPhoto(completion:(photo: UIImage!) -> ()) {
dispatch_async(
dispatch_get_global_queue(
DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)){
var image: UIImage!
let coverPhoto = self.myRecord.objectForKey("Picture") as CKAsset!
if let asset = coverPhoto {
if let url = asset.fileURL {
let imageData = NSData(contentsOfFile: url.path!)!
image = UIImage(data: imageData)
}
}
completion(photo: image)
}
}
override func viewDidLoad() {
super.viewDidLoad()
loadCoverPhoto() { photo in
dispatch_async(dispatch_get_main_queue()) {
self.theImage.image = photo
}
}
You could get the record directly if you know the recordId by performing a fetch like:
database.fetchRecordWithID(CKRecordID(recordName: recordId), completionHandler: {record, error in
Or if you don't know the ID you should query for the record with something like the code below. just create the right NSPredicate. A query can return more than 1 record.
var query = CKQuery(recordType: recordType, predicate: NSPredicate(value: true))
var operation = CKQueryOperation(query: query)
operation.recordFetchedBlock = { record in
// is this your record...
}
operation.queryCompletionBlock = { cursor, error in
self.handleCallback(error, errorHandler: {errorHandler(error: error)}, completionHandler: {
// ready fetching records
})
}
operation.resultsLimit = CKQueryOperationMaximumResults;
database.addOperation(operation)
In your case when using the fetchRecordWithID option the code would look like this:
override func viewDidLoad() {
super.viewDidLoad()
database.fetchRecordWithID(CKRecordID(recordName: recordId), completionHandler: {record, error in
self.myRecord = record
loadCoverPhoto() { photo in
dispatch_async(dispatch_get_main_queue()) {
self.theImage.image = photo
}
}
}
}

Resources