SetUbiquitous showing 'file already exists' for file that doesn't - ios

In AppDelegate.swift, on first launch, the intent is to place some sample docs in the local Documents folder, or in the iCloud Documents folder if iCloud is enabled.
var templates = NSBundle.mainBundle().pathsForResourcesOfType(AppDelegate.myExtension, inDirectory: "Templates")
dispatch_async(appDelegateQueue) {
self.ubiquityURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)
if self.ubiquityURL != nil && templates.count != 0 {
// Move sample documents from Templates to iCloud directory on initial launch
for template in templates {
let tempurl = NSURL(fileURLWithPath: template)
let title = tempurl.URLByDeletingPathExtension?.lastPathComponent
let ubiquitousDestinationURL = self.ubiquityURL?.URLByAppendingPathComponent(title!).URLByAppendingPathExtension(AppDelegate.myExtension)
// let exists = NSFileManager().isUbiquitousItemAtURL(ubiquitousDestinationURL!)
do {
try NSFileManager.defaultManager().setUbiquitous(true, itemAtURL: tempurl, destinationURL: ubiquitousDestinationURL!)
}
catch let error as NSError {
print("Failed to move file \(title!) to iCloud: \(error)")
}
}
}
return
}
Before running this, I delete the app from the device and make sure no doc of that name is in iCloud. On first launch, without iCloud, the sample docs copy properly into the local Documents folder. With iCloud, this code runs, and the setUbiquitous call results in an error that says the file already exists. The commented call to isUbiquitousItemAtURL also returns true.
What might be making these calls register that a file exists that I'm pretty sure doesn't? Thank you!

The file already exists, so just replace it

The primary solution...in all the trial and error, I'd forgotten to put "Documents" back in the url. Should be:
let ubiquitousDestinationURL = self.ubiquityURL?.URLByAppendingPathComponent("Documents").URLByAppendingPathComponent(title!).URLByAppendingPathExtension(AppDelegate.myExtension)
Without that, wrote the file to the wrong directory, and so I couldn't see it by normal means.

Related

Move file from app to phone documents folder

So in my app I have created a Test.JSON file that I want the user to be able to move to the documents directory, outside of the app. I understand I have to do this by using UIDocumentPickerViewController, but haven't found any way to proceed. I have created the Test.JSON file, and can use it from variable data.
I have this following code to open the UIDocumentPickerViewController:
let documentPicker =
UIDocumentPickerViewController(forExporting: [.documentsDirectory])
documentPicker.delegate = self
// Set the initial directory.
documentPicker.directoryURL = .documentsDirectory
// Present the document picker.
present(documentPicker, animated: true, completion: nil)
How can I attach the data file to the UIDocumentPickerViewController, so I can place it in the documents directory?
If you already have the URL for the document replace 'newFile' with the document URL. 'vc' is the current ViewController
Note asCopy = false will move the the document, asCopy = true will copy the document. There appears to be bug in iOS 16+ which disables the Move button when asCopy = false. Bug fixed in subsequent release FB11627056
//Present Document Picker
let controller = UIDocumentPickerViewController(forExporting: [newFile], asCopy: false)
vc.present(controller, animated: true) {
//this will be called as soon as the picker is launched NOT after save
}
Have you followed the instructions that Apple provides here? I'm summarizing the important bits here:
After the user taps Done, the system calls your delegate’s documentPicker(_:didPickDocumentsAt:) method, passing an array of security-scoped URLs for the user’s selected directories .... When the user selects a directory in the document picker, the system gives your app permission to access that directory and all of its contents.
So first, you need to implement the delegate methods so that you know what the user selects. Specifically, documentPicker(_:didPickDocumentsAt:) is the important one, although you'll want to listen for "cancel" as well. Then you need to access that scoped resource and write to it.
Here is an example that I took from the documentation linked above. This example only reads from the directory, but you can also write to it in the same way.
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) {
// Start accessing a security-scoped resource.
guard url.startAccessingSecurityScopedResource() else {
// Handle the failure here.
return
}
// Make sure you release the security-scoped resource when you finish.
defer { url.stopAccessingSecurityScopedResource() }
// Use file coordination for reading and writing any of the URL’s content.
var error: NSError? = nil
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
let keys : [URLResourceKey] = [.nameKey, .isDirectoryKey]
// Get an enumerator for the directory's content.
guard let fileList =
FileManager.default.enumerator(at: url, includingPropertiesForKeys: keys) else {
Swift.debugPrint("*** Unable to access the contents of \(url.path) ***\n")
return
}
for case let file as URL in fileList {
// Start accessing the content's security-scoped URL.
guard url.startAccessingSecurityScopedResource() else {
// Handle the failure here.
continue
}
// Do something with the file here.
Swift.debugPrint("chosen file: \(file.lastPathComponent)")
// Make sure you release the security-scoped resource when you finish.
url.stopAccessingSecurityScopedResource()
}
}
}

Why is url.bookmarkData returning nil?

We have an implementation with the UIDocumentPickerViewController that looks something like this:
case .openInitialization:
// Setup UIDocumentPicker.
if #available(iOS 14, *) {
documentsPicker = UIDocumentPickerViewController(forOpeningContentTypes: [
UTType.text,
UTType.utf8PlainText,
UTType.flatRTFD,
UTType.pdf])
} else {
documentsPicker = UIDocumentPickerViewController(documentTypes: [
String(kUTTypeText),
String(kUTTypeUTF8PlainText),
String(kUTTypeFlatRTFD),
String(kUTTypePDF)], in: .open)
}
Everything works great and we can select a document. When we select a document we get a document url but in some cases (especially with one drive) we get issues when we want to turn the url into a bookmark. Following code returns nil:
guard let bookmark = try? url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil) else { return }
Do anyone have an idea to why this is happening? Or what we can do to get it to work without returning nil?
Edit:
We've tryed to add try catch and we got following error which doesn't quite help much: Error Domain=NSCocoaErrorDomain Code=260 (file doesn't exist).
Edit 2:
So if I open from archive directly into our app it works no issues at all. But we still need to work from UIDocumentPickerViewController.
Also for some reasons files unlocked this way will just work from UIDocumentPickerViewController afterward.
Files can also be opened from onedrive and from there be opened in another app (ours). But this does't and gives a file does not exist error as well.
Edit 3:
So I've tested and read a ton. I can tell that following will return false for some files picked by documentpicker:
var exist = FileManager.default.fileExists(atPath: url.path)
But again if I open the file just once from iOS archive app it will work perfectly fine afterward. If there just were some way to tell it to update/download like apples does.
Edit 4:
I've made a sample project demonstrating the problem at github .
I answered a similar question here: PDFKit doesn’t work on iPads while works fine on a simulator [iOS, Swift]
Can you check if wrapping your url in a security access scope helps?:
url.startAccessingSecurityScopedResource()
print(FileManager.default.fileExists(atPath: url.path))
url.stopAccessingSecurityScopedResource()
The above should print true. This is because these API's access files outside of the applications sandbox.
See: https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
Used a technical ticket for apple and they came with a solution :D
NSFileCoordinator().coordinate(readingItemAt: url, options: .withoutChanges, error:&err, byAccessor: { (newURL: URL) -> Void in
do {
let bookmark = try newURL.bookmarkData()
} catch let error {
print("\(error)")
}
})
if let err = err {
print(err.localizedDescription)
}

FileProvider: "CopyItem()" is called twice -> error (FTP download)

The first view of my app (Swift 5, Xcode 10, iOS 12) has a "username" TextField and a "login" Button. Clicking on the button checks if there's a file for the entered username on my FTP server and downloads it to the Documents folder on the device. For this I'm using FileProvider.
My code:
private func download() {
print("start download") //Only called once!
let foldername = "myfolder"
let filename = "mytestfile.txf"
let server = "192.0.0.1"
let username = "testuser"
let password = "testpw"
let credential = URLCredential(user: username, password: password, persistence: .permanent)
let ftpProvider = FTPFileProvider(baseURL: server, mode: FTPFileProvider.Mode.passive, credential: credential, cache: URLCache())
ftpProvider?.delegate = self as FileProviderDelegate
let fileManager = FileManager.default
let source = "/\(foldername)/\(filename)"
let dest = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent(filename)
let destPath = dest.path
if fileManager.fileExists(atPath: destPath) {
print("file already exists!")
do {
try fileManager.removeItem(atPath: destPath)
} catch {
print("error removing!") //TODO: Error
}
print("still exists: \(fileManager.fileExists(atPath: destPath))")
} else {
print("file doesn't already exist!")
}
let progress = ftpProvider?.copyItem(path: source, toLocalURL: dest, completionHandler: nil)
progressBar.observedProgress = progress
}
I'm checking if the file already exists on the device because FileProvider doesn't seem to provide a copyItem function for downloading that also lets you overwrite the local file.
The problem is that copyItem tries to do everything twice: Downloading the file the first time succeeds (and it actually exists in Documents, I checked) because I manually delete the file if it already exists. The second try fails because the file already exists and this copyItem function doesn't know how to overwrite and of course doesn't call my code to delete the original again.
What can I do to fix this?
Edit/Update:
I created a simple "sample.txt" at the root of my ftp server (text inside :"Hello world from sample.txt!"), then tried to just read the file to later save it myself. For this I'm using this code from the "Sample-iOS.swift" file here.
ftpProvider?.contents(path: source, completionHandler: {
contents, error in
if let contents = contents {
print(String(data: contents, encoding: .utf8))
}
})
But it also does this twice! The output for the "sample.txt" file is:
Optional("Hello world from sample.txt!")
Fetching on sample.txt succeed.
Optional("Hello world from sample.txt!Hello world from sample.txt!")
Fetching on sample.txt succeed.
Why is it calling this twice too? I'm only calling my function once and "start download" is also only printed once.
Edit/Update 2:
I did some more investigating and found out what's called twice in the contents function:
It's the whole self.ftpDownload section!
And inside FTPHelper.ftpLogin the whole self.ftpRetrieve section is
called twice.
And inside FTPHelper.ftpRetrieve the whole self.attributesOfItem
section is called twice.
And probably so on...
ftpProvider?.copyItem uses the same ftpDownload func, so at least I know why both contents() and copyItem() are affected.
The same question remains though: Why is it calling these functions twice and how do I fix this?
This isn't an answer that shows an actual fix for FileProvider!
Unfortunately the library is pretty buggy currently, with functions being called twice (which you can kind of prevent by using a "firstTimeCalled" bool check) and if the server's slow(-ish), you also might not get e.g. the full list of files in a directory because FileProvider stops receiving answers before the server's actually done.
I haven't found any other FTP libraries for Swift that work (and are still supported), so now I'm using BlueSocket (which is able to open sockets, send commands to the server and receive commands from it) and built my own small library that can send/receive,... files (using the FTP codes) around it.

iOS device unable to access an iCloud account created by another iOS device

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.
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.

Will images be removed from within the local filesystem on update in iOS?

I am saving images captured from the camera within the local sandbox (filesystem) and store the filepath within my app to show the images (using Swift). I see that if I hit play in XCode, the images will be removed (which is ok)
Now I wonder what would happen if I submit this to the app store, the user saves images and I will update the app later on. Will the images will be removed as well?
To store the image, I use this function..
func saveImageLocally(imageData:NSData!) -> String{
let time = NSDate().timeIntervalSince1970
let fileManager = NSFileManager.defaultManager()
let dir = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0].stringByAppendingPathComponent(subDirForImage) as String
if !fileManager.fileExistsAtPath(dir) {
var error: NSError?
if !fileManager.createDirectoryAtPath(dir, withIntermediateDirectories: true, attributes: nil, error: &error) {
println("Unable to create directory: \(error)")
return ""
}
}
let path = dir.stringByAppendingPathComponent("IMAGENAME\(Int(time)).png")
var error: NSError?
if !imageData.writeToFile(path, options: NSDataWritingOptions.DataWritingAtomic, error: &error) {
println("error writing file: \(error)")
return ""
}
return path
}
Anything you store in your documents folder will persist through app updates providing you keep the same app ID and increment the version number.
Though it is worth noting that the full path to the app sandbox will be different (there will be a new sandbox for the update and the old data copied into it), so make sure you are only accessing resources by storing relative paths etc and are not storing full paths to images and expecting them to resolve after an update.

Resources