GTLRDriveQuery_FilesCreate (Google REST API) - ios

1.I need to create a folder in google drive.And I use this code to do this(official code from google developers page):
GTLRDrive_File *metadata = [GTLRDrive_File object];
metadata.name = #"Invoices";
metadata.mimeType = #"application/vnd.google-apps.folder";
GTLRDriveQuery_FilesCreate *query = [GTLRDriveQuery_FilesCreate queryWithObject:metadata
uploadParameters:nil];
query.fields = #"id";
[driveService executeQuery:query completionHandler:^(GTLRServiceTicket *ticket,
GTLRDrive_File *file,
NSError *error) {
if (error == nil) {
NSLog(#"File ID %#", file.identifier);
} else {
NSLog(#"An error occurred: %#", error);
}
}];
And the folder creates fine!
The problem is that each time I run the App , the code creates dublicates of the folder in my Google Drive.But I need to create only one folder of that name and kind.
I know its a rookie question, but I cannot figure it out how to do this.And I know that I should compare( GTLRDrive_File *file) identifier (as it is a unique string),but how do I do it?
2.So the question is :How do check if the identifier is already created and compare it ?
I understand that to fulfil my tsk I should check if the Identifier is exists,if not I should create it, and if its doesn't exist do nothing.

The Google Drive API does allow multiple files with duplicate names. File/folder names are not unique, but identifiers are.
What you likely want to do is:
Check if a folder with the same name exists
If not, then create the folder
If it already exists, then use the identifier of the existing folder.
How to check if a folder exists. This is swift, but it's trivial to convert it to ObjC
func getFolderID(name: String, completion: #escaping (String?) -> Void) {
let query = GTLRDriveQuery_FilesList.query()
// Add "and '\(self.user.profile!.email!)' in owners" to only list folders owned by a specific GIDGoogleUser.
query.q = "mimeType = 'application/vnd.google-apps.folder' and name = '\(name)'"
driveService.executeQuery(query) { (_, queryResults, error) in
if let error = error {
fatalError(error.localizedDescription)
}
let fileList = queryResults as! GTLRDrive_FileList
completion(fileList.files?.first?.identifier)
}
}
And then you'd call getFolderID in this manner:
getFolderID(name: "my-folder-name") { (folderID) in
if let folderID = folderID {
print("Found folder with ID: \(folderID)")
} else {
print("Folder does not exist")
}
}
File name search is case insensitive.

The simplest way is to do a files.get of the id. It will return status 200 and the file object if it's there, or 404 not found.

Related

Identify Type of Record Change when Retrieving Changed CloudKit Records

I am attempting to complete an app using CloudKit synchronization and local CoreData. Most of the operations work as expected but I cannot find the methodology for determining the type of change that is reported by CloudKit. I get the changed records, but I need to know if the change was an edit, a new record or a deletion. Any guidance would be appreciated.
Here's the piece of code that I thought could be configured to identify the type of edit I would need to make to CoreData. Xcode 10.2.1 iOS 12.2 Swift (Latest)
func fetchZoneChangesInZones( _ zones : [CKRecordZone.ID], _ completionHandler: #escaping (Error?) -> Void) {
var fetchConfigurations = [CKRecordZone.ID : CKFetchRecordZoneChangesOperation.ZoneConfiguration]()
for zone in zones {
if let changeToken = UserDefaults.standard.zoneChangeToken(forZone: zone) {
let configuration = CKFetchRecordZoneChangesOperation.ZoneConfiguration(previousServerChangeToken: changeToken, resultsLimit: nil, desiredKeys: nil)
fetchConfigurations[zone] = configuration
}//if let changeToken
}//for in
let operation = CKFetchRecordZoneChangesOperation(recordZoneIDs: zones, configurationsByRecordZoneID: fetchConfigurations)
operation.fetchAllChanges = true
var changedPatients = [CKRecord]()
var changedCategory1s = [CKRecord]()
//I thought that I should be able to query for the change type here and make separate arrays for each change type
operation.recordChangedBlock = { record in
if record.recordType == "Patient" {
changedPatients.append(record)
}
}//recordChangedBlock
operation.fetchRecordZoneChangesCompletionBlock = { [weak self] error in
for record in changedPatients {
//my actions here - need to choose new, changed or delete
self!.saveCKRecordToCoreData(record: record)
}//for record in
completionHandler(error)
}//fetchRecordZoneChangesCompletionBlock
operation.recordZoneFetchCompletionBlock = { recordZone, changeToken, data, moreComing, error in
UserDefaults.standard.set(changeToken, forZone: recordZone)
}//recordZoneFetchCompletionBlock
privateDatabase.add(operation)
}//fetchZoneChangesInZones
I'm not that good in swift but I will post in objective c so you can convert this into swift
First things first, if you want to notify if a record has been edited, deleted or created you need register for push notifications.
Then Subscribe to update add this block in didFinishLaunchingWithOptions
- (void)subscribeToEventChanges
{
BOOL isSubscribed = [[NSUserDefaults standardUserDefaults] boolForKey:#"subscribedToUpdates"];
if (isSubscribed == NO) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"TRUEPREDICATE"];
CKQuerySubscription *subscription = [[CKQuerySubscription alloc] initWithRecordType:#"Patient" predicate:predicate options:CKQuerySubscriptionOptionsFiresOnRecordCreation | CKQueryNotificationReasonRecordDeleted | CKQueryNotificationReasonRecordUpdated];
CKNotificationInfo *CKNotification=[[CKNotificationInfo alloc]init];
CKNotification.shouldSendContentAvailable=YES;
CKNotification.soundName=#"";
subscription.notificationInfo=CKNotification;
CKDatabase *publicDatabase = [[CKContainer containerWithIdentifier:#"your container identifir"] privateCloudDatabase];
[publicDatabase saveSubscription:subscription completionHandler:^(CKSubscription * _Nullable subscription, NSError * _Nullable error) {
if (error) {
// Handle here the error
} else {
// Save that we have subscribed successfully to keep track and avoid trying to subscribe again
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"subscribedToUpdates"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}];
}
}
You will get notified in didReceiveRemoteNotification
Here is a piece of code
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
CKNotification *cloudKitNotification = [CKNotification notificationFromRemoteNotificationDictionary:userInfo];
if (cloudKitNotification.notificationType == CKNotificationTypeQuery) {
CKQueryNotification *queryNotification = (CKQueryNotification *)cloudKitNotification;
if (queryNotification.queryNotificationReason == CKQueryNotificationReasonRecordDeleted) {
// If the record has been deleted in CloudKit then delete the local copy here
} else {
// If the record has been created or changed, we fetch the data from CloudKit
CKDatabase *database;
if (queryNotification.databaseScope) {
database = [[CKContainer containerWithIdentifier:#"your container identifier"] privateCloudDatabase];
}
[database fetchRecordWithID:queryNotification.recordID completionHandler:^(CKRecord * _Nullable record, NSError * _Nullable error) {
if (error) {
// Handle the error here
} else {
if (queryNotification.queryNotificationReason == CKQueryNotificationReasonRecordUpdated) {
// Use the information in the record object to modify your local data
}else{
// Use the information in the record object to create a new local object
}
}
}];
}
}
}
The solution is a separate method on the version of the operation that is being used. I was already getting notifications, I just could not tell whether they were update, create or delete. Update and create can be handled simply by searching core data for the recordName(which is a UUID). If found, then edit, if not create. The issue is deletion - using the fetchRecordZoneChangesCompletionBlock could not identify deletions. However, the operation family has a method just to report deletions - operation.recordWithIDWasDeletedBlock. I modified the former code and added the deletion code as shown below.
My single database subscription covers the entire private database so it is not necessary to subscribe to every record type.
operation.fetchRecordZoneChangesCompletionBlock = { error in
for record in changedPatients {
//search for the record in coredata
if self.isSingleCoreDataRecord(ckRecord: record) {
//if found - then modify
self.saveUpdatedCloudKitRecordToCoreData(record: record)
} else {
//else add new
self.saveCKRecordToCoreData(record: record)
}
}//for record in
completionHandler(error)
}//fetchRecordZoneChangesCompletionBlock
operation.recordWithIDWasDeletedBlock = { (recordID, recordType) in
//delete the core data record here
let ckRecordToDelete = CKRecord(recordType: recordType, recordID: recordID)
self.removeOnePatientRecordFromCoreData(ckRecord: ckRecordToDelete)
}//recordWithIDWasDeletedBlock

Error after converting objective-c code to swift 3.0.2 Google Drive Rest API

The problem is quite simple.I have a code that works fine in Objective-C, but I need the same code to work in swift.With the code below I am uploading file named"photo.png" to my Google Drive from Documents folder on iPhone.(I deleted the part with setting Documents directory folder to save some space)
Here is the original code:
-(void)uploadPhoto{
metadata.parents = ids;
GTLRUploadParameters *uploadParameters;
uploadParameters = [GTLRUploadParameters uploadParametersWithData:photoData MIMEType:#"image/png"];
uploadParameters.shouldUploadWithSingleRequest = TRUE;
GTLRDriveQuery_FilesCreate *query = [GTLRDriveQuery_FilesCreate queryWithObject:metadata
uploadParameters:uploadParameters];
query.fields = #"id";
[self.service executeQuery:query completionHandler:^(GTLRServiceTicket *ticket,
GTLRDrive_File *file,
NSError *error) {
if (error == nil) {
NSLog(#"File photo ID %#", file.identifier);
} else {
NSLog(#"An error occurred: %#", error);
}
}];
}
Most of it I got form Google developers.IN Objective - Its working fine!
Here is what In got in swift:
func uploadPhotos()
{
var ids : NSArray!
metadata.parents = ident as! [String]?
var uploadParameters = GTLRUploadParameters(data:photoData! as Data,mimeType:"image/png")
uploadParameters.shouldUploadWithSingleRequest = true
var query :GTLRDriveQuery_FilesCreate
query = GTLRDriveQuery_FilesCreate.query(withObject: metadata, uploadParameters: uploadParameters)
query.fields = "id"
let vc:DriveListTableViewController
self.service.executeQuery(query,completionHandler :{(ticket: GTLRServiceTicket!,
file: GTLRDrive_File!,
error: Error!)-> Void in
if error == nil {
print("File photo ID \(file.identifier)")
}
else {
print("An error occurred: \(error)")
}
})
}
The Error I get is in the the line with self.service.exequteQuery
swift:314:59: Cannot convert value of type '(GTLRServiceTicket!, GTLRDrive_File!, Error!) -> Void' to expected argument type 'GTLRServiceCompletionHandler?'
Please help me solve that Error! Have tried different variants!
Refer with this SO thread. You may try changing the unwrapping of the parameters in the closure. Maybe this is required to match what the Objective-C implementation of the function is expecting.
You may also check this additional references:
Swift 2 to Swift 3: Cannot convert value of type '(Data?, NSError?) -> Void' to to expected argument type 'GTMSessionFetcherCompletionHandler?'
As per SE-0112, NSError is now bridged to Swift as the Error protocol. In fact, if you ⌥ + click on the GTMSessionFetcherCompletionHandler type in Swift, you'll see exactly how it's bridged:
typealias GTMSessionFetcherCompletionHandler = (Data?, Error?) -> Void
Swift error Cannot convert value of type '(AFHTTPRequestOperation?, AnyObject?) -> ()
Cannot convert value of type (PFUser!, NSError) void to expected argument type PFUserResultBlock

iOS9 Contacts Framework get identifier from newly saved contact

I need the identifier of a newly created contact directly after the save request. The use case: Within my app a user creates a new contact and give them some attributes (eg. name, address ...) after that he can save the contact. This scenario is working as aspected. My code looks like this:
func createContact(uiContact: Contact, withImage image:UIImage?, completion: String -> Void)
{
let contactToSave = uiContact.mapToCNContact(CNContact()) as! Cnmutablecontawctlet
if let newImage = image
{
contactToSave.imageData = UIImageJPEGRepresentation(newImage, 1.0)
}
request = CNSaveRequest()
request.addContact(contactToSave, toContainerWithIdentifier: nil)
do
{
try self.contactStore.executeSaveRequest(request)
print("Successfully saved the CNContact")
completion(contactToSave.identifier)
}
catch let error
{
print("CNContact saving faild: \(error)")
completion(nil)
}
}
The Contact Object (uiContact) is just an wrapper of CNContact.
In the closure completion I need to return the identifier but on this time I have no access to them, because he is creating by the system after the write process.
One solution could be to fetch the newly saved CNContact with predicate
public func unifiedContactsMatchingPredicate(predicate: NSPredicate, keysToFetch keys: [CNKeyDescriptor]) throws -> [CNContact]
but this seems to me like a bit unclean because this contact could have only a name and more than one could exist. Something like a callback with the created identifier would be nice. But there isn´t.
Is there a other way to solve this problem?
This may be a little late but in case someone needs this.
By using the latest SDK (iOS 11), I was able to get the identifier just by:
NSError *error = nil;
saveReq = [[CNSaveRequest alloc] init];
[saveReq addContact:cnContact toContainerWithIdentifier:containerIdentifier];
if (![contactStore executeSaveRequest:saveReq error:&error]) {
NSLog(#"Failed to save, error: %#", error.localizedDescription);
}
else
{
if ([cnContact isKeyAvailable:CNContactIdentifierKey]) {
NSLog(#"identifier for new contact is: %#", cnContact.identifier);
// this works for me everytime
} else {
NSLog(#"CNContact identifier still isn't available after saving to address book");
}
}
swift 4
This is the way to get contact id when creating contact
do {
try store.execute(saveRequest)
if contactToAdd.isKeyAvailable(CNContactIdentifierKey) {
print(contactToAdd.identifier) // here you are getting identifire
}
}
catch {
print(error)
}

Downloading Files from Kinvey in iOS

I have read documents over kinvey here:
http://devcenter.kinvey.com/ios/guides/files#DownloadingtoCachesDirectory
They have provided sample function as well:
[KCSFileStore downloadData:#"myId" completionBlock:^(NSArray *downloadedResources, NSError *error) {
if (error == nil) {
KCSFile* file = downloadedResources[0];
NSData* fileData = file.data;
id outputObject = nil;
if ([file.mimeType hasPrefix:#"text"]) {
outputObject = [[NSString alloc] initWithData:fileData encoding:NSUTF8StringEncoding];
} else if ([file.mimeType hasPrefix:#"image"]) {
outputObject = [UIImage imageWithData:fileData];
}
NSLog(#"downloaded: %#", outputObject);
} else {
NSLog(#"Got an error: %#", error);
}
} progressBlock:nil];
But when i replaced "myId" with file id it gave me this error:
Error Domain=KCSServerErrorDomain Code=401 "The credentials used to authenticate this request are not authorized to run this operation. Please retry your request with appropriate credentials"
While i can access other collections i have created over kinvey (same user) with same credentials (secret,api_key)
Is there any other requirement before calling this function?
Thanks
I had the same problem. I reached out and got help from the kinvey support team.
I solved the problem by setting the "acl" for each file that I upload. According to the iOS Files guide under upload options: "By default the file will only be available to the creating user.", this was what was causing the problem for me.
I upload files like this now:
let metadata = KCSMetadata()
metadata.setGloballyReadable(true)
let data = UIImagePNGRepresentation(image)
KCSFileStore.uploadData(
data,
options: options: [KCSFileACL : metadata],
completionBlock: {KCSFileUploadCompletionBlock!},
progressBlock: nil)
This allows anyuser to read that specific file. I Use a KCSUser.createAutogeneratedUser() to read the files afterwards.
Yes, you will have access to other collections under the same kinvey app, but the permissions needs to be set for the files collection. So the file you describe as "myId" needs to have the proper permissions set, before it can be downloaded by another user than the user that upload the file.
You must be an active user for calling this function. You just need to make sure you have successfully logged in.

How to Clear PFFIle Cache (Parse.com)

I have an app that downloads and displays a lot of images from parse. An image is added to the database almost every minute. Since PFFile is automatically cached with no expiration date, even though I only need to display recent images, the older images still stay in cache thus occupying a lot of storage space. Because of this now the app takes about 5GB of storage on my iPhone. I have been doing a lot of research on this issue and found out that Parse does not have a public api for cleaning up PFFile Cache and also it doesn't allow setting expiration date on the cached files. Is there a workaround on this where I could manually delete older cache data?
Thank you in advance.
Here is a method you can use to clean up the PFFile Cache. If you call it when your app starts, before initializing Parse, I think it should be safe. You can check the file creation date in the for loop if you don't want to remove everything.
+ (void)cleanUpPFFileCacheDirectory
{
NSError *error = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *cacheDirectoryURL = [[fileManager URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject];
NSURL *PFFileCacheDirectoryURL = [cacheDirectoryURL URLByAppendingPathComponent:#"Parse/PFFileCache" isDirectory:YES];
NSArray *PFFileCacheDirectory = [fileManager contentsOfDirectoryAtURL:PFFileCacheDirectoryURL includingPropertiesForKeys:nil options:0 error:&error];
if (!PFFileCacheDirectory || error) {
if (error && error.code != NSFileReadNoSuchFileError) {
NSLog(#"Error : Retrieving content of directory at URL %# failed with error : %#", PFFileCacheDirectoryURL, error);
}
return;
}
for (NSURL *fileURL in PFFileCacheDirectory) {
BOOL success = [fileManager removeItemAtURL:fileURL error:&error];
if (!success || error) {
NSLog(#"Error : Removing item at URL %# failed with error : %#", fileURL, error);
error = nil;
}
}
}
TimWhiting's answer translated to Swift 2.1:
Note: I have to say thought that is better to use file urls and use your own cache system as Matt S says, I'm using this just for testing purposes. I wish also that Parse could provide us with the correct path instead of having to hardcode it, that's why I think is better to use URLs.
func cleanUpParseDirectory(){
let parseFilesCache = "Parse/PFFileCache"
let fileManager = NSFileManager.defaultManager()
let cacheDirectoryURL = fileManager.URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask)
let PFFileCacheDirectoryURL = cacheDirectoryURL[0].URLByAppendingPathComponent(parseFilesCache, isDirectory: true)
do {
let PFFileCacheDirectory = try fileManager.contentsOfDirectoryAtURL(PFFileCacheDirectoryURL, includingPropertiesForKeys: nil, options: [])
print("number of cached files: \(PFFileCacheDirectory.count)")
for fileURL in PFFileCacheDirectory {
try fileManager.removeItemAtURL(fileURL)
}
print("Success removing items")
} catch let error {
print("Error: \(error)")
}
}
Per this answer from Hector: https://www.parse.com/questions/pffile-cache-size you can manually clear your applications ~/Library/Caches folder if you are insistent upon it. However, I'm fairly certain this will also impact things like NSURL/AFNetworking caches, amongst others.
My suggestion? Don't use PFFile to download the file. PFFile gives you back the remote URL to where it's hosted on Parse's site, so you can pass that to something like AFNetworking or NSURLSession to actually download the image for you, and then you can then assign cache lifetimes (or manage it yourself) since those systems actually support that, unlike PFFile.
deadbeef's answer translated to Swift for anyone that needs it.
func cleanUpParseDirectory(){
var error: NSError?
var fileManager: NSFileManager = NSFileManager.defaultManager()
var cacheDirectoryURL: [NSURL] = fileManager.URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask) as! [NSURL]
var PFFileCacheDirectoryURL: NSURL = cacheDirectoryURL[0].URLByAppendingPathComponent("Parse/PFFileCache", isDirectory: true)
var PFFileCacheDirectory: [AnyObject]? = fileManager.contentsOfDirectoryAtURL(PFFileCacheDirectoryURL, includingPropertiesForKeys: nil, options: NSDirectoryEnumerationOptions.allZeros, error: &error)// [fileManager contentsOfDirectoryAtURL:PFFileCacheDirectoryURL includingPropertiesForKeys:nil options:0 error:&error];
if (PFFileCacheDirectory == nil || error != nil) {
if ((error != NSFileReadNoSuchFileError && error!.code != NSFileReadNoSuchFileError)) {
println("error finding path")
} else {
println("no error finding path")
}
return
}
println("number of cached files: \(PFFileCacheDirectory!.count)")
for fileURL in PFFileCacheDirectory! {
var success: Bool = fileManager.removeItemAtURL(fileURL as! NSURL, error: &error)
if ((!success != false || error != nil) ) {
println("error removing item")
error = nil
} else {
println("success removing item")
}
}
}

Resources