Does NSFileWrapper load everything into memory? - ios

Lets say I have an NSFileWrapper directory. This directory is made up of several levels of directories and files. Some of the files are large. Are all these files loaded into memory, or are they lazily loaded?
If they are loaded into memory, are there any alternatives to NSFileWrapper with similar functionality that won't load files into memory? Something I can hook into UIDocument?
This is for a document based application, which uses UIDocument's that are synced with iCloud. A document can have images and videos embedded inside it. Each image/video has a preview image (thumbnail) that is shown in an HTML document. The full size images or videos shouldn't be loaded into memory, but rather loaded on demand.
I also need a way to add a resource without loading it into memory. Something like "initWithAsset:(ALAsset *)" would be ideal.

I've made an app a while ago that generates a video. This video was then saved to a specific file format using a UIDocument subclass.
The only way to make the app not run out of memory while executing contentsForType:error: was to output the video to a file in the tmp dir and init the filewrapper with the url to the video with NSFileWrapperReadingWithoutMapping-option to prevent it from loading the video to memory and just copy in the file.
- (id)contentsForType:(NSString *)typeName error:(NSError **)outError {
if (self.fileWrapper == nil) {
self.fileWrapper = [[NSFileWrapper alloc] initDirectoryWithFileWrappers:nil];
}
if (self.videoURL != nil) {
NSError *fileReadError;
NSFileWrapper *videoFileWrapper = [[NSFileWrapper alloc] initWithURL:self.videoURL options:NSFileWrapperReadingWithoutMapping error:&fileReadError];
if(fileReadError){
NSLog(#"File read error: %#", [fileReadError localizedDescription]);
}else {
[videoFileWrapper setPreferredFilename:#"video.mov"];
[self.fileWrapper addFileWrapper:videoFileWrapper];
}
}
//...
}

It's not totally clear from the documentation, but I'm 99% positive that NSFileWrapper will memory-map everything. The main use case for NSFileWrapper is to embed files inside documents, where you'd presumably need access to said file at all times.
What are you trying to use the file wrapper for? I assume if you're mapping an entire directory it's not necessarily to embed media inside a document, but perhaps I'm mistaken. If you maybe talk a bit more about your use case, as that will influence what alternatives you might go for.

Related

How to share files for open-in-place on iOS

We have an iOS application that manages documents via Core Data. The actual files reside in the app's shared container so that the app's file provider extension can also access them for Files.app support. We want to give the user the option to open these files in third-party apps so that they can edit them in-place instead of sending a copy to the other app.
We provide a UIActivityViewController for sharing files with other apps. We also provide a UIActivity that shows a UIDocumentInteractionController which seems to work better in some cases. We give the UIActivityViewController the document's file URL, the raw text content, and printable data.
This works but all third-party editors are shown as Copy to … instead of Open in …
We've also set the UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace properties to YES in the app's info.plist but they seem to be only relevant for open-in-place when sharing files residing in the app's Documents folder.
Now we've stumbled upon the NSItemProviderFileOptionOpenInPlace option for NSItemProvider. As we're already supporting a file provider extension and from Apple's documentation this seemed like a great place to accomplish just what we want.
Adding a "pure" NSItemProvider works, in a way, but shows fewer options than when also sharing the file URL and text in addition (which is expected). However, when we use -[NSItemProvider registerFileRepresentationForTypeIdentifier:fileOptions:visibility:loadHandler:] with the said option (or just zero, same result) and return the file URL in the loadHandler's completionHandler() nothing is shared anymore. E.g., Mail no longer attaches the file, Messages doesn't show the document for sending.
These are the relevant bits of the code:
NSMutableArray *items = [NSMutableArray array];
NSMutableArray <UIActivity *> *activities = [NSMutableArray array];
NSURL *fileURL = self.record.metadata.fileURL;
NSString *fileUTI = self.record.metadata.uti;
NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithItem: fileURL typeIdentifier: fileUTI];
[itemProvider registerFileRepresentationForTypeIdentifier:fileUTI fileOptions:NSItemProviderFileOptionOpenInPlace visibility:YES loadHandler:^NSProgress * _Nullable(void (^ _Nonnull completionHandler)(NSURL * _Nullable, BOOL, NSError * _Nullable))
{
if (fileURL)
completionHandler(fileURL, YES, nil);
else
completionHandler(nil, YES, [NSError errorWithDomain:NSCocoaErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]);
return nil;
}];
[items addObject:itemProvider];
self.activityViewController = [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:activities];
[UIAppDelegate.splitViewController presentViewController:self.activityViewController animated:YES completion:nil];
The using the Share menu the item provider's load handler is correctly called and the file's actual URL returned.
Is that not how NSItemProviderFileOptionOpenInPlace is intended to be used? Or are we using it simply wrong? Apple's description is extremely sparse and we couldn't find any information elsewhere on the internet except for the official documentation.
I've found out what my problem was: Not deep enough understanding of the relationship between the activity view controller and file providers.
As all my files reside in the shared container and are published also through the file provider extension, what I need to share through the activity view controller is the exact same URL that is shared through the file provider extension. Technically then the app that opens the file accesses it through there file provider mechanism.

Delete File After contentsOfFile Load

I am loading an array of UIImage from the iOS Documents directory:
var images = [UIImage]()
for fileName in fileNames {
images.append(UIImage(contentsOfFile: "\(imagesPath)/\(fileName).png")!)
}
I'm going to continue using this array but I don't need the files anymore, so I go ahead and delete them:
for fileName in fileNames {
do {
try NSFileManager.defaultManager().removeItemAtPath("\(imagesPath)/\(fileName).png")
} catch {
print("Error")
}
}
When I do this, my array of UIImage is now invalid and gives me errors while trying to access them. Shouldn't this be in memory and not related to the files on disk?
I tried using the ".copy()" command on the images when I load them but that made no difference.
I have confirmed that the delete is the issue above because if I comment out that line the app works great with no errors. I only get errors accessing the array after I delete the files from disk.
Is there a way to sever this connection?
Edit: Per the correct answer from #Wain, the code works fine if I change it to:
var images = [UIImage]()
for fileName in fileNames {
let imgData = NSFileManager.defaultManager().contentsAtPath("\(imagesPath)/\(fileName).png")!
images.append(UIImage(data: imgData)!)
}
Doing this doesn't keep the link back to the file on disk.
The images haven't been displayed so the data hasn't fully been loaded yet, only enough to know the image details and size has been loaded. This is for memory efficiency. depending on the image format and usage different parts of data may be loaded at multiple different times.
If you load the file into NSData, ensuring that it isn't memory mapped, and create the image from that then the data and image should be unlinked from the underlying file. This is less memory efficient and it would be better to keep your current code and delete the files when you know you're finished with the images all together.

(IOS) Cordova Camera Plugin Referring to deleted images

I'm building a cordova app (primarily for IOS & Android) in which the user can take an image, retake (, etc.) it and save it locally.
I'm currently struggling with the cordova cameraPlugin. So, here a short description of the problem.
When the user takes an image, it's saved locally in the apps temp folder and the user is able to view in in the UIWebView. On retaking, the image will be deleted from the temp folder and should not be available any longer (in RAM and local FS).
It works as long as the user doesn't retakes the image 2 or more times, if he does instead of the last image the first image will be referenced/rendered in WebView. After reopening the app, the image is displayed correctly.
An Example:
The user takes the first image. ==> cdv_photo_001.png
The second. ==> cdv_photo_002.png and the first one will be deleted (which seems to work correctly)
And the third. ==> cdv_photo_001.png and the second image will be deleted.
The third image will look the same as the deleted first one. This happens with every image after the third one.
It works fine after restarting the app
I've already tried to disable the App-Cache, delete the app cache before updating the image, refreshing the page and looking for answers online.
I'm getting an error when opening the camera UI, but I could not find a solution for it either.
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before
snapshotting or snapshot after screen updates.
The code for the camera Call:
function getPhoto() {
navigator.camera.getPicture(getPhotoOnSuccess, getPhotoOnFail, {
quality: 25,
destinationType: Camera.DestinationType.FILE_URL,
correctOrientation: true,
encodingType: Camera.EncodingType.PNG
});
}
In getPhotoOnSuccess I'm basically saving the image path to a db and appending it with jQuery to the view.
And the code to delete the image: (sidenote I`m new to Objective C)
- (void) deleteImageByPath:(NSString *)imagePath withSelector:(SEL)selector{
NSError *error = nil;
NSFileManager *mgr = [NSFileManager defaultManager];
NSString *tempFolder = NSTemporaryDirectory();
if([mgr removeItemAtPath: imagePath error:&error] == NO) {
NSLog(#"File deleted");
}
//The files can be edited as well, so there can be two files in different directories
if(error != nil){
NSString *imgEl = tempFolder;
imgEl = [imgEl stringByAppendingPathComponent:imagePath.lastPathComponent];
if(![mgr removeItemAtPath:imgEl error:&error]){
NSLog(#"Old element couln't be deleted.");
}
}
[self performSelector:selector withObject:error];
}
The file is not in the directory anymore after deleting it, so I guess it works.
An important detail could be, that I wrote my own IOS cordova plugin, because the method for the file system access provided by cordova sucks.
So thats it.
The specific question is: Why and how is this happening and is there a chance to change this behavior? If yes, how should I proceed?
By the way, I`m using cordova 3.1.0 and the build target is IOS 7.
Thanks in advance.
Ok folks, I finally got it.
The whole problem was not related to my code or any of the cordova code.
So why did it happen? ==> I don't exactly know that, for it seems that this bug or whatever you might call it, has occurred to many people.
And they all tried to delete or deactivate the cache as I did, some of their problems are very close to my own but most aren't, so it took a while til I found a solution.
I read this thread and tried to append a timestamp to the image path and it worked!
My conclusion to this it, that there might be a problem with the UIWebView and the cache management.
Or it might proof as a general WebView problem, I will be able to check that in a few days on an Adroid device.

Check if file is saved in documents directory

In my application, when I press on a button, I download a file from the internet, and then I read it. The only thing is that the file takes about five seconds to load, depending on the speed of the network, but since the file gets bigger and bigger, the time needed increases. If i save the file and after run the code to read it, the code runs but the file is not their, because it doen't wait the file to load. So I added an NSTimer of 6 seconds and it works. But if the file gets bigger I would need to change it. Is there a code to run the code to read the file, once the load is completed??
Thanks for the help
Since I use dropbox I can use this code (just found out):
- (void)restClient:(DBRestClient*)client loadedFile:(NSString*)localPath {
}
but it runs every time a file is downloaded, and I would like to run it for every 2 files loaded...
- (void)restClient:(DBRestClient*)client loadedFile:(NSString*)localPath {
NSLog(#"%#",localPath); //just to see the console log of what this method spits out
NSString *documents = ***DOC_DIRECTORY_METHOD***;
NSString *expectedFilePath = [documents stringByAppendingPathComponent:#"myDownloadedFile"];
if ([localPath isEqualToString:expectedFilePath]) {
//insert my read file method here
}
}
I don't know what the variable localPath will output, either the full path or just the name of the file. You might have to tweak this.
Good luck.

How to slow down UIDocument's initial loadFromContents:ofType:error:?

When I drag a file package from Finder into the iTunes.app file sharing pane, my UIDocument class is trying to read in the associated file wrappers, and it appears my code to read it is executing faster than iTunes.app can copy the contents over. The initial file wrappers array contains only one of the two files inside the wrapper.
So how do I "slow" my code down?
I ran a test using performSelector:withObject:AfterDelay:1.0f, and that worked fine, but that feels really risky: What if a really large file is dragged in to iTunes (by really large I mean one that exceeds my delay)? What if multiple files are all dropped on at the same time?
I looked at somehow discerning that the file is ready to be read, but Apple's NSFileManager documentation says
"It's far better to attempt an operation (such as loading a file or
creating a directory), check for errors, and handle those errors
gracefully than it is to try to figure out ahead of time whether the
operation will succeed."
But where the timing problem comes up is during the decoding of the constituent file wrappers, so how to handle it "gracefully" is eluding me.
My package file has two data files within it (at this early stage, but the design is because there will be more): data.dat and info.dat. When I first got code working to the point where I could drag a file into iTunes.app and notice it in my view controller, data.dat was always decoding fine, but info.dat was not found. Once the file is inside my local documents folder, the view controller presents all the data as expected (i.e. info.dat is inside the file wrapper and correctly formed). Suspecting this to be a timing issue, I renamed the filename constants and on-disk files to zdata.dat and ainfo.dat -- sure enough: ainfo.dat loads and my UIDocument subclass complains that zdata.dat wasn't found.
I use lazy loading, but the view controller has an immediate interest in the info.dat contents, so lazy loading isn't lazy enough for iTunes to get through copying!
From my UIDocument subclass implementation:
- (BOOL)loadFromContents:(id)contents ofType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
self.fileWrapper = (NSFileWrapper *)contents;
// Lazy load everything!
_data = nil;
_metadata = nil;
return YES;
}
- (id)decodeObjectFromWrapperWithPreferredFilename:(NSString *)preferredFilename
{
NSFileWrapper *fw = [self.fileWrapper.fileWrappers objectForKey:preferredFilename];
if (!fw) {
NSLog(#"Unexpected error: Couldn't find %# in the file wrapper for %#", preferredFilename, self.fileURL);
return nil;
}
NSData *data = [fw regularFileContents];
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
return [unarchiver decodeObjectForKey:#"data"];
}
- (GSBMetadata *)metadata
{
if (_metadata == nil) {
if (self.fileWrapper != nil) {
// NSLog(#"Loading metadata for %#...",self.fileURL);
_metadata = [self decodeObjectFromWrapperWithPreferredFilename:kGSBMetadataFileName];
} else {
_metadata = [[GSBMetadata alloc] init];
}
}
return _metadata;
}
The problem is discovered in decodeObjectFromWrapperWithPreferredFilename: when the check is made to ensure that the expected file wrapper is present. Normally this would be the case if the file was corrupt, or perhaps version 2 of the app used a different file format. But gracefully handling those circumstances fall under the heading of "paranoid programming expected that sort of thing" and not under the heading of "just wait a second and all your data will be available to you."

Resources