iOS Video Caching - Manual cache deletion - ios

I have a React Native application which uses React Native Video with iOS caching. I have been working on a method inside RCTVideoCache.m which would manually delete the data of a particular cache key. According to the documentation of SPTPersistentCache, which the video library uses for caching, data can be deleted either by locking/unlocking a file and invoking a wipe or after inspecting the source code of SPTPersistentCache.h with a method named removeDataForKeys.
I have tried both ways, however, unsuccessfully.
In my first try, I am using wipeLockedFiles. I have created a deleteFromCache() method inside RCTVideoCache.m. Since all my video files are unlocked by default, in this method I am trying to lock the file corresponding to my cacheKey and invoke a wipe on all locked files (which would consist of only my target cacheKey file) as it is demonstrated in the documentation. This method looks like:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil queue:nil];
[self.videoCache wipeLockedFiles];
NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes));
handler(YES);
}
The following results in two errors during compilation:
/Users/.../MyApp/node_modules/react-native-video/ios/VideoCaching/RCTVideoCache.m:79:20: error: no visible #interface for 'SPTPersistentCache' declares the selector 'lockDataForKeys:callback:queue:'
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil queue:nil];
~~~~~~~~~~~~~~~ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/.../MyApp/node_modules/react-native-video/ios/VideoCaching/RCTVideoCache.m:80:20: error: no visible #interface for 'SPTPersistentCache' declares the selector 'wipeLockedFiles'
[self.videoCache wipeLockedFiles];
~~~~~~~~~~~~~~~ ^~~~~~~~~~~~~~~
I really have no idea why these selectors are not visible from SPTPersistentCache.
In my second try, I am using removeDataForKeys(). Again, I have created a deleteFromCache() method inside RCTVideoCache.m which looks like this:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
[self.videoCache removeDataForKeys:#[cacheKey] callback:^(SPTPersistentCacheResponse * _Nonnull response) {
NSLog(#"Result output: %#", response.output);
NSLog(#"Error output: %#", [response.error localizedDescription]);
} onQueue:dispatch_get_main_queue()];
NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes));
handler(YES);
}
In this second way, there are no errors, however, the data of the key is never deleted. Also, both NSLogs for the response output null inside the terminal.
I am 100% sure that the cacheKey I am providing to my deleteFromCache() method is correct and data corresponding to it exists. However, in both methods NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes)); does not change and I can also manually verify that the file has not been deleted.
I am really stuck and do not know what is wrong with the code I've written in both cases and why neither of them works. I would appreciate any help on this!

You can delete all sub-folder's files (tmp/rct.video.cache), iterating each one:
+ (void)deleteFromCache
{
NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.temporaryCachePath error:NULL];
for (NSString *file in tmpDirectory) {
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:#"%#%#", self.temporaryCachePath, file] error:NULL];
}
}

I ran your example and discovered that you are using incorrect method signatures. These methods simply don't exist in the caching library, their signatures are different.
Try something like this:
- (void)deleteFromCache:(NSString *)cacheKey withCallback:(void(^)(BOOL))handler;
{
NSLog(#"Size before = %#", #(self.videoCache.totalUsedSizeInBytes));
[self.videoCache lockDataForKeys:#[cacheKey] callback:nil onQueue:nil];
[self.videoCache wipeLockedFilesWithCallback:^(SPTPersistentCacheResponse * _Nonnull response) {
NSLog(#"Size after = %#, response = %#", #(self.videoCache.totalUsedSizeInBytes), response);
// Call handler after the files are wiped
handler(YES);
} onQueue:nil];
}
I have no idea why the second approach doesn't work, but NSLog(#"Size = %#", #(self.videoCache.totalUsedSizeInBytes)); is for sure called before the actual deletion happens. In the example I posted above, I have moved the logging statement into the callback closure, so that it reports the size before and after the deletion takes place.

Related

Xcode - Dropbox - NSMutabledictionary

I'm iterating through the user's Dropbox content (using the loadedMetaData delegate) in order to get information about all files and folder names present in the user's dropbox (I need this so that I cab download all necessary data to the Documents folder of the app for offline use). The information shall be stored in an NSMutabledirectionary where "key = folder-name" and the object is always an array containing all files inside the folder. I'm doing this:
-(void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata
{
if (metadata.isDirectory) {
//subfolders are loaded into array
for (DBMetadata *directory in metadata.contents) {
if (directory.isDirectory)
{
[directoryList addObject:directory.filename];
//
}
}
//files are loaded according loadmeta-folder (first run is root folder)
[fileList removeAllObjects];
for (DBMetadata *file in metadata.contents) {
if (!file.isDirectory)
{
NSLog(#"Directy is called %#", metadata.path);
//NSLog(#"%# was last changed %#", file.filename, file.lastModifiedDate);
[fileList addObject:file.filename];
[fileRevisionDates addObject:file.lastModifiedDate];
}
}
NSLog(#"Key is called %#", metadata.path);
//This is where I store the information in the dictionary
[subFolderContent setValue:fileList forKey:metadata.path];
//loadmetadata for all sub-folders
for (int i = 0; i < directoryList.count; i++) {
[restClient loadMetadata:[NSString stringWithFormat:#"/TestFolder/%#/", [directoryList objectAtIndex:i]]];
}
}
}
Now this basically works, but of course, every time the array "fileList" is updated, all values for each key in the dictionary are updated accordingly... What am I missing? Or is there a better way to achieve this?
Thanks for your help!
Tom
Here's some rough code that might work. (I haven't tested it at all.) To answer your direct question, note that I'm initializing a new fileList in this method instead of having a global one somewhere else. This is what the commenters above were getting at.
I also did a little cleanup:
There's no need to keep the list of directories in an array and then call loadMetadata on each later. You can just call loadMetadata on each directory as you see it.
I deleted fileRevisionDates since it didn't seem usable in its current state. (I guess it was literally an array of dates? You probably need an NSMutableDictionary instead.)
I fixed the path construction on the recursive call to loadMetadata. If a user's Dropbox contained a directory like /foo/bar/baz, it looked like your code would try to get metadata on a path like /TestFolder/baz. I think the change I made will take care of that.
Again, I haven't tested this code at all (and my Objective-C is not very good), so there may be bugs/typos. Others should feel free to suggest edits:
-(void)restClient:(DBRestClient *)client loadedMetadata:(DBMetadata *)metadata {
if (metadata.isDirectory) {
NSMutableArray *fileList = [[NSMutableArray alloc] init];
for (DBMetadata *entry in metadata.contents) {
if (entry.isDirectory) {
[restClient loadMetadata:[NSString stringWithFormat:#"%#/%#", metadata.path, entry.filename]];
}
else {
[fileList addObject:entry.filename];
}
}
[subFolderContent setValue:fileList forKey:metadata.path];
}
}
Finally, please note that this is not a great way to enumerate the contents of a user's Dropbox. See https://blogs.dropbox.com/developers/2013/12/efficiently-enumerating-dropbox-with-delta/ for the preferred approach, which is to use the /delta endpoint.

Share Extension loadItemForTypeIdentifier returns error for NSURL

I have the following code to read in passed URLs. I'm testing this with the Pocket app and although hasItemConformingToTypeIdentifier is returning YES for kUTTypeURL, trying to load it in returns a error instead stating
"Unexpected value class."
. If I try to load it as an id<NSSecureCoding> item and debug, I find that the passed in object is indeed just the title of the page and not the URL. How do I read the URL?
NSURL *pageURL = nil;
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier: (NSString*) kUTTypeURL]) {
[itemProvider loadItemForTypeIdentifier:(NSString*) kUTTypeURL options:nil completionHandler:^(id <NSSecureCoding> urlItem, NSError *error) {
if ([((NSObject*)urlItem) isKindOfClass: [NSURL class]]) {
pageURL = [((NSURL*)urlItem) absoluteString];
}
}];
}
}
}
If you read the documentation for:
loadItemForTypeIdentifier(_:options:completionHandler:)
You'll see that:
The type information for the first parameter of your completionHandler
block should be set to the class of the expected type. For example,
when requesting text data, you might set the type of the first
parameter to NSString or NSAttributedString. An item provider can
perform simple type conversions of the data to the class you specify,
such as from NSURL to NSData or NSFileWrapper, or from NSData to
UIImage (in iOS) or NSImage (in OS X). If the data could not be
retrieved or coerced to the specified class, an error is passed to the
completion block’s.
Maybe you can experiment by coercing to different types?
Try this
__block NSURL *pageURL = nil;
for (NSExtensionItem *item in self.extensionContext.inputItems) {
for (NSItemProvider *itemProvider in item.attachments) {
if ([itemProvider hasItemConformingToTypeIdentifier: (NSString*) kUTTypeURL]) {
[itemProvider loadItemForTypeIdentifier:(NSString*) kUTTypeURL options:nil completionHandler:^(NSURL *urlItem, NSError *error) {
if (urlItem) {
pageURL = urlItem;
}
}];
}
}
}
And now if you want take URL of your current site use
NSString *output = [pageURL absolutestring];
Output - will be your URL.
I stumbled over this issue now myself. The Pocket App seems to be the only App which shows this issue. The strange thing is that there are Apps which can get the URL form Pocket. Like for example Firefox for iOS. Firefox is Open Source so I looked at its code (at Github) and found that it is doing exactly the same to get the URL that is shown here. The only difference is that Firefox is written in Swift, and my code (and the one posted here) is Objective C. So I wonder if the Pocket App is doing something strange that is triggering a bug in the Objective C API of the iOS only, so Swift Apps are not affected? I do not have any Swift experience yet, so I haven’t checked if switching to Swift would „solve“ this.
The fact that the method "hasItemConformingToTypeIdentifier:“ states that there is a URL available but „loadItemForTypeIdentifier:“ can not deliver it, is a clear indication that the iOS itself has a bug here (at least in the Objective C API). But there still must be something special the Pocket App is doing to trigger this bug, because otherwise this would not work in all other Apps.

iOS Error: request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_

I'm using Nico Kreipke's FTPManager (click here to go to GiHub) to download some data from an FTP address.
The code works if it's run before the user's first interaction, after that it will usually fail (about 9 out of 10).
When it fails, the following message is written (0x_ are actually valid addresses):
request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_
That message isn't written by neither my code nor by FTPManager, but by Apple's. On its GitHub, I've found some one with the same error, but the source of it could possible be the same as mine. (That person wasn't using ARC.)
If I try to print the objects of those addresses with the pocommand, the console writes that there's no description available.
Also, the memory keeps adding up until the app receives a memory warning, and soon after the OS terminates it.
By pausing the app when that message appears, I can see that the main thread is in a run loop.
CFRunLoopRun();
The Code
self.ftpManager = [[FTPManager alloc] init];
[self downloadFTPFiles:#"192.168.2.1/sda1/1668"];
ftpManageris a strong reference.
The downloadFTPFiles: method:
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer* server = [FMServer serverWithDestination: basePath username:#"test" password:#"test"];
NSArray* serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary * sDataI = serverData[i];
NSString* name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber* type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString * nextDestination = [basePath stringByAppendingPathComponent: name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
[self.ftpManager downloadFile:name toDirectory:[NSURL fileURLWithPath:NSHomeDirectory()] fromServer:server];
}
}
}
What I've Done
I've tried running that code on several places:
The app delegate's application:didFinishLaunchingWithOptions:;
The viewDidLoad, viewWillAppear: and viewDidAppear: of the a view controller loaded just after the app launches and a view controller presented later.
By an action triggered with a button event.
The download of the data is always well performed when executed by the delegate or a view controller loaded with the app (with an exception). But when run after the user's first interaction with the app, it'll most likely fail with the mentioned error.
The exception for view controllers loaded before the user's first interaction is when the call is in either the viewWillAppear: or viewDidAppear: methods. When it's called a second time (for example, a tab of a tab bar controller) it'll also, most likely, fail.
The Question
Does anyone have an idea of what may be happening, or if I'm doing something wrong? Or any alternative solution, maybe?
Any help to solve this problem will be welcomed.
Thanks,
Tiago
I ended up sending the downloadFile:toDirectory:fromServer: message inside a dispatch_async block. I've also created an FTPManage for every file downloaded.
It worked, but I have no idea why.
I'm leaving this answer to whomever crosses with this problem.
If anyone can let me know why this technique worked, please comment bellow so I can update the answer.
Here's the new way I'm downloading each file:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
Again, If you know why this worked, let me know.
Thanks.
Full Method
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer *server = [FMServer serverWithDestination:basePath username:#"test" password:#"test"];
NSArray *serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary *sDataI = serverData[i];
NSString *name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber *type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString *nextDestination = [basePath stringByAppendingPathComponent:name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
}
}
}

Need a generic way to extract error text

I've developed some iOS 6.1 code to deal with NSError. But, I'm not happy with it. It is at best a hack:
-(bool) reptErrAtModule: (NSString *) module
atSubr: (NSString *) subr
atFunc: (NSString *) func
withErr: (NSError *) err
{
id value = [[err userInfo] objectForKey: NSUnderlyingErrorKey];
NSString * errDesc = (value != nil) ?
[value localizedDescription]:
(NSString *)[[err userInfo] objectForKey: #"reason"];
NSLog( #"ERR -> %#",[NSString stringWithFormat:
#"(%#>%#) %# failed! %#",module,subr,func,errDesc] );
}
I had a simpler form (without the (NSString *)[[err userInfo] objectForKey: #"reason"] case) and it worked for errors that I got back from calls to removeItemAtPath.
But then I got an error back from this code:
NSPersistentStore * entStor =
[myPerStoCor addPersistentStoreWithType: NSSQLiteStoreType
configuration: nil
URL: [NSURL fileURLWithPath: Path]
options: nil
error: &err];
And my routine failed to extract the error. So I added the #"reason" logic because I could see the text I wanted in the Info data in the debugger.
Now the code works with both types of errors but I'm thinking this is not the way to do this. There must be a better, more generic way to deal with all the types of errors stuff the system can give you back in NSError.
I use this:
NSString *description = error.localizedDescription;
NSString *reason = error.localizedFailureReason;
NSString *errorMessage = [NSString stringWithFormat:#"%# %#", description, reason];
For debugging purposes, you ideally want to log out the entire contents of the error. Roughly speaking this is the domain, code, and userInfo. Bear in mind that userInfo might well include an underlying error, which you want to apply the same logic to. And in some cases, the error might supply a description (or failure reason etc.) which isn't present in the userInfo.
If you scroll down my blog post at http://www.mikeabdullah.net/easier-core-data-error-debugging.html, there's a snippet there showing how to generate a dictionary representation of an NSError object, and then get a string representation of that. This is pretty handy for debugging/logging purposes.
For presentation to users though, -[NSError localizedDescription] is expressly designed for such purposes. -localizedFailureReason serves a similar role, tending to specify what went wrong, without the context of the operation being tried. (One way to think of it is localizedDescription = task desceription + localizedFailureReason)

Core data related objects being removed

I'm currently stuck with a weird problem with core data. The app I'm writing downloads a bunch of data from the server, which is translated into core data objects and stored. The device can also create new objects and upload them to the server. One of these objects is a Document which is essentially a representation of a file.
The model for this is MPDocument. A document can also be linked to an MPPlace model, and an MPUser model (users create documents, and documents belong to places).
I'm having no problem downloading the objects from the server, and all relationships are being created and assigned correctly. The problem lies when I try to create a new document on the device itself. The document gets created, and I set all of the relationships, the document gets uploaded and everything seems fine. But when I check the database through a core-data viewer tool, all of the document objects have no value for the place relationship. This happens to all the existing documents, not just the new one. I really can't figure out what's going on!
I'm creating the document like so :
MPUser *current = [MPUser currentUser];
MPDocument *doc = [[MPDocument alloc] init];
doc.name = #"App Upload";
doc.local_url = [NSString stringWithFormat:#"%#", [info valueForKey:UIImagePickerControllerReferenceURL]];
doc.local_url_type = #(MPDocumentUrlTypeAsset);
doc.user = current;
[current addCreatedDocumentsObject:doc];
[doc setValue:self.place forKey:#"place"];
[self.place addDocumentsObject:doc];
I then have a document uploader which handles all uploading :
MPDocumentUploader *uploader = [[MPDocumentUploader alloc] initWithDocument:doc];
uploader.requestDelegate = self;
uploader.successBlock = ^(MPDocumentUploader *uploader, MPDocument *doc) {
NSLog(#"Got doc = %#", doc);
};
[uploader upload];
When the success block is called, the document object DOES have the place relationship set. So even once the upload has finished, the place is set, so I'm really confused now as to where the relationships are being completely cleared.
The document uploader looks something like this :
- (void) upload
{
.... retrieve the local file and turn into NSData. This is fine
MPRequest *request = [MPRequest requestWithURL:_url];
[MPUser signRequest:request];
[request setDelegate:_requestDelegate];
[request setRequestMethod:#"POST"];
[request mountDocumentUploader:self];
[request submit:^(MPResponse *resp, NSError *error) {
if (!error) {
NSDictionary *data = (NSDictionary *)[resp paramForKey:#"data"];
if (data) {
NSLog(#"Document = %#", _document);
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
}
if (_successBlock) {
_successBlock(self, _document);
}
} else {
if (_failBlock) {
_failBlock(self, error);
}
}
}];
}
The MPRequest class handles all the actual uploading and server requests, but doesn't actually touch the MPDocument object.
I can't figure out what's going on or why it's clearing out the relationships. Please can someone help!?
Update
I've played around, and found that the error occurs when the submit block is called. Commenting out
_document.url = [data objectForKey:#"url"];
_document.objID = [data objectForKey:#"id"];
[_document saveLocally];
works as it is meant to, but now those values obviously aren't set. Adding either of those lines back in 1 at a time in isolation still causes the problem, so it seems that simply editing it at all is breaking it. Still no clue why though :(
ok well I'm not entirely sure what happened or why, but between fiddling with the code, cleaning the project, and restarting my laptop, it seems to have fixed itself so not a clue what the issue was but it currently seems to be ok. Very confused :S

Resources