NSMutableArray to NSData to .txt file - output unreadable - objective-c

I have an NSMutableArray created storing song names and artists. I am trying to convert this array to NSData format so that I can save it to a text file. When attempting to do so, the output I get is unreadable:
bplist00ÔghX$versionX$objectsY$archiverT$top † ¯$
!")-./3459:;?#AEFGKLMQRSWXY]^_cU$nullÒ
R$0V$class€€#Ò
ZNS.objectsª€€€
€
...etc.
The code I'm using to convert from an NSMutableArray to NSData is:
NSFileManager *fm;
NSData *dataCache = [NSKeyedArchiver archivedDataWithRootObject:myList];
fm = [NSFileManager defaultManager];
if ([fm createFileAtPath:#"/users/nicholasvogler/desktop/Mysongs.txt" contents:dataCache attributes:nil] == NO)
{
NSLog (#"Could not create data file");
return 1;
}
All objects in the array are NSStrings and I've added the following method for the strings and the array:
-(void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject: songname];
[encoder encodeObject: artist];
}

What exactly are you expecting the output to be? What you have there is a binary plist. That's what archived data (e.g. NSKeyedArchiver) emits. There's an XML format as well, which can be accessed via NSPropertyListSerialization, although nobody uses the XML format for archived data because the actual plist representation is an implementation detail.
So despite the fact that it's an unreadable binary blob to you, it still encodes what you want, and you can decode it again with NSKeyedUnarchiver.

If all objects in your array are strings (or dictionaries containing strings), it seems to me like it would make more sense to save it as JSON.

Related

How to store objects in plist

I have some CGPoints that I need to store in a NSDictionary then write to a file. Later, I need to be able to load the file into a NSDictionary and access the CGPoint within.
self.dict is the NSDictionary I want to store points in.
- (void)setPoint:(CGPoint)point forKey:(NSString *)key {
NSValue *value = [NSValue valueWithCGPoint:point];
[self.dict setValue:value forKey:key];
}
I also want the information to be encrypted. So I convert the NSDictionary to NSData to encrypt it.
- (void)encryptDictionaryWithKey:(NSData *)key writeToFile:(NSString *)file {
NSData *encryptedDict = [[NSKeyedArchiver archivedDataWithRootObject:self] encryptWithKey:key];
[encryptedDict writeToFile:file atomically:YES];
}
Then to get the information from the file, decrypt it, and put it in NSDictionary form:
+ (NSDictionary *)dictionaryWithContentsOfEncryptedData:(NSData *)data decryptWithKey:(NSData *)key {
NSData *decryptedData = [data decryptedWithKey:key];
return (NSDictionary *)[NSKeyedUnarchiver unarchiveObjectWithData:decryptedData];
}
I can put some other values (like NSNumber) into the NSDictionary, encrypt it and write it to file, then get it from file and decrypt it... and the value is still in tact. So my code seems to be fine. But it won't work with NSValue.
I use this to get CGPoint from NSValue. At this point, self.plist may have been (but not necessarily) encrypted, written to file, then set to an unencrypted version of the file.
- (CGPoint)pointForKey:(NSString *)key {
NSValue *value = [self.prefs objectForKey:key];
return [value CGPointValue];
}
This code only returns 0,0 (and value == nil) if self.plist has been encrypted, written to file, then loaded from the file and unencrypted.
So the NSValue with CGPoint seems to be set to nil during the process of writing to the file. I have no idea what I did wrong, so any help is appreciated . Thanks!
You can convert the CGPoint into an object that can be stored in a plist. For example, the
CGPointCreateDictionaryRepresentation() function will convert a CGPoint into an NSDictionary (or rather, a CFDictionaryRef which can be cast to an NSDictionary). You can store that in the plist, and then convert it back to a CGPoint using the CGPointMakeWithDictionaryRepresentation() companion function when you are loading the plist.

Writing FileURL's name to Pasteboard

I want to write some fileURL's to general pasteboard using [pasteboard writeObjects:pasteboardArray]; where pasteboard is an object of general pasteboard and pasteboardArray is a NSArray of NSURL's. It writes urls on pasteboard like
file://localhost/Usr/
file://localhost/Vol/
...and so on
I want to write these URL's as following
Usr
Vol
..and so on
i.e only the name of file/Directory as the system does on copying any file/Directory.
For each item on the pasteboard, there can be multiple types of data. When you write an NSURL to the pasteboard, it puts types such as public.file-url (the URL as UTF-8 text), NSFilenamesPboardType (an array of file path strings, serialized to an XML property list), "Apple URL pasteboard type" (an array of file URL strings, serialized to an XML property list), and public.utf8-plain-text (the URL as UTF-8 text).
When you copy a file in the Finder, it puts many of those same types, although it seems to convert the URLs to file reference URLs first. In addition, it puts the file's icon as an ICNS and a TIFF image. However, the public.utf8-plain-text type has just the file's name, not its URL. That string is also present with type public.utf16-plain-text.
So, presumably, the Finder is not simply using -writeObjects: to populate the pasteboard. It may be using the older approach of -declareTypes:owner: and/or -addTypes:owner: followed by -set...:forType: (e.g. -setString:forType:). Or it may be constructing instances of NSPasteboardItem with the desired types and data using its -set...:forType: methods and then writing those items to the pasteboard.
For your needs it would probably be sufficient to do:
[pasteboard writeObjects:arrayOfURLs];
NSMutableString* names = [NSMutableString string];
for (NSURL* url in arrayOfURLs)
{
NSString* name;
if ([url getResourceValue:&name forKey:NSURLLocalizedNameKey error:NULL])
{
if (names.length)
[names appendString:#"\r"];
[names appendString:name];
}
}
if (names.length)
{
[pasteboard addTypes:#[(__bridge NSString*)kUTTypeUTF8PlainText] owner:nil];
[pasteboard setString:names forType:(__bridge NSString*)kUTTypeUTF8PlainText];
}
Note: when you write multiple URLs to the pasteboard, there are multiple items, each with multiple types. However, certain types are for the collection as a whole and those only go on the first item. The plain string types and NSFilenamesPboardType are like this. The above code puts the plain string on the first item, and includes display names for all of the URLs separated by a carriage return (which is what the Finder does).
All of the above said, it's not clear if you wanted the pasteboard to contain just the display names for the URLs and not the URLs themselves in any representation. In that case, just create an array of string from the display names of the URLs and pass that array to -writeObjects: and you're done.
Update:
I've found a way to more closely match what the Finder does while taking a bit of a shortcut:
NSMutableArray* arrayOfPaths = [arrayOfURLs valueForKey:#"path"];
[pasteboard setPropertyList:arrayOfPaths forType:NSFilenamesPboardType];
NSMutableString* names = [NSMutableString string];
BOOL first = YES;
for (NSURL* url in arrayOfURLs)
{
NSString* name;
if (![url getResourceValue:&name forKey:NSURLLocalizedNameKey error:NULL])
name = #"";
if (first)
first = NO;
else
[names appendString:#"\r"];
[names appendString:name];
}
if (names.length)
{
[pasteboard addTypes:#[(__bridge NSString*)kUTTypeUTF8PlainText] owner:nil];
[pasteboard setString:names forType:(__bridge NSString*)kUTTypeUTF8PlainText];
}
When you set the data for type NSFilenamesPboardType, Cocoa automatically sets up multiple items, one for each element in the array. The second and subsequent ones each have just the single type public.file-url. The first item has that type plus most of the others that the Finder puts on the pasteboard, other than the image types. Then, it's just a matter of setting the string value on the first item.
It's important to include the NSFilenamesPboardType type because that's how multiple files were represented on the pasteboard before support for multiple pasteboard items was added. Some apps may still depend on that.
Also, if you provide the string data for each item separately (as done in your own answer), then an app which asks for the string from the pasteboard as a whole (i.e. [pasteboard stringForType:NSStringPboardType]) gets them concatenated with newlines. That's slightly different from what the app would get when files are copied from the Finder, where the strings are concatenated with carriage returns. Some apps may not cope well with the difference.
NSMutableArray *archive=[NSMutableArray array];
for (int i = 0; i < [pasteboardArray count]; i++) {
NSPasteboardItem *item = [[NSPasteboardItem alloc] init];
NSURL *url = [pasteboardArray objectAtIndex:i];
if (url != nil) {
[item setString:[url lastPathComponent] forType:(__bridge NSString*)kUTTypeUTF8PlainText];
NSString *str = [NSString stringWithFormat:#"%#",url];
[item setData:[str dataUsingEncoding:NSUTF8StringEncoding] forType:(__bridge NSString*)kUTTypeFileURL];
}
[archive addObject:item];
}
[pasteboard writeObjects:archive];
This worked for me, where pasteboard is an object of general pasteboard and pasteboardArray is a NSArray of NSURL's. Any better solutions are welcome...

Reading in from a plist, but accepting only certain strings

The goal is to have an array where all strings are of length n.
So at the moment what I have my code doing is reading in a plist (which is just 250,000 strings) into an array, and then iterating over the array in order to find which ones are/aren't of length n. Of course, for the sake of efficiency, I'd prefer being able to read in from the plist STRING BY STRING so, as I'm reading in, I may the length then before inserting into the array. I'm just starting to learn objective-c, but I was struggling to Google around for a solution =P
EDIT: Well I just found out I can find much more documentation typing property list rather than plist into google :) so I may be able to figure this out myself
You can parse plist into tree (NSMutableDictionary). Dict will have keys with name of string length.
for example
NSMutableDictionary *result = [NSMutableDictionary dictionary];
for (NSString *str in [plistDict allObjects]) {
NSString *key = [NSString stringWithFormat:#"%d", [str length]];
NSMutableArray *array = [result objectForKey:key];
if (!array) {
array = [NSMutableArray array];
}
[array addObject:str];
[result setObject:array forKey:key];
}
than you can access array with needed strings length
NSArray *string4Lenght = [result objectForKey:#"4"];
Apple doesn't provide an API for incrementally parsing a plist.
If you store your plist in XML format, you could use NSXMLParser to parse it. The schema is pretty simple and somewhat described in the Property List Programming Guide.
If you want to incrementally parse the binary format, you're going to have to do more work. There's no official documentation for the format. Apple's source code for reading and writing the format is open source (CFBinaryPList.c) and there are some useful comments along with the actual code.
If you really need to do it incrementally, I suggest going the XML route. If you do, you might want to subclass NSInputStream to be able to read from a gzip or bzip2 file and decompress on the fly.

Objective-c: Initialize NSMutableArray with NSString that is plist structure

If I have a NSString that came back from a web service in the form of a plist structure, how can I initialize a NSMutableArray with this NSString. I want to know if there is a similar way to initWithContentsOfFile for NSString.
My first thought was to save the NSString to a file and then use initWithContentsOfFile; but I am trying to avoid save to file first. It seems like there should be a simpler way.
Untested, should work like this:
NSData *data = [myString dataUsingEncoding:NSUTF8StringEncoding];
NSMuableArray *array = [NSPropertyListSerialization
propertyListWithData:data
options:NSPropertyListMutableContainers
format:NSPropertyListXMLFormat_v1_0
error:NULL];
See the Property List Programming Guide "Reading and Writing Property-List Data". It covers how to turn NSData into a property list. If you already have NSData from the network, just don't convert it to NSString. If you only have an NSString, use dataUsingEncoding: to convert it to NSData.
Check the documentation for the -propertyList and -mutableCopy methods.
You could use NSXMLParser to parse the XML (which is what a plist is) and turn it into a dictionary (and retrieve the array from there)

Parsing XML file using NSXMLParser

I have to parse an XML file using NSXMLParser. There are so many HTML tags in them, so when I am trying to parse it, it will store the string up to that and then again go to found character method and starts to append it.
my code is:
if (Bio_CResults) {
[BioResults appendString: string];
[Info appendString:string];
[stringarr addobject:Info];
NSLog(#"bio==%#",BioResults);
NSLog(#"string==%#",string);
}
and I want to add it in string array, but here it will make create extra object of array. i.e.
stringarr objectAtIndex 0 = abc
stringarr objectAtIndex 1 = def
stringarr objectAtIndex 2 = ghi
but actually I want all of them together in one object because they are actually one string only..
plz help me for that
If you don't even need multiple string objects you can use an NSMutableString instead of an array. Just use the appendString: method to add to the end of the string:
NSMutableString *string = [NSMutableString string];
[string appendString:#"abc"];
[string appendString:#"def"];
NSLog(#"New string is %#", string);
This will log "New string is abcdef".
If you really want an array, use an NSMutableArray instead of an NSArray. That way you can change an object in-place (replace a string with a new string created by appending another string). So for example:
// First create an array with #"abc"
NSMutableArray *mArray = [NSMutableArray arrayWithObject:#"abc"];
// Next get the first object as a string and append #"def" to it in-place
NSString *string = (NSString *)[mArray objectAtIndex:0];
[mArray replaceObjectAtIndex:0 withObject:[string stringByAppendingString:#"def"]];
// Now get the new first object
NSLog(#"First object is %#", (NSString *)[mArray objectAtIndex:0]);
This will log the message "First object is abcdef".
I really dont like NSXmlParser. You might want to read my blog post on using RegxKitLite
http://blog.bluespark.co.nz/?p=51
It might be of some help. Hopefully it wont lead you in the wrong direction.
Cheers, John.
There are so many HTML tags in them…
Be aware that HTML is usually not valid XML. If you're parsing HTML, an XML parser will throw an error some part of the way through the document.
You may be better off creating a WebView, loading the HTML content into it, and then getting a DOMDocument from the view's main frame and traversing the DOM hierarchy. You don't have to put the view into a window; you can just use it to get a DOMDocument.
If you are parsing XML and meant to say “XML tags”, you can disregard this answer.