Getting NSMetadataItem for local files on iOS App Sandbox - metadata

I am aware that on iOS you can only use NSMetadataQuery for iCloud files. However, according to Apple's guide, you can still get NSMetadataItem by searching for the file you want manually with FileWrapper:
iOS allows metadata searches within iCloud to find files corresponding files. It provides only the Objective-C interface to file metadata query, NSMetadataQuery and NSMetadataItem, as well as only supporting the search scope that searches iCloud.
Unlike the desktop, the iOS application’s sandbox is not searchable using the metadata classes. In order to search your application’s sandbox, you will need to traverse the files within the sandbox file system recursively using the NSFileManager class. Once matching file or files is found, you can access that file in the manner that you require. You are also able to use the NSMedatataItem class to retrieve metadata for that particular file.
However, I couldn't find a way to get the NSMetadataItem from a local file on iOS sandbox. You can initialize NSMetadataItem from a file URL but that's available only on macOS. FileManager also doesn't seem to have an API to retrieve NSMetadataItem from it.
How do I get NSMetadataItem for local files on iOS App Sandbox?

Use NSMetadataQuery to query iCloud files, add observer to get notification.
metaDataQuery.searchScopes = [NSMetadataQueryUbiquitousDocumentsScope, NSMetadataQueryUbiquitousDataScope]
NotificationCenter.default.addObserver(self, selector: #selector(self.metadataQueryDidFinishGathering),
name: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: metaDataQuery)
metaDataQuery.start()
func metadataQueryDidFinishGathering(notification: NSNotification) {
let result: [NSMedatataItem] = (notification.object as! NSMetadataQuery).results as! [NSMedatataItem]
// handle result
}

Related

How to write to a file in a project while the project is running? [duplicate]

Very new to MacOS development (as in, completely new). I've developed a MacOS app (SwiftUI / Swift) and now figuring out a very things related to deployment.
My app generates a text file, but I'd like to save it within the app's folder (or whatever it is called) - and not in a user specified folder, and read it when I want. I can read resources from the Assets.xcassets but I'd like to be able to save as well without making the user choose a location.
Is there a way I can write/read from the app folder (I'm struggling to explain as I'm very unfamiliar with this system)?
Yes you can create a directory inside your application support folder, name it with app’s bundle identifier or your company and store all files that are not accessible to the user there:
Use this directory to store all app data files except those associated
with the user’s documents. For example, you might use this directory
to store app-created data files, configuration files, templates, or
other fixed or modifiable resources that are managed by the app. An
app might use this directory to store a modifiable copy of resources
contained initially in the app’s bundle. A game might use this
directory to store new levels purchased by the user and downloaded
from a server. All content in this directory should be placed in a
custom subdirectory whose name is that of your app’s bundle identifier
or your company.
You should take some time and read the File System Basics documentation
do {
let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let bundleID = Bundle.main.bundleIdentifier ?? "company name"
let appSupportSubDirectory = applicationSupport.appendingPathComponent(bundleID,isDirectory: true)
try FileManager.default.createDirectory(at: appSupportSubDirectory, withIntermediateDirectories: true, attributes: nil)
print(appSupportSubDirectory.path) // /Users/.../Library/Application Support/YourBundleIdentifier
} catch {
print(error)
}

MacOS: How/where do I store a programmatically created file within my app folder?

Very new to MacOS development (as in, completely new). I've developed a MacOS app (SwiftUI / Swift) and now figuring out a very things related to deployment.
My app generates a text file, but I'd like to save it within the app's folder (or whatever it is called) - and not in a user specified folder, and read it when I want. I can read resources from the Assets.xcassets but I'd like to be able to save as well without making the user choose a location.
Is there a way I can write/read from the app folder (I'm struggling to explain as I'm very unfamiliar with this system)?
Yes you can create a directory inside your application support folder, name it with app’s bundle identifier or your company and store all files that are not accessible to the user there:
Use this directory to store all app data files except those associated
with the user’s documents. For example, you might use this directory
to store app-created data files, configuration files, templates, or
other fixed or modifiable resources that are managed by the app. An
app might use this directory to store a modifiable copy of resources
contained initially in the app’s bundle. A game might use this
directory to store new levels purchased by the user and downloaded
from a server. All content in this directory should be placed in a
custom subdirectory whose name is that of your app’s bundle identifier
or your company.
You should take some time and read the File System Basics documentation
do {
let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let bundleID = Bundle.main.bundleIdentifier ?? "company name"
let appSupportSubDirectory = applicationSupport.appendingPathComponent(bundleID,isDirectory: true)
try FileManager.default.createDirectory(at: appSupportSubDirectory, withIntermediateDirectories: true, attributes: nil)
print(appSupportSubDirectory.path) // /Users/.../Library/Application Support/YourBundleIdentifier
} catch {
print(error)
}

Get the content of iCloud Drive

In my viewDidLoad, I need to verify if the files that I saved to the iCloud Drive are still available and have not been deleted. As I have read, I can not use standard FileManager calls:
FileManager.default.fileExists(atPath: filePath)
What would be the alternative. I do use NSMetadataQuery, but I wanted to know if there is an easy way to query the content of the App's UbiquitousDocument prior to the notifications kicking in.
Also, I am using the ios11 facility of sharing files between different users, and again I need to be able to verify if those files are still available when my App comes to the foreground. Using the NSMetadataQuery and searching in NSMetadataQueryUbiquitousDocumentsScope the shared documents are not displayed.
Any suggestions
Best
Reza
Try this:
let DOCUMENTS_DIRECTORY = "Documents"
if let iCloudDocumentsURL = NSFileManager.defaultManager().URLForUbiquityContainerIdentifier(nil)?.URLByAppendingPathComponent(DOCUMENTS_DIRECTORY)?.URLByAppendingPathComponent(fileName) {
if let pathComponent = iCloudDocumentsURL.path {
if (NSFileManager.defaultManager().fileExistsAtPath(pathComponent, isDirectory: nil)) {
}
}
}
This checks the file under the public Document directory.

Prevent iCloud sync of data (using .nosync?)

EDIT: So far, the best I've been able to come up with is a pop-up to ask the user to disable iCloud sync, along with moving all the data to the Documents directory so it won't get wiped: In iOS5, is it possible to detect if a user has an app set to back up?
I develop offline mapping application for iPhone/iPad.
We used to store all of the data (many gigs potentially) in the Caches directory.
As of iOS5, the files in the Caches directory can be randomly deleted when the user's hard drive starts getting full.
How can I store local data, without the data being synced to iCloud, iTunes, and without it being randomly deleted? My local data is a large directory tree with many small data files, in thousands of subdirectories.
I moved our directory tree from the library cache directory to a data.nosync directory in the documents directory, because we had read this might be a solution. However, the data in the nosync folder is still being backed up to iCloud.
Here is now I create the directory:
NSString* noSyncDirectory() {
static NSString *directory = nil;
if (!directory) {
directory = [[NSString stringWithFormat:#"%#/%#",
documentsDirectory(), #"data.nosync"] retain];
[Constants createDirectoryIfNeeded:directory];
}
return directory;
}
From: https://developer.apple.com/library/ios/#qa/qa1719/_index.html
You can use the following method to set the "do not back up" extended attribute. Whenever you create a file or folder that should not be backed up, write the data to the file and then call this method, passing in a URL to the file.
#include <sys/xattr.h>
- (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
const char* filePath = [[URL path] fileSystemRepresentation];
const char* attrName = "com.apple.MobileBackup";
u_int8_t attrValue = 1;
int result = setxattr(filePath, attrName, &attrValue, sizeof(attrValue), 0, 0);
return result == 0;
}
More information can be found: https://developer.apple.com/icloud/documentation/data-storage/
Further note: While the developer documentation incorrectly implies ("These files will not be purged and will not be included in the user's iCloud or iTunes backup.") that the do-not-backup flag doubles as a do-not-purge flag that is not the case. Simply leaving files in the Caches Directory and flagging them do-not-backup will not prevent their wipe.
Perhaps you can disable backup for your app and store data files somewhere else in the app tree. Any stuff that needs to be backed can be put in a common area outside your app.
You might be able to do it in provisioning: invalid code signing app submission
or settings:
When you want to give the user the option to enable or disable iCloud usage entirely for your app. If your app includes a Settings bundle or inline preferences, you could include a preference to toggle whether your app stores content in iCloud at all. For example, an app whose data consists entirely of privately managed files might do this to give the user the choice of how those files are stored.
or by removing the com.apple.developer.ubiquity-container-identifiers entitlement (which could get auto-added) with Xcode: Configuring Your App's iCloud Entitlements
Otherwise you might need to issue a warning with instructions on disabling through the UI:
http://www.pcmag.com/article2/0,2817,2394702,00.asp#fbid=bpIwPLZ1HeQ
Another workaround is to group the maps into collections that are installed as separate applications. That would be a way to store the data without creating any directories that sync or get backed-up. The data will be stored in the the .app directory and will be protected.
Depending on how the cache space reclamation function works, it might not delete recently accessed or modified files. You could try periodically touching them on a timer. You could also add some old files as decoys and detect when they've been deleted or when space is low to at least issue a warning or re-download the deleted objects...
This issue might not have a workaround yet... You could possibly try calling URLForUbiquityContainerIdentifier explicitly, since it does some initialization on the first invocation. Then create a sub-directory with a .nosync suffix (based on this example).
The first time you call this method for a given container directory, iOS extends your application sandbox to include that container directory. Thus, it is important that you call this method at least once before trying to search for files in iCloud. And if your application accesses multiple container directories, you should call the method once for each directory.
The doc on .nosync:
To ensure that the persistent store itself is not synced by iCloud: when you set a value for the NSPersistentStoreUbiquitousContentNameKey, UIManagedDocument puts the persistent store in a .nosync directory inside the document package. If you make use of additional content (using the writeAdditionalContent:toURL:originalContentsURL:error: method), you must make sure that the document directory is not a package. Typically you give the document directory an extension that is not recognized as a document extension.
You may want to ensure you have the com.apple.developer.ubiquity-container-identifiers entitlement.
The iCloud Containers field identifies the list of container directories that your app can access in the user’s iCloud storage. (This field corresponds to the com.apple.developer.ubiquity-container-identifiers entitlement.)
Maybe uncheck "Enable Entitlements" in the summary pane of the project or edit the profile to remove the *ubiquity settings. (Notes for ios 5 beta 7 reference entitlements.)
There is also the setUbiquitous function:
setUbiquitous:itemAtURL:destinationURL:error:
Sets whether the item at the specified URL should be stored in the cloud.
Parameters
flag
Specify YES to move the item to iCloud or NO to remove it from iCloud (if it is there currently).
http://developer.apple.com/library/ios/#DOCUMENTATION/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/Reference/Reference.html#//apple_ref/occ/instm/NSFileManager/setUbiquitous:itemAtURL:destinationURL:error:
There is a newer way to prevent iCloud syncing of data without using extended attributes directly:
- (BOOL)addSkipBackupAttributeToItemAtPath:(NSString *) filePathString
{
NSURL* URL= [NSURL fileURLWithPath: filePathString];
assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);
NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
if(!success){
NSLog(#"Error excluding %# from backup %#", [URL lastPathComponent], error);
}
return success;
}
See Apple's Technical Q&A QA1719 for more details.
Did you try just naming the directory ".nosync" without the data in front? It could be generally . directories are ignored, or perhaps that specifically.
But, it seems like the behavior is just as a user would want it - potentially gigs of space used by an application they may not be using currently, where the space could be reclaimed automatically. I am not sure but you would think the system would be smart about reclaiming files created recently only after there was no choice, so if the user had just stored maps recently they would not be deleted unless there was no other choice.
For the purposes of filing a bug I am going to ask for a way to mark a directory for user prompting before deletion - so that if they are syncing a lot of movies and it would clear out a data set like the maps you are talking about, the user would be asked if they want to remove the data "offline maps" from application "MyCoolMapper" to proceed with the sync.

iCloud: can I ignore those who disable iCloud?

I'm struggling a bit with the idea of iCloud and posted a more general question here. My biggest problem is to decide whether I should stop putting the user's data in the good old documents folder which is found in the app's sandbox. To illustrate my problem:
The docs don't give an answer as far as I can see. Let's suppose I have an App which handles different txt files. Once I start my app, I simply check if any txt files are in the cloud like so:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(#"AppDelegate: app did finish launching");
self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
self.window.rootViewController = self.viewController;
[self.window makeKeyAndVisible];
// (1) iCloud: init
NSURL *ubiq = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if (ubiq) {
NSLog(#"User has iCloud enabled! Let's get the txt files from the cloud.");
[self loadDocument];
} else {
NSLog(#"User doesn't have iCloud enabled. App is thus worthless.");
}
return YES;
}
I then have a method to check if there are any txt files in the cloud and if so, load them. If not, I simply create new txt files in the cloud.
This means that the app does not store any data in the documents folder. As far as I understand it, everything is either in the local iCloud storage of my device (which is also accessible if the user is OFFLINE) or in the cloud. So the text file exists in two places: on my device and the cloud.
So there is simply no need to store a third copy in my local documents folder, right? Or is this essential for some reason I have overlooked? In other words, for what should I use the local documents folder if I offer iCloud to my users? (And can I simply ignore those who won't sign up for iCloud?)
EDIT: Just to clarify, when I'm talking about the standard documents folder in the app's sandbox, I mean this one:
NSArray *paths =NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
Perhaps I'm a bit slow, by re-reading the docs for the 4th or 5th time, I came across this which suggests that you should always create your files in the sandbox and then move them to the cloud. So in a way, Apple suggests to have 3 versions of the same file at all times:
Apps use the same technologies to manage files and directories in
iCloud that they do for local files and directories. Files and
directories in iCloud are still just files and directories. You can
open them, create them, move them, copy them, read and write from
them, delete them, or any of the other operations you might want to
do. The only differences between local files and directories and
iCloud files and directories is the URL you use to access them.
Instead of URLs being relative to your app’s sandbox, URLs for iCloud
files and directories are relative to the corresponding iCloud
container directory.
To move a file or directory to iCloud:
Create the file or directory locally in your app sandbox. While in
use, the file or directory must be managed by a file presenter, such
as a UIDocument object.
Use the URLForUbiquityContainerIdentifier: method to retrieve a URL
for the iCloud container directory in which you want to store the
item. Use the container directory URL to build a new URL that
specifies the item’s location in iCloud. Call the
setUbiquitous:itemAtURL:destinationURL:error: method of NSFileManager
to move the item to iCloud. Never call this method from your app’s
main thread; doing so could block your main thread for an extended
period of time or cause a deadlock with one of your app’s own file
presenters. When you move a file or directory to iCloud, the system
copies that item out of your app sandbox and into a private local
directory so that it can be monitored by the iCloud daemon. Even
though the file is no longer in your sandbox, your app still has full
access to it. Although a copy of the file remains local to the current
device, the file is also sent to iCloud so that it can be distributed
to other devices. The iCloud daemon handles all of the work of making
sure that the local copies are the same. So from the perspective of
your app, the file just is in iCloud.
All changes you make to a file or directory in iCloud must be made
using a file coordinator object. These changes include moving,
deleting, copying, or renaming the item. The file coordinator ensures
that the iCloud daemon does not change the file or directory at the
same time and ensures that other interested parties are notified of
the changes you make.
See here.
There is no reason to store documents both in local storage as well as in iCloud. However, you should give users the option of turning iCloud storage off. With iCloud storage off, you should only look for files in local storage (as with pre-iOS5 apps). The best thing is to try to isolate the part of your code that needs to know where documents are stored, and have it test whether or not iCloud is available and enabled and have that piece of code return the URL where documents should be stored.
Update:
If you want to diverge for iOS5 vs. iOS4, you just need to test if the iOS5 features are there. There are several ways that you can do this. One way is to check:
if ([UIDocument class] == nil)
On iOS4 this will be true, and on iOS5 this will be false. I don't know what kind of datastructures you have for your files, but one thing you could do is create a wrapper around UIDocument. Inside this wrapper class, you could have instance variables for a UIDocument structure as well as for fields you would need in IOS4 (such as the path to the file). When you instantiate your class, test whether iCloud is enabled and if UIDocument is available, and if so, use it and set the field. Otherwise, set the other fields and leave the UIDocument field to be nil. When you need to do operations on your "file", test if the UIDocument field is nil, and if it is do it the "old" way. Otherwise, just pass on the request to the UIDocument object.
Remember that if the user has enabled device backup to iCloud, the documents directory gets backed up anyways.
It really depends on if your planning to use iCloud as a sync tool between apps / platforms. If your not, it really doesn't make sense to use iCloud to store your documents.