I'm having a lot of trouble deciphering Apple's documentation around UIManagedDocument, specifically the following methods:
- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError **)error
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError **)error
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error
Has anyone successfully managed to save additional content into the "addition content" directory inside their UIManagedDocument packages? I'm looking to save straight images (PNGs, JPEGs, etc) and videos (m4v, etc) into this directory using UUIDs as the filenames (with the correct file extension), and storing references to these individual files as NSString file paths within my persistent store.
Credit goes to Apple DTS for helping me understand this class. I'm sharing some of the example they helped me with here (modified slightly).
OK, so basically it works like this: subclass UIManagedDocument, and implement the following methods (where the extraInfo property is just an NSDictionary implemented on our subclass):
- (BOOL)readAdditionalContentFromURL:(NSURL *)absoluteURL error:(NSError **)error
{
NSURL *myURL = [absoluteURL URLByAppendingPathComponent:#"AdditionalInformation.plist"];
self.extraInfo = [NSDictionary dictionaryWithContentsOfURL:myURL];
return YES;
}
- (id)additionalContentForURL:(NSURL *)absoluteURL error:(NSError **)error
{
if (!self.extraInfo) {
return [NSDictionary dictionaryWithObjectsAndKeys: #"Picard", #"Captain", [[NSDate date] description], #"RightNow", nil];
} else {
NSMutableDictionary *updatedFriendInfo = [self.extraInfo mutableCopy];
[updatedFriendInfo setObject:[[NSDate date] description] forKey:#"RightNow"];
[updatedFriendInfo setObject:#"YES" forKey:#"Updated"];
return updatedFriendInfo;
}
}
- (BOOL)writeAdditionalContent:(id)content toURL:(NSURL *)absoluteURL originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error
{
if (content) {
NSURL *myURL = [absoluteURL URLByAppendingPathComponent:#"AdditionalInformation.plist"];
[(NSDictionary *)content writeToURL:myURL atomically:NO];
}
return YES;
}
UIManagedDocument will call these methods when it needs to, automatically saving whatever you need to save to the document package inside an AdditionalContent directory.
If you need to force a save, simply call the following on your UIManagedDocument instance:
[self updateChangeCount:UIDocumentChangeDone];
At present, I'm not using this for images and videos — but the example should give you enough to go off.
The documentation for -additionalContentForURL:error: indicates that returning a nil supposed to signal an error.
A return value of nil indicates an error condition. To avoid generating
an exception, you must return a value from this method. If it is not always
the case that there will be additional content, you should return a sentinel value (for example, an NSNull instance) that you check for in
writeAdditionalContent:toURL:originalContentsURL:error:.
I override -writeContents:andAttributes:safelyToURL:forSaveOperation:error: for another purpose (doing some stuff on first save of a new document), and calling super invokes the NSException gods because contents value is nil, not an NSDictionary as seemingly expected by UIManagedDocument. Hmm.
The more you know...
P.S. I guess it depends on the time of day with -writeContents:andAttributes:... It once threw an exception complaining about expecting an NSDictionary, but later threw an exception complaining that I didn't pass it an NSData. My eyebrow could not be raised in a more Spock-like fashion than it is right now.
Related
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.
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.
I've been stuck on this for approximately two weeks. I hate posting things that have been asked a lot but I really have gone through them all.
I used Ray Wenderlich's tutorial for saving data in an iPhone app.
http://www.raywenderlich.com/tutorials
So that is the setup I have going on in my app. I'm saving very simple objects. My Card object consists of a name, type, and image. That's all. So the tutorial is quite close to mine. Which is making this more frustrating.
The thing is, I have some NSLog statements in there for loading. I have it displaying the folder it's using to load and what objects it does load. Right now it is displaying this.
Loading cards from /Users/zach/Library/Application Support/iPhone Simulator/7.0.3-64/Applications/E3DB01FD-A37E-4A69-840B-43830F2BDE2C/Library/Private Documents
2013-11-04 00:02:50.073 CardMinder[84170:a0b] ()
So it seems to be trying to load them, but there's nothing there to load. Here is my function to save data.
- (void)saveData {
if (_data == nil) return;
[self createDataPath];
NSString *dataPath = [_docPath stringByAppendingPathComponent:kDataFile];
NSMutableData *data = [[NSMutableData alloc] init];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:_data forKey:kDataKey];
[archiver finishEncoding];
NSLog(#"%#",dataPath);
NSLog(#"%#",data);
[data writeToFile:dataPath atomically:YES];
}
Which is really just what's posted in that tutorial. I know if you feel generous enough to help me out i'll have to post some more code but I don't want to flood the post with useless stuff so just let me know and i'll get it out here.
I really appreciate anyone that can help, I have recently entered the desperation state and need help.
Thanks
UPDATE
NSError *error;
[data writeToFile:dataPath options:NSDataWritingAtomic error:&error];
NSLog(#"error: %#", error.localizedFailureReason);
These are the methods for the CardData class. I'm doing the name, type, and a bool here.
- (void)encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:_name forKey:kNameKey];
[aCoder encodeObject:_cardType forKey:kTypeKey];
[aCoder encodeBool:_checkedOut forKey:kOutKey];
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
NSString *name = [aDecoder decodeObjectForKey:kNameKey];
NSString *cardType = [aDecoder decodeObjectForKey:kTypeKey];
BOOL checkedOut = [aDecoder decodeBoolForKey:kOutKey];
return [self initWithName:name cardType:cardType _Bool:checkedOut];
}
UPDATE 2
I just put some more NSLog statements in and I found out that when I press the "Save card" button in my app, it doesn't seem to execute the saveData function at all. I have log statements galore in that saveData function and when I click the saveCard button it doesn't show any of those logs. Why would that be happening?
This is my saveButton code.
- (IBAction)saveNewCard:(id)sender
{
NSString *cardName = self.nameField.text;
_cardDoc.data.name = cardName;
CardDoc *newCard = [[CardDoc alloc] initWithName:cardName cardType:cardTypeString _Bool:NO image:chosenIcon];
[_cardDoc saveData];
NSLog(#"Card save button pressed!");
CardViewController *cardViewController = (CardViewController *)[self.navigationController.viewControllers objectAtIndex:self.navigationController.viewControllers.count-2];
[cardViewController.cards addObject:newCard];
[self.navigationController popToRootViewControllerAnimated:YES];
}
You should use writeToFile:options:error: instead of writeToFile:atomically:; that will give you an error message that should prove helpful. (The equivalent to atomically:YES is the option constant NSDataWritingAtomic.) Make sure you're getting back a return value of YES; if not, the error should be set.
If you're getting a value of NO but the error is not set, it means you're messaging nil. A quirk of Objective-C is that messaging nil is completely valid. If the method is defined to return something, you'll even get a result: 0 or equivalent (NO, nil, etc.)
In this case, you're messaging _cardDoc. There's no return result to detect. This is a bit harder to defensively code around, but [_cardDoc saveData] is actually [nil saveData]. The debugger will just breeze past the line.
Generally, if something absolutely should not be nil, you can use NSAssert:
NSAssert(_cardData, #"_cardData should not be nil");
[_cardData saveData];
But use this sparingly; you'll probably come to usually appreciate this behaviour.
A few things.
Post the results of your log statements so we know what you are seeing.
In order for your approach to work, your _data object needs to conform to the NSCoding protocol. That means you need to add the protocol declaration to your interface, and implement the methods encodeWithCoder and initWithCoder.
In those methods you need to save all the state data for your object / load the state back into your object.
Those methods are the most likely source of problems with your code. Post those methods if you need help with them, and walk though them in the debugger.
You might also look at the NSKeyedArchvier class method archivedDataWithRootObject. That method takes an object and encodes it into an NSData object in one step. The method archiveRootObject:toFile: take it a step further, and writes the data directly to a file for you.
NSKeyedUnarchiver has the corresponding methods unarchiveObjectWithData and unarchiveObjectWithFile to recreate your object from data/a file.
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)
What is the best approach for sending the local player's alias (or any text for that matter) to another device, since I can't put an NSString in my struct because of ARC/pointers?
So far, I've tried converting to & from a char array, using the __unsafe_unretained- option and trying to create a class to put the text in. Though all three of these attempts worked through compiling, they crashed the device (simulator keeps running but no alias displays.)
Is sending text in multiplayer games really difficult when using ARC? The issues I'm facing are most likely a result of the fact that I am not very experienced at programming... so if anyone could point me in the right direction or provide me with some snips of code, I'd really appreciate it.
You can easily encode and decode strings to NSData objects and send them over Game Center.
To encode: use this method on a string
- (NSData *)dataUsingEncoding:(NSStringEncoding)encoding
With encoding:NSUTF8StringEncoding
This will return a NSData object
To decode;
NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
Edit:
Essentially you'll never have to send player aliases explicitly. There are 2 cases:
1: GKTurnBasedMatch
If you're using this, then here's how to get a list of all of the aliases (including yours)
NSMutableArray *playerIds = [NSMutableArray arrayWithCapacity:match.participants.count];
for (GKTurnBasedParticipant *part in match.participants) {
if([participant.playerID isKindOfClass:[NSString class]]){
[playerIds addObject:part.playerID];
}
} //at this point you have an array full of playerID strings, next you call this:
[GKPlayer loadPlayersForIdentifiers:(NSArray *)playerIds withCompletionHandler:(void (^) (NSArray *players, NSError *error))completionHandler {
for (GKPlayer *playa in players) {
NSLog(#"%#",playa.alias); // here i'm just logging the aliases but you can do whatever..
}
}];
2.GKMatch : this case is much easier since your GKMatch has already playerIDs array, same as before:
[GKPlayer loadPlayersForIdentifiers:(NSArray *) match.playerIDs withCompletionHandler:(void (^)(NSArray *players, NSError *error))completionHandler {
//again you get the array players full of GKPlayer objects , simply pull the alias you want
}];