1
votes

I am revisiting a problem I presented here last year. I didn't ask the question correctly and believe I didn't provide the relevant code. I deleted that question and have rephrased it better (and longishI this time. This time I hope someone can understand my question. Maybe I have been looking too long at the screen to see my errors or iOS 10.2 has implemented new iCloud permissions. I have universal iOS (and macOS version) app that writes and reads text files to iCloud. Writing out the files to iCloud is not a problem. It is reading them back that has got me running around it circles.

(1) If an iPad writes out the file, it can read it back into the app, but it cannot read files written out by an iPhone using the same app. (2) If an iPhone writes out the file, it can read it back into the app, but it cannot read files written out by an iPad using the same app. (3) The Mac can read files written out by iOS devices but iOS devices cannot read the files written out by a macOS device.

Now when an attempt is made to read the file, it fails with error code 260 - That file doesn't exist. This happens for each of the aforementioned steps above. Since it's the same universal app, it has left me completely stupefied. The devices don't add anything specific to the device to the file name. This means I have misunderstood something about the caching of iCloud files on the device. I understood that the iOS (and macOS) take of this automatically.

Here is the code from my iOS project.

This is how I set up the metaDataQuery to get the file URL from iCloud (in iOS project):

//Get list of iCloud files or read a file from iCloud

func iCloud_ListOrReadFiles(_ accountName:String)
{

    //Format for search predicate of document(s) in iCloud storage
    var theFormat     :String

    //List documents or open a document
    if(listMode)
    {
        requestedAccountName = kSuffix  //List all files with suffix kSuffix (= "txt")
        theFormat = "%K ENDSWITH %@"    //Just like all text documents
    } else {
        requestedAccountName = accountName  //Read the file
        theFormat = "%K LIKE %@"
    }

//And now set up the metaDataQuery metadataQuery = NSMetadataQuery() metadataQuery!.predicate = NSPredicate.init(format:theFormat, NSMetadataItemFSNameKey,requestedAccountName!)

    metadataQuery!.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope]

    NotificationCenter.default.addObserver(self,selector:#selector(metadataQueryDidFinishGathering),
                                      name:NSNotification.Name.NSMetadataQueryDidFinishGathering,object:metadataQuery)
    metadataQuery!.start()
}

This is how I process the file URLs returned from iCloud via the metaDataQuery (in iOS project):

func metadataQueryDidFinishGathering(_ notification:Notification) { let query = notification.object! as! NSMetadataQuery

    query.disableUpdates()  //Disable the querying updates
    NotificationCenter.default.removeObserver(self, name:NSNotification.Name.NSMetadataQueryDidFinishGathering, object:query) //And remove from Notifications
    query.stop()    //Final nail in the coffin for this query

    let results = NSArray.init(array: query.results)
    let theCount = query.resultCount

    //Vamoose if nothing found
    if (theCount < 1) {
       return
    }

    if(listMode)    //Just create a list of iCloud files found
    {
        listMode = false

        for i in 0..<theCount
        {
            let account = Accounts()
            account.startDate   = nil
            account.stopDate    = nil
            account.modDate     = nil   //Can't set it below because the compiler is chocking up there.
            account.location    = 2

            let urlString = ((results[i] as AnyObject).value(forAttribute: NSMetadataItemURLKey) as! URL).lastPathComponent
            account.accountName = String( (urlString as NSString).deletingPathExtension)

            listOfAccounts?.add(account)
        }

        //If user wants the list sorted alphabetiucally, then do it
        if(appSettings.bSortingsFlag)
        {
            if( (((listOfAccounts?.count)!-1)) > onDeviceIndex) { //Sort only iCloud accounts
                self.bubbleSortAccountNames(onDeviceIndex, toIndex:((listOfAccounts?.count)!-1))
            }
        }
    } else {        //Came here to read one text file
        ubiquityURL = ((results[0] as AnyObject).value(forAttribute: NSMetadataItemURLKey) as? URL)! //URL of file

        print(String(format:"metadataQueryDidFinishGathering:ubiquityURL = %@", ubiquityURL! as CVarArg)) //Let's see it

        copyFromiCloud2Device(ubiquityURL! as NSURL) //Copy the file from iCloud (in the function below)
}

This is how I read the file from iCloud, using the iCloud URL returned by metaDataQuery. Below the code are the console prints (in iOS project):

/* Copy the text file from iCloud using standard NSFilemanager method copyItemAtURL No UIDocument class used here */ func copyFromiCloud2Device(_ iCloudURL : NSURL) { let nameWithSuffix = iCloudURL.lastPathComponent! //Extract just the file name (and suffix to use for target) let deviceURL = CPLib().fullURLPath(nameWithSuffix, inFolder: nil) //My function to get full path to the Documents folder on device

    print("copyToDeviceDocumentsFolder:iCloudURL \(iCloudURL)") 
    print("copyToDeviceDocumentsFolder:deviceURL \(deviceURL)")

    do {
        try FileManager.default.copyItem(at: iCloudURL as URL, to:deviceURL) //Now copy the file from iCloud

        //Process the contents after 0.25 seconds
        Timer.scheduledTimer(timeInterval: 0.25, target:self, selector:#selector(converText2CoreData), userInfo:nil,repeats:false)
    } catch let error as NSError  { // End up here with error (code 260  = The file doesn't exist)
            print("copyToDeviceDocumentsFolder:nameWithSuffix = \(nameWithSuffix)")
        let noSuffix = String((nameWithSuffix as NSString).deletingPathExtension) //Remove the text suffix because user doesn't need to know that
        let title = String(format:"Copy '%@' from iCloud",noSuffix!)
        let errorDescription = String(format:"Error (%d), %@",error.code, error.localizedFailureReason!)
        CPLib().showAlert(title, message:errorDescription, button:["Done"], calledBy:self, action:nil)
    }
}

These are the print statements in: "metadataQueryDidFinishGathering" and "CopyFromiCloud2Device" (in iOS project):

metadataQueryDidFinishGathering:ubiquityURL = file:///private/var/mobile/Library/Mobile%20Documents/UZMZA52SXK~com~macsoftware~CheckPad/Documents/DemAccount.txt

copyToDeviceDocumentsFolder:iCloudURL file:///private/var/mobile/Library/Mobile%20Documents/UZMZA52SXK~com~macsoftware~CheckPad/Documents/DemAccount.txt copyToDeviceDocumentsFolder:deviceURL file:///var/mobile/Containers/Data/Application/DF9EE5C0-E3EA-444A-839D-C2E8C1D1B408/Documents/DemAccount.txt copyToDeviceDocumentsFolder:Failed to read nameWithSuffix = DemAccount.txt

+++++++++++++ This is the Objective C code used in macOS to read the same text files from iCloud (works):

/* Copy the file from iCloud using standard NSFilemanager method copyItemAtURL and NOT setUbiquitous. No UIDocument implements class used here */ -(void)copyFromiCloud:(NSString *)fileName { NSString *nameWithExtension = [fileName stringByAppendingPathExtension:kTEXTOne]; NSURL *deviceURL = [[CoreDataStuff accountsLocation:nil] URLByAppendingPathComponent:nameWithExtension]; NSURL *iCloudURL = [ubiquityContainerURL URLByAppendingPathComponent:nameWithExtension];

NSFileManager *fileManager = [[NSFileManager alloc] init];
NSError *error = nil;

//Copy the file from iCloud to local directory "Documents" on device
BOOL success = [fileManager copyItemAtURL:iCloudURL toURL:deviceURL error:&error];

if (!success)
    [self showAnAlert:[NSString stringWithFormat:@"Copy %@ from iCloud",fileName]   //Private library call
                message:[NSString stringWithFormat:@"Aborting...%@",[error localizedFailureReason]] altButton:nil];
else {
    [NSTimer scheduledTimerWithTimeInterval:0.25 //Set up a timer to fire up after .25 seconds
                                     target:self
                                   selector:@selector(convertText2CoreData:) //My function to convert the data to CoreData
                                   userInfo:nil
                                    repeats:NO];
}

}

I also noticed that when an iOS device fails to to find the file, this appears in the Xcode console:

**** Running on an iPad or iPhone ****

2017-03-25 20:09:15.543784 CheckPad[405:66745] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /private/var/containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles 2017-03-25 20:09:15.554561 CheckPad[405:66745] [MC] Reading from public effective user settings.

1

1 Answers

0
votes

After reading the Apple iOS overview, I discovered why my iCloud file access was failing. The iOS keeps an 'iCloud container' area on the device. To read from iCloud, the OS looks at this local storage area (also known as ubiquity container)for the file. If it doesn't find it, it returns with error code 260 - That file doesn't exists. If a file has been saved to iCloud before, it just reads it from local storage. If the file has been modified by another device (later date), it downloads that version from iCloud. To solve this, I simply used

"FileManager.default.startDownloadingUbiquitousItem(at: iCloudURL)"

with the URL returned by "metadataQueryDidFinishGathering" then delay it for 2 seconds with:

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(1), execute: { code to download the file here }

and then download the file. This has been working so far.