iOS: Asynchronously Download Images from Web *unless* they already exists in filesystem? - objective-c

In my iOS app I'm using Core Data to model a number of entities that exist in a remote Ruby on Rails application. A number of these entities reference the URLs of images stored in S3 which I need to download and store locally on the user's iDevice as needed.
My specific requirements are:
Display the image (in a UITableView usually or on its own) from the local filesystem.
IF the image doesn't exist locally I need to be able to download it in the background (usually there will be two images that need to be downloaded ... a thumbnail and an original). A default image should be displayed until the downloaded image is persisted.
Once the image is downloaded I need to have it saved in the filesystem and displayed on the screen.
I know there are a number of related posts but I'm interested to hear of what you all would consider a recommended approach based on my specific needs. Also, are there any gotchas I need to be wary of?
Thanks -wg

Use ASIHTTPRequest. Read the section on using a download cache here - http://allseeing-i.com/ASIHTTPRequest/How-to-use
Basically you'll want to turn on the cache which you can do for all requests or just your image requests:
[ASIHTTPRequest setDefaultCache:[ASIDownloadCache sharedCache]]; // all requests
// specific requests
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDownloadCache:[ASIDownloadCache sharedCache]];
Then you probably want to use the cache policy:
ASIOnlyLoadIfNotCachedCachePolicy
and the storage policy:
ASICachePermanentlyCacheStoragePolicy
However multiple cache policies would meet your needs.
I'm recommending to use ASI because it solves lots of problems in an app. I recently developed my own network library, partially as an exercise and partially because I did not know about ASI and didn't like the other options I found at the time. I recently started moving everything to use ASI and I'm very happy with my choice

I have a similar requirement for a project I worked on. In my cases I wanted local files when running in test mode but remote files otherwise.
To implement this, I defined a "ResourceURLProvider" protocol within the header file for the class that would be loading the files:
#protocol ResourceURLProvider
-(NSURL *)getURLForFile:(NSString *fileName);
#end
#interface MyFileLoader {
id<ResourceURLProvider> provider;
}
#property (nonatomic, assign) id<ResourceURLProvider> provider;
In my implementation class I did something like this:
NSURL *url = [provider getURLForFile:#"myfile.xml"];
if (!url) {
url = // Some code to create the URL for the remote myfile.xml
}
If you set something up like this then you can implement your version of the provider (whatever you name it) and have it returned a URL to the cached file if it exists. If not, you return nil and the code above will use the URL to the remote location.
Unlike my case, you want to cache the file after you've downloaded it. To do that I might add another method to the protocol I defined:
#protocol ResourceURLProvider
-(NSURL *)getURLForFile:(NSString *fileName);
-(void)cacheFileData:(NSData *)data filename:(NSString *)fileName;
#end
Your implementation would look at the file name and check to see if the file was already cached. If not, the NSData (the contents of the file) would be written to your cache.
Hope that helps.

Related

When initialising an NSFilePresenter for a fileURL within an NSFileCoordinator accessor block passing newURL, which URL should I use?

Apple's documentation is not as clear as I would like when it comes to understanding subtle details relating to NSFileCoordinator usage. My current interpretation is as follows:
When you access a fileURL using NSFileCoordinator, the coordinator liaises with all registered NSFilePresenters for that fileURL (or hierarchically dependent on that fileURL) and other NSFileCoordinators accessing it as needed to ensure that reads/writes proceed without conflict in an efficient manner (across processes even), taking into account options you specify.
In order to do this, NSFileCoordinator may move the file in order for it to be accessed. Focusing on synchronous coordination of a single fileURL, the accessor block passes a 'newURL' parameter which should be used to access the underlying file or directory.
According to the docs, newURL constitutes:
A URL identifying the file or directory to read/write to. If other
objects or processes are acting on the item at the URL, the actual URL
passed to reader/writer parameter may be different from the one in this
parameter.
I am file coordinating the fileURL of a file package. Within the accessor block I am reading from this file package using the 'newURL' accessor parameter in order to instantiate an object which itself needs to register a file presenter for its parent file package. Consider for example a 'project' object, born from a 'project file package', which needs to present its parent project file package to maintain current knowledge of the project file package location and file coordinate access for future reads/writes.
The question is, should the instantiated object (e.g., project) initialise its file presenter using the original fileURL, or the newURL provided in the accessor block? As far as I can tell there isn't enough information in Apple's docs to really know for sure.
The crux of the confusion:
It appears that NSFileCoordinator can make a temporary copy of the original file and pass the location of the temporary copy as newURL. In such a scenario, I would not want the object instantiated from newURL to present newURL, I would want it to present the original fileURL for the purpose of ongoing operations on the file package.
It may also be the case that the original fileURL is legitimately moved by other concurrent operations at the time of file coordination, and thus it seems possible that the accessor block's newURL may NOT be a temporary copy, but could actually represent a new permanent location for the file (package). In that case I would want to present the file package using newURL to facilitate future coordination of the file (package).
Example for clarity:
- (instancetype) initProjectWithFilePackageURL:(NSURL *)projectfilePackageURL
{
NSFileCoordinator *fc = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fc coordinateReadingItemAtURL:projectFilePackageURL
options:NSFileCoordinatorReadingWithoutChanges
error:&coordinationError
byAccessor:^(NSURL * _Nonnull newURL) {
NSURL *dataURL = fileURLForProjectDataInProjectFilePackageAtURL(newURL);
self = [EGUtility unarchiveProjectFromDataAtURL:dataURL];
if (self)
{
// Which URL should the project present?
// It needs to monitor its parent file package indefinitely, not just within the accessor block.
EGFilePresenter *presenter = [EGPresenter newWithDelegate:project fileURL:<projectFilePackageURL OR newURL>?]
[project setPresenter:presenter];
// Etc…
}
return self;
}
It is difficult to know which I should do, or whether I am missing any other subtleties. I would be very grateful for any insight on this. Thank you.
Having now spent a TSI and discussed the matter with an Apple engineer, here is a summation of what I learned for the benefit of others:
NSFileCoordinator works hard to identify the right URL to pass to the accessor as the newURL. It does the following:
Standardises the URL to convert special characters in the URL, if any.
Resolves symbolic links, if needed.
Handles downloading if the coordinated URL is on a file provider.
Handles access issues if the URL is remote.
If the coordinated file is moved while the access claim was pending, newURL will take that into account and point to the new location of the file.
If the provided option is NSFileCoordinatorReadingForUploading, newURL will point to a temporary snapshot of the file so that access to the original file is not blocked by the uploading process.
Generally, I was assured that when initialising a file presenter for the coordinated file within the coordinator's accessor block, the URL to use would be the newURL parameter passed to the accessor block unless specifying NSFileCoordinatorReadingForUploading, in which case the coordinated URL should be used instead.

Prevent NSDocument saving in temporary dictionary

I have an app with subclass of NSDocument that has overridden method writeToURL:(NSURL *) ofType:(NSString *) error:(NSError **) which saves data at given NSURL location, but also can save additional file (with appended .my2ext) with debug information. Previously it worked well (I created the app several years ago), but now I see that instead of user selected location the method gets some temporary directory:
file:///var/folders/yv/gwf3_hjs0ps7sb3psh3d0w3m0000gn/T/TemporaryItems/(A%20Document%20Being%20Saved%20By%20MyApp%202)/myfilename.myext
Then, as I understand, the framework relocates the main file (at given url), but the additional file gets lost. So, can I somehow obtain the user selected path to save directly into it? Or prevent using temp directories at all?
I've already turned off the SandBox mode, but this didn't help. I also know that I can use "File Package" approach, but my app is created for a few people only, so, there is not interest in good production approach, only in simplicity.
I tried to google any possible solution, but found nothing helpful or just related. Even the documentation says nothing about using temporary directories! So, I decided to override different NSDocument methods. After several experiments I almost lost hope, but then I found that the method
saveToURL: ofType: forSaveOperation: delegate: didSaveSelector: contextInfo: provides real, user selected location. And this finally solved the problem.

Core Data app does not generate spotlight indexable metadata (or i can not find it)

I have a sandboxed, Mavericks only, Core Data, non-document, Mac application.
For one attribute of one entity I selected „Index in Spotlight“,
and in a second attempt I selected „Index in Spotlight“ and „Store in External Record File“.
Following Apples Core Data Spotlight Integration Programming Guide i am at the first point, Your program:
There are three processes involved in creating and maintaining the
Spotlight index:
Your program. Within your program, the Core Data framework maintains
the persistent store and creates the external record files to trigger
the spotlight indexing
The Spotlight importer. [...]
Core Data External Record daemon. [...]
I assume, now, there must be some place where metadata (that a Spotlight Importer could index) or external record files will be generated if I run the application and add data in it.
I can nowhere find such metadata or external record files. I searched everywhere in- and outside the sandbox container of my application. (Note, i am not trying to build the Spotlight Importer yet — i am merely looking for the metadata to be indexable.)
Where would this spotlight indexable metadata normally be found?
What could be the reasons no spotlight indexable metadata is generated?
The directory for the external records has to be in ~/Library/CoreData (or the equivalent in a sandboxed application). You must create it.
Also don't forget to set the store options for your PersistentStoreCoordinator, in the Application Delegate, like this in - (NSPersistentStoreCoordinator *) persistentStoreCoordinator :
//creating the External Records Directory
error = nil;
NSString *externalRecordsSupportFolder = [#"~/Library/CoreData/YOUR_EXTERNAL_RECORD_DIRECTORY" stringByExpandingTildeInPath];
[fileManager createDirectoryAtPath:externalRecordsSupportFolder
withIntermediateDirectories:YES
attributes:nil
error:&error];
if(error){
[NSApp presentError:error];
}
//options for your main Persistent Store Coordinator
NSDictionary *storeOptions = #{
NSExternalRecordExtensionOption:#"YOUR_EXTERNAL_RECORD_EXTENSION",
NSExternalRecordsDirectoryOption:externalRecordsSupportFolder,
NSExternalRecordsFileFormatOption:NSXMLExternalRecordType
};
Then you pass the storeOptions
NSPersistentStoreCoordinator *coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
if (![coordinator addPersistentStoreWithType:NSXMLStoreType configuration:nil URL:url options:storeOptions error:&error]) {
[[NSApplication sharedApplication] presentError:error];
return nil;
}
External record data is stored in the same directory as your primary Core Data files (i.e. your SQLite file), but in a hidden subdirectory. The naming convention for the subdirectory is .ApplicationName_SUPPORT/_EXTERNAL_DATA .
For non-document-based applications, Core Data creates a directory structure within ~/Library/Caches/Metadata for the application. The directory structure may vary with the OS version, Core Data version, etc. At some level there should be an application-specific directory within that structure, and inside that should be the external records file(s) created by Core Data.
If you cannot find these files, use kqueue events, lsof, or libdispatch to monitor the filesystem for changes while you run your application. You should be able to see what locations on the file system are being accessed easily. If external records files are not being created, or are being created in some new location, that too should be obvious.
Spotlight metadata is not stored in discrete files but in Spotlight's own data. You can inspect the metadata of a file by using the mdls command from a terminal.
Example:
mdls /Applications/Maps.app
You can also use the mdimport command to tell Spotlight to index something on demand.
Example:
mdimport ~/Documents/MyAwesomeStuff
mdimport also has a command line option to use a specific importer rather than a system importer. This can be very useful for development. Both mdls and mdimport will print out help messages detailing these arguments if asked.
In the core data model editor, you can check the spotlight indexing for each individual attribute.
Select the attribute and open the Data Model inspector
Check "Index in Spotlight"

App Sandbox: document-scoped bookmark not resolving; not returning any error

I'm sandboxing my app, and trying to allow for import/export of multiple files, using an XML file to refer to them. To allow my app (or another sandboxed app) access to the files listed in the XML, I'm also including a serialized security-scoped bookmark. I'm serializing it as described in this answer, and my unit tests (which are not sandboxed) write and read the XML data without issue. When my app resolves the bookmark, the NSURL returned is nil, as is the NSError reference. Since I don't believe that should be the case, why is it happening? I can work around it by prompting the user to select a file/directory with an NSOpenPanel, but I'd still like to get the bookmarks to work as they should.
Reproduced in a test project
To reproduce at home, create a new Cocoa app in Xcode, and use the following Gist for the files in the project: https://gist.github.com/2582589 (updated with a proper next-view loop)
Then, follow Apple's instructions to code-sign the project. You reproduce the problem (which I submitted to Apple as rdar://11369377) by clicking the buttons in sequence. You pick any file on disk (outside the app's container), then an XML to export to, and then the same XML to import.
Hopefully you guys will be able to help me figure out what I'm doing wrong. Either I'm doing something wrong and the framework is erroneously keeping to itself, or I'm doing it right and it's totally broken. I try not to blame the framework, so which is it? Or is there another possibility?
Sample Code
Exporting the XML to docURL:
// After the user picks an XML (docURL) destination with NSSavePanel
[targetURL startAccessingSecurityScopedResource];
NSData *bookmark = [targetURL bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope
includingResourceValuesForKeys:nil
relativeToURL:docURL
error:&error];
[targetURL stopAccessingSecurityScopedResource];
Importing the XML from docURL:
// After the user selected the XML (docURL) from an NSOpenPanel
NSURL *result = [NSURL URLByResolvingBookmarkData:bookmarkData
options:NSURLBookmarkResolutionWithSecurityScope
relativeToURL:docURL
bookmarkDataIsStale:nil
error:&error];
I tried surrounding this call with[docURL ..AccessingSecurityScopedResource], which didn't make a difference (as expected, since the docURL is already within scope after having been selected in the Open Panel
Also, I specify the following in my app.entitlements file:
com.apple.security.files.user-selected.read-write
com.apple.security.files.bookmarks.app-scope
com.apple.security.files.bookmarks.collection-scope
As mentioned above, the second step (resolving the bookmark) completes, but leaves both error and result nil. As I've been implementing sandboxing, most of the mistakes I've made have resulted in an NSError being returned, which helped me to resolve the bug. But now there's no error, and no URL is resolved.
Miscellaneous troubleshooting steps
I tried placing the XML file into my app's sandbox, which didn't make a difference, so access to the XML file is not the problem
The app uses ARC, but so do the unit tests, which succeed. I tried using an alloc/init instead of the autoreleased class method, too (just in case)
I pasted the URL resolution code immediately after creating the bookmark, and it runs fine, producing a security-scoped URL
I did a po on the originally created bookmark (before serialization), and then on the bookmark after deserialization, and they match 100%. Serialization is not the problem
I replaced the resolution call with CFURLCreateByResolvingBookmarkData(..), with no change. If it is a bug, it's present in the Core Foundation API as well as the Cocoa layer
Specifying a value for bookmarkDataIsStale: has no effect
If I specify 0 for options:, then I do get back a valid NSURL, but it has no security scope, and therefore subsequent calls to read the file do still fail
In other words, the deserialized bookmark does appear to be valid. If the bookmark data were corrupted, I doubt NSURL would be able to do anything with it
NSURL.h didn't contain any useful comments to point out something I'm doing wrong
Is anyone else using security-scoped document bookmarks in a sandboxed application with success? If so, what are you doing differently than I am?
OS Version Request
Can someone with access to the Mountain Lion beta verify whether or not my sample project shows the same (lack of an) error? If it's a bug that has been fixed after Lion, I won't worry about it. I'm not in the developer program yet, and so don't have access. I'm not sure if answering that question would violate the NDA, but I hope not.
In your Gist code, change the following line in AppDelegate.m (line 61):
[xmlTextFileData writeToURL:savePanel.URL atomically:YES];
to
[xmlTextFileData writeToURL:savePanel.URL atomically:NO];
Your code will then work.
The reason for this is likely the same reason for which it is necessary to have an existing (but empty) file that will contain the document-scoped bookmarks before calling [anURL bookmarkDataWithOptions]: While creating the NSData instance, the ScopedBookmarkAgent adds something (like a tag, probably an extended file attribute) to that file.
If you write data (i.e. the bookmark URLs) to that file atomically, in fact they're written not directly to the file but first to a temporary file that is renamed if the write operation was successful. It seems that the tag that has been added to the (empty, but existing) file that will contain the bookmarks is getting lost during this process of writing to a temporary file and then renaming it (and thereby likely deleting the original, empty file).
By the way: It shouldn't be necessary to create app-scoped bookmarks before passing the respective URLs to the xml file containing the document-scoped bookmarks.
Addition: com.apple.security.files.bookmarks.collection-scope has been renamed to com.apple.security.files.bookmarks.document-scope in 10.7.4.

Fastest way to retrieve a folder's content on Mac, in obj-c

Well, the title is quite explicit, but a little explantations for those interested in the background.
I'm developing a little image browser. On part of the application is a directory browser which allows me to browse all the folders of my hard drive and mounted volumes.
And while profiling, I noticed that the most time consuming method of my application was the following piece of code :
// get the content of the directory
NSFileManager * fileManager = [NSFileManager defaultManager];
NSURL * url = [NSURL fileURLWithPath:mPath];
mCachedContent = [[fileManager contentsOfDirectoryAtURL:url
includingPropertiesForKeys:nil
options:NSDirectoryEnumerationSkipsHiddenFiles
error:nil] retain];
// parse the content, count the number of images and directories.
for (NSURL * item in mCachedContent)
{
if (CFURLHasDirectoryPath((CFURLRef)item))
{
++mNumChildren;
}
else if ([FileUtils isImage:[item path]] == YES)
{
++mNumImages;
}
}
This is necessary so that the NSOutlineView can know if a directory is expandable (and the number of images is also a feature I need)
To be more precise, the most time consuming method if [NSFileManager contentsOfDirectoryAtURL...]
So, is there any other way of getting a directory's content more efficient than the one I'm using ?
Thanks in advance for any help !
No matter how you write this function (e.g. with either Cocoa's NSFileManager API or the Unix opendir(3)/readdir(3) API), it's going to be I/O-bound—you're going to spend more time waiting on I/O than on any CPU operations performed in the middle layers.
If this is truly your bottleneck, then that means you're doing way too much I/O. Make sure you're not doing anything stupid like continually reading the contents of the same directory over and over again hundreds of times per second. If you need to continually watch a particular directory and take action whenever something in that directory changes (e.g. a file gets written to, a file is created or deleted, etc.), then use the File Systems Events API. This allows you to efficiently respond to those events when they happen without having to continually poll the directory.