I'm now using CKQueryOperation and CKModifyRecordsOperation to deal with my records in cloudKit.I find that whether I use cellular or wifi, sometimes it goes well, but sometimes the operations offen appears time out,CKError.Code = 4.Here are my codes:
let operation = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: [])
let config = CKOperation.Configuration()
config.timeoutIntervalForRequest = 10
config.timeoutIntervalForResource = 10
operation.configuration = config
operation.queuePriority = .veryHigh
operation.modifyRecordsCompletionBlock = { [unowned self] (resultRecords, resultRecordIDs, error) in
DispatchQueue.main.async {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
if let error = error {
DispatchQueue.main.async {
self.delegate!.failedWithError(error)
}
} else {
let resultDevice = Device(record: resultRecords!.first!)
self.deviceList.insert(resultDevice, at: 0)
DispatchQueue.main.async {
self.delegate!.succeedWithOperationMode(DeviceModel.OperationMode.insert, indexes: [])
}
}
}
privateDatabase.add(operation)
UIApplication.shared.isNetworkActivityIndicatorVisible = true
Any help would be appreciated.
Related
I'm doing my very first IOS app using Cloud Firestore and have to make the same queries to my database repeatedly. I would like to get rid of the duplicate lines of code. This is examples of func where documents ID are duplicated. Also I using other queries as .delete(), .addSnapshotListener(), .setData(). Should I refactor all that queries somehow or leave them because they were used just for one time?
#objc func updateUI() {
inputTranslate.text = ""
inputTranslate.backgroundColor = UIColor.clear
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { [self] (document, error) in
if let document = document, document.exists {
let document = document
let label = document.data()?.keys.randomElement()!
self.someNewWord.text = label
// Fit the label into screen
self.someNewWord.adjustsFontSizeToFitWidth = true
self.checkButton.isHidden = false
self.inputTranslate.isHidden = false
self.deleteBtn.isHidden = false
} else {
self.checkButton.isHidden = true
self.inputTranslate.isHidden = true
self.deleteBtn.isHidden = true
self.someNewWord.adjustsFontSizeToFitWidth = true
self.someNewWord.text = "Add your first word to translate"
updateUI()
}
}
}
#IBAction func checkButton(_ sender: UIButton) {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, error) in
let document = document
let label = self.someNewWord.text!
let currentTranslate = document!.get(label) as? String
let translateField = self.inputTranslate.text!.lowercased().trimmingCharacters(in: .whitespaces)
if translateField == currentTranslate {
self.inputTranslate.backgroundColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
updateUI()}
} else {
self.inputTranslate.backgroundColor = UIColor.red
self.inputTranslate.shakingAndRedBg()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [self] in
self.inputTranslate.backgroundColor = UIColor.clear
self.inputTranslate.text = ""
}
}
}
}
func deletCurrentWord () {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.getDocument { (document, err) in
let document = document
if let err = err {
print("Error getting documents: \(err)")
} else {
let array = document!.data()
let counter = array!.count
if counter == 1 {
// The whole document will deleted together with a last word in list.
let user = Auth.auth().currentUser?.email
self.db.collection(K.FStore.collectionName).document(user!).delete() { err in
if let err = err {
print("Error removing document: \(err)")
} else {
self.updateUI()
}
}
} else {
// A current word will be deleted
let user = Auth.auth().currentUser?.email
let wordForDelete = self.someNewWord.text!
self.db.collection(K.FStore.collectionName).document(user!).updateData([
wordForDelete: FieldValue.delete()
]) { err in
if let err = err {
print("Error updating document: \(err)")
} else {
self.updateUI()
}
}
}
}
}
}
Another query example
func loadMessages() {
let user = Auth.auth().currentUser?.email
let docRef = db.collection(K.FStore.collectionName).document(user!)
docRef.addSnapshotListener { (querySnapshot, error) in
self.messages = []
if let e = error {
print(e)
} else {
if let snapshotDocuments = querySnapshot?.data(){
for item in snapshotDocuments {
if let key = item.key as? String, let translate = item.value as? String {
let newMessage = Message(key: key, value: translate)
self.messages.append(newMessage)
}
}
DispatchQueue.main.async {
self.messages.sort(by: {$0.value > $1.value})
self.secondTableView.reloadData()
let indexPath = IndexPath(row: self.messages.count - 1, section: 0)
self.secondTableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
}
}
}
enum Error {
case invalidUser
case noDocumentFound
}
func fetchDocument(onError: #escaping (Error) -> (), completion: #escaping (FIRQueryDocument) -> ()) {
guard let user = Auth.auth().currentUser?.email else {
onError(.invalidUser)
return
}
db.collection(K.FStore.collectionName).document(user).getDocument { (document, error) in
if let error = error {
onError(.noDocumentFound)
} else {
completion(document)
}
}
}
func updateUI() {
fetchDocument { [weak self] error in
self?.hideShowViews(shouldHide: true, newWordText: nil)
} completion: { [weak self] document in
guard document.exists else {
self?.hideShowViews(shouldHide: true, newWordText: nil)
return
}
self?.hideShowViews(shouldHide: false, newWordText: document.data()?.keys.randomElement())
}
}
private func hideShowViews(shouldHide: Bool, newWordText: String?) {
checkButton.isHidden = shouldHide
inputTranslate.isHidden = shouldHide
deleteBtn.isHidden = shouldHide
someNewWord.adjustsFontSizeToFitWidth = true
someNewWord.text = newWordText ?? "Add your first word to translate"
}
The updateUI method can easily be refactored using a simple guard statement and then taking out the common code into a separate function. I also used [weak self] so that no memory leaks or retain cycles occur.
Now, you can follow the similar approach for rest of the methods.
Use guard let instead of if let to avoid nesting.
Use [weak self] for async calls to avoid memory leaks.
Take out the common code into a separate method and use a Bool flag to hide/show views.
Update for step 3:
You can create methods similar to async APIs for getDocument() or delete() etc and on completion you can update UI or perform any action. You can also create a separate class and move the fetchDocument() and other similar methods in there and use them.
I'm tracking down a CloudKit error of 'Failed to modify some records.'
How can I throw this error so that I can test my error handling code?
Is there a property of CKRecord I can set to force it to fail?
Code is currently something like:
var someRecords = [CKRecord]()
for i in (1...10) {
let record = CKRecord(recordType: "Track", recordID: CKRecord.ID(zoneID: recordZone.zoneID))
...
someRecords.append(record)
}
let operation = CKModifyRecordsOperation(recordsToSave: someRecords, recordIDsToDelete: nil)
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecords, error in
DispatchQueue.main.async {
if !self.handleError(error) { ... }
}
}
You need an Atomic zone (as I remember)
Try to save a big amount of objects (as I remember: it will allow you to use no more < 500)
I hope this will help you
var someRecords = [CKRecord]()
for i in (1...1000) {
let record = CKRecord(recordType: "Track", recordID: CKRecord.ID(zoneID: recordZone.zoneID))
...
someRecords.append(record)
}
let operation = CKModifyRecordsOperation(recordsToSave: someRecords, recordIDsToDelete: nil)
operation.isAtomic = true
operation.modifyRecordsCompletionBlock = { savedRecords, deletedRecords, error in
DispatchQueue.main.async {
if !self.handleError(error) { ... }
}
}
This is a method I call to fetch public records:
private func fetchPublicRecordZonesChanges(completion: ErrorHandler?) {
let zone = CKRecordZone.default()
let options = CKFetchRecordZoneChangesOptions()
options.previousServerChangeToken = nil
var records = [CKRecord]()
var recordIDsToDelete = [CKRecordID]()
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: [zone.zoneID], optionsByRecordZoneID: [zone.zoneID: options])
operation.database = CloudAssistant.shared.publicDatabase
operation.recordChangedBlock = { record in
records.append(record)
}
operation.recordWithIDWasDeletedBlock = { recordID, string in
recordIDsToDelete.append(recordID)
}
operation.recordZoneChangeTokensUpdatedBlock = { _, token, _ in
if let token = token {
Token.temporaryPublicZoneServerChangeToken = token
}
}
operation.recordZoneFetchCompletionBlock = { [weak self] _, token, _, _, error in
if let error = error, error.isTokenExpiredError {
UserDefaults.remove(forKey: PublicZoneServerChangeTokenKey)
self?.fetchPublicRecordZonesChanges(completion: completion)
return
}
if let token = token {
Token.temporaryPublicZoneServerChangeToken = token
}
}
operation.fetchRecordZoneChangesCompletionBlock = { [weak self] error in
self?.save(records: records, recordIDsToDelete: recordIDsToDelete) { error in
completion?(error)
}
}
operationQueue.addOperation(operation)
}
But nothing is fetched although that method is called, additionally not even once recordChangedBlock closure is called. Why?
I am pretty sure I have a records there:
and environment is also very fine, because private development records are fetched correctly (using of course different method). What am I doing wrong?
To be notified of changes to your Public database, create a CKQuerySubscription for the record type(s) you care about. Here's an example:
let subscription = CKQuerySubscription(
recordType: "Question",
predicate: NSPredicate(value: true),
subscriptionID: "subscriptionQuestion",
options: [
.firesOnRecordCreation,
.firesOnRecordUpdate,
.firesOnRecordDeletion
])
let info = CKNotificationInfo()
info.shouldSendContentAvailable = true
subscription.notificationInfo = info
let operation = CKModifySubscriptionsOperation(subscriptionsToSave: [subscription], subscriptionIDsToDelete: nil)
operation.modifySubscriptionsCompletionBlock = { saved, deleted, error in
if let error = error{
print("Add subscription error: \(error)")
}else{
print("Successfully added Question subscription.")
}
}
//:::
let container = CKContainer(identifier: "...")
container.publicCloudDatabase.add(operation)
I am trying to cancel my NSOperationsQueue and remove all the operations from the queue. I called the method "cancelAllOperations()" but this doesnt remove the operation from queue, I am aware that this method only adds a flag to the operation.
How do I go about removing all operations from my queue? Or prevent the operation queue from executing operations that have a flag?
private let mediaDownloadOperationQueue: NSOperationQueue = {
let queue = NSOperationQueue()
queue.maxConcurrentOperationCount = 1
queue.qualityOfService = .Background
return queue
}()
func startDownloadProcess() {
guard downloadSwitchState == true else { return }
let context = DataBaseManager.sharedInstance.mainManagedObjectContext
let mediasToDownload = self.listOfMediaToDownload(context)
for media in mediasToDownload {
downloadMedia(media)
}
}
private func downloadMedia(media: Media) {
//check if operation already exist
for operation in mediaDownloadOperationQueue.operations {
let operation = operation as! MediaDownloadOperation
if operation.media.objectID == media.objectID { return }
}
//HERE I am adding the operation to queue
mediaDownloadOperationQueue.addOperation(MediaDownloadOperation(media: media))
}
//EDIT: Here is my MediasDownloadOperation
import Foundation
import Alamofire
class MediaDownloadOperation: BaseAsyncOperation {
let media: Media
init(media: Media) {
self.media = media
super.init()
}
override func main() {
guard let mediaSourceURI = media.sourceURI
else {
self.completeOperation()
return
}
var filePath: NSURL?
let destination: (NSURL, NSHTTPURLResponse) -> (NSURL) = {
(temporaryURL, response) in
if let directoryURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first, let suggestedFilename = response.suggestedFilename {
filePath = directoryURL.URLByAppendingPathComponent("\(suggestedFilename)")
return filePath!
}
return temporaryURL
}
RequestManager.sharedAlamofireManager.download(.GET, mediaSourceURI, destination: destination).response {
(request, response, data, error) -> Void in
if let filePath = filePath where error == nil {
let saveToCameraRoll = self.media.saveToCameraRoll?.boolValue == true
self.media.fileLocation = filePath.lastPathComponent
self.media.saveToCameraRoll = false
DataBaseManager.sharedInstance.save()
if saveToCameraRoll {
self.media.saveMediaToCameraRoll(nil)
}
}
self.completeOperation()
NSNotificationCenter.defaultCenter().postNotificationName(MediaDownloadManager.MediaIsDownloadedNote, object: nil)
}
}
}
Adding a conditional to test to see if operation has cancelled seemed to have solved the problem:
RequestManager.sharedAlamofireManager.download(.GET, mediaSourceURI, destination: destination).response {
(request, response, data, error) -> Void in
//Only download if logged in
if (self.cancelled == false){
print("RAN operation!")
if let filePath = filePath where error == nil {
let saveToCameraRoll = self.media.saveToCameraRoll?.boolValue == true
self.media.fileLocation = filePath.lastPathComponent
self.media.saveToCameraRoll = false
DataBaseManager.sharedInstance.save()
if saveToCameraRoll {
self.media.saveMediaToCameraRoll(nil)
}
}
self.completeOperation()
NSNotificationCenter.defaultCenter().postNotificationName(MediaDownloadManager.MediaIsDownloadedNote, object: nil)
}else{
print("did not run operation! was cancelled!")
self.completeOperation()
}
}
Client received error 1298: This operation has been rate limited error from CloudKit when downloading records with CKQueryOperation, only once, during Apple review. How can I fix this issue?
Here is to code, nothing special:
let query = CKQuery(recordType: "Movie", predicate: NSPredicate(format: "creationDate > %#", d!))
let qo = CKQueryOperation(query: query)
let fb: (CKRecord!) -> () = {record in
temporaryContext.performBlockAndWait({
let fr = NSFetchRequest(entityName: "Movie")
fr.predicate = NSPredicate(format: "recordName = %#", record.recordID.recordName)
let a = temporaryContext.executeFetchRequest(fr, error: nil) as! [Movie]
if a.count == 0 {
let m = NSEntityDescription.insertNewObjectForEntityForName("Movie", inManagedObjectContext: temporaryContext) as! Movie
m.title = record.valueForKey("title") as! String
m.image = (record.valueForKey("image") as! CKAsset).fileURL.description
m.imageSize = Int32(record.valueForKey("imageSize") as! Int)
m.recordName = record.recordID.recordName
}
})
}
let c: ()->() = {
temporaryContext.performBlockAndWait({
let success = temporaryContext.save(nil)
})
Utility.managedObjectContext().performBlockAndWait({
let success = Utility.managedObjectContext().save(nil)
})
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "moviesDownloaded")
NSUserDefaults.standardUserDefaults().synchronize()
dispatch_semaphore_signal(self.sema)
}
let cb: (CKQueryCursor!, NSError!) -> () = {cursor, error in
if error == nil {
if cursor != nil {
let qo2 = Utility.qo(cursor, recordFetchedBlock: fb, completion: c)
publicDatabase.addOperation(qo2)
} else {
c()
}
} else {
Utility.log("error 1298: \(error.localizedDescription)")
dispatch_sync(dispatch_get_main_queue(), {
self.status.backgroundColor = UIColor.orangeColor()
})
NSThread.sleepForTimeInterval(0.5)
dispatch_semaphore_signal(self.sema)
}
}
qo.recordFetchedBlock = fb
qo.queryCompletionBlock = cb
publicDatabase.addOperation(qo)
dispatch_semaphore_wait(self.sema, DISPATCH_TIME_FOREVER)
I try to put this whole code into a loop like:
for i in 1 ... 2 {
var rateLimited = false
...
if error == nil {
} else {
NSThread.sleepForTimeInterval(3)
rateLimited = true
}
...
if !rateLimited {
break
}
}
Do you think it will work?
If you get CKErrorRequestRateLimited the error will have a CKErrorRetryAfterKey key in the error's userInfo dictionary. You should wait at least that amount of time before retrying your operation.
Waiting with a sleep is a bad idea because it can cause unexpected hangs in your application, especially if that code runs on your main thread. Use dispatch_after or a NSTimer to re-send your operation.
You will also get this error if you are not logged in to your iCloud account.