Part of my iOS app will include exporting a .csv file.
I have the file made and data is added to each row as I would like.
I would like to add a header row so if a file is emailed to a person they will know what each column is for.
How do I implement something like this?
Here is my csv code below:
- (NSString *)dataFilePath
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:#"myfile.csv"];
}
- (IBAction)save:(id)sender
{
if (![[NSFileManager defaultManager] fileExistsAtPath:[self dataFilePath]]) {
[[NSFileManager defaultManager] createFileAtPath: [self dataFilePath] contents:nil attributes:nil];
}
NSString * writeString = [NSString stringWithFormat:#"%#,%#,%#\n", self.nameTextField.text, self.cityTextField.text, self.stateTextField.text];
NSFileHandle *handle;
handle = [NSFileHandle fileHandleForWritingAtPath: [self dataFilePath]];
[handle truncateFileAtOffset:[handle seekToEndOfFile]];
[handle writeData:[writeString dataUsingEncoding:NSUTF8StringEncoding]];
self.nameTextField.text = #"";
self.cityTextField.text = #"";
self.stateTextField.text = #"";
[self.stateTextField resignFirstResponder];
}
Questions 2:
How can I write over a row? Currently if I update any of the UITextFields a new row is added.
A header row in a CSV file is simply fixed data in the first row - it is up to the program that reads the CSV file to treat the first row as a header row - Excel has an option in the file open/import dialog, for example.
In your code you can simply write the header when you first create the file -
- (IBAction)save:(id)sender
{
NSString *headerRow;
if (![[NSFileManager defaultManager] fileExistsAtPath:[self dataFilePath]]) {
[[NSFileManager defaultManager] createFileAtPath: [self dataFilePath] contents:nil attributes:nil];
headerRow=#"name,city,state";
}
NSString * writeString = [NSString stringWithFormat:#"%#,%#,%#\n", self.nameTextField.text, self.cityTextField.text, self.stateTextField.text];
NSFileHandle *handle;
handle = [NSFileHandle fileHandleForWritingAtPath: [self dataFilePath]];
[handle truncateFileAtOffset:[handle seekToEndOfFile]];
if (headerRow != nil) {
[handle writeData:[headerRow dataUsingEncoding:NSUTF8StringEncoding]];
}
[handle writeData:[writeString dataUsingEncoding:NSUTF8StringEncoding]];
self.nameTextField.text = #"";
self.cityTextField.text = #"";
self.stateTextField.text = #"";
[self.stateTextField resignFirstResponder];
}
Right away you need to know that adding something to a CSV file makes it not a CSV file. Exporting with comments will work if you're also the only one importing it - in which case it is your custom data format based on CSV. There is a defined IETF standard for CSV - RFC4180.
To make it work you would need to define some escape character that informs your format parser that a line is a comment. I suggest "", - this will never normally appear in a RFC4180-compliant CSV file.
In practical terms, add your comment lines after each CSV-compliant line. In your save: method you have only one line so it will work to write a comment line and then the RFC-compliant line or in the opposite order, as long as you preserve the integrity of the RFC lines. If you ever want to save a longer string with many lines, you would need to split the NSString you are saving with something like
- (NSArray *)componentsSeparatedByString:(NSString *)separator
where your separator is a "\n", and then loop through the resulting array to write each line, adding your comments as required.
You can use a regular CSV parser still (like CHCSVParser) if you preprocess the file and strip out the lines beginning with your special comment marker. Again you would need to split the incoming file by line break and discard the comments before trying to parse it.
THANK YOU Dave DeLong for CHCSVParser! I use it often.
Related
I am working on a registration form app. Right now I'm trying to come up with a way to to dynamically create forms. Right now I have a hardcoded form, with hardcoded questions, that will save the form data into a .csv file. Here is some of the code:
// saves text field data in comma separated CSV file format
NSString *formData = [NSString stringWithFormat:#"%#,%#,%#,%#,%#,%#,%#,%#,%#,%#,%#\n",
self.nameTextfield.text, self.emailTextfield.text,
self.phoneTextfield.text, self.termTextfield.text,
self.schoolTextfield.text, self.graduationTextfield.text,
self.gpaTextfield.text, degreeString,
self.interestTextfield.text, groupString,
appliedString];
// get document directory path
NSString *documentDirectoryPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES)objectAtIndex:0];
NSString *csv = #".csv";
NSString *filename = [NSString stringWithFormat:#"%#%#", _eventName.text, csv];
// append results.csv onto doc path
NSString *event = [documentDirectoryPath stringByAppendingPathComponent:filename];
// creates folder if it does not exist
if (![[NSFileManager defaultManager] fileExistsAtPath:event]) {
[[NSFileManager defaultManager] createFileAtPath:event contents:nil attributes:nil];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:event];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[formData dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
Basically I want to be able to create a form on the app that can be any number of questions, and either uses segmented controls or text inputs. One issue I see already is formatting the file in the comma separated format. For instance, if the app creates 5 questions, how do you format the file such that formData = [NSString stringWithFormat:#"%#,%#,%#,%#,%#\n"] without hardcoding that in?
Thank you so much for your help!
One (but not the only) way to correctly create dynamic form is to build it of typical blocks (such as switch-block, textfield-block, etc). You can add these blocks to view manually or use UITableView for example. Also you need data structure describing concrete form (number and types of blocks).
Formatting file is no problem since you always can use NSMutableString and append values one by one.
I have a function which saves the data extracted from a local SQLite DB to a .CSV file.
My problem is that this function also triggers another which attaches the file to an email and sends it. As the file has not yet finished saving the attachment is sent as an empty file.
Is there anyway I can check if the file has finished saving before attaching it to the email? Please see my function below:
// When the tick is visible within the animation...
// Play the bellToneSound.
AudioServicesPlaySystemSound(bellToneSound);
// Creates a temporary GPS object that we will use to save our database as a .CSV file.
GPS *saveGPS = [[GPS alloc] init];
// Finds the phone's documents directory and creates a file path in order create a new file/folder there.
NSError *error = nil;
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataPath = [documentsDirectory stringByAppendingPathComponent:#"/Jobs"];
if (![[NSFileManager defaultManager] fileExistsAtPath:dataPath]){
[[NSFileManager defaultManager] createDirectoryAtPath:dataPath withIntermediateDirectories:NO attributes:nil error:&error];
}
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"/Jobs/%#.csv", proposalNumber]];
// Creates our new file, with a name matching "jobNo.csv" overrites old one if it already exists.
[[NSFileManager defaultManager] createFileAtPath:filePath contents:nil attributes:nil];
// Creates a file handler which will allow us to write to our file.
NSFileHandle *myHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
// Creates and writes the first line to our CSV file, which tells the program reading it what the column titles are.
NSString *csvTitleString =#"Source/Monitor, Latitude, Longitude";
[myHandle writeData:[csvTitleString dataUsingEncoding:NSUTF8StringEncoding]];
// Creates initializes another string object which will hold each line we want to write.
NSString *csvString = [[NSString alloc] init];
// Declares an array and fills it with all GPS objects found in our Database.
NSArray *allGPS = [[NSArray alloc]initWithArray:[database getAll]];
// While the current index value is less than the length of the array write the GPS values into our file then take a new line.
for(int i=0;i<(allGPS.count);i++){
saveGPS = [allGPS objectAtIndex:i];
csvString = [NSString stringWithFormat:#"\n %# %d, %#, %#", [saveGPS sourceMonitor], [[saveGPS positionNo] intValue], [saveGPS latitude], [saveGPS longitude]];
[myHandle seekToEndOfFile];
[myHandle writeData:[csvString dataUsingEncoding:NSUTF8StringEncoding]];
}
}
UPDATE
Here is the code you asked for.
NSString *docsDir;
NSString *realpath;
NSArray *dirPaths;
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
docsDir = dirPaths[0];
realpath=[[NSString alloc] initWithString:[docsDir stringByAppendingPathComponent: [NSString stringWithFormat:#"/Jobs/temp.csv"]]];
NSFileManager *filemgr = [NSFileManager defaultManager];
if ([filemgr fileExistsAtPath: realpath ] == YES)
{
[self checkIfSaved];
}
else
{
NSLog(#"file found");
// Checks if the device can send email.
if([MFMailComposeViewController canSendMail]){
// Sets the subject to data from (our current proposal number).
[mail setSubject:[NSString stringWithFormat:#"Data from %#", proposalNumber]];
[mail setMessageBody:#"Please see the attached .CSV file." isHTML:NO];
// Finds the .CSV file we just saved, and attaches it to the email.
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *filePath = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:#"/Jobs/%#.csv", proposalNumber]];
NSData *attachment = [NSData dataWithContentsOfFile:[NSString stringWithFormat:#"%#", filePath]];
[mail addAttachmentData:attachment mimeType:#"text/csv" fileName:[NSString stringWithFormat:#"%#",proposalNumber]];
// Opens up the email screen.
[self presentViewController:mail animated:YES completion:NULL];
}
else
{
// Tells the user that there device cannot send email.
NSLog(#"This device cannot send email");
// Creates a popup window to inform the user that their location wasn't updates.
UIAlertController* alert = [UIAlertController alertControllerWithTitle:#"Error"
message:#"Unable to send email. Have you set up a mail account on this device?"
preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction* dismissAction = [UIAlertAction actionWithTitle:#"Dismiss" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) {}];
alert.view.tintColor = [UIColor orangeColor];
[alert addAction:dismissAction];
[self presentViewController:alert animated:YES completion:nil];
}
}
You need to arrange your code so that the email part doesn't start until after the file has been completely saved. There are a number of ways to do that:
polling: You can keep checking the status of the file until you find that the operation is complete. Sometimes polling is unavoidable, but it's generally the worst option. Don't go this route.
completion routine: The file saving method could take a parameter that is a block of code to execute when the saving is done. This is very common, and you'll see it all over the place in the various iOS frameworks. For example, NSURLSession has a -dataTaskWithURL:completionHandler: method. When the requested data has been loaded, the completion handler is called.
serial execution: Put your file-saving code in one block and your mail sending code in another block. Then you can use NSOperationQueue or use Grand Central Dispatch directly to schedule both blocks in a serial queue, so that the second block won't start until the first one is finished.
Both the completion routine approach and the serial execution approach are reasonable ways to ensure that you don't start the e-mail process until the file is saved, so pick whichever seems easiest to you.
I have a picture slideshow app which allows the user to rate 3 images. The ratings get stored in a NSMutableArray called rated like this:
2014-01-06 07:10:23.040 SlideShowSurvey[50425:70b] (
1, <-- Rating for Picture 1
2, <-- Rating for Picture 2
3 <-- Rating for Picture 3
)
This is then saved to a .csv file using the following code:
-(void)saveRatings
{
NSString *picRatings = [NSString stringWithFormat:#"%#, \n",self.rated];
// Find documents directory
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *survey = [docPath stringByAppendingPathComponent:#"pictureRatings.csv"];
// Create new file if none exists
if (![[NSFileManager defaultManager] fileExistsAtPath:survey])
{
[[NSFileManager defaultManager] createFileAtPath:survey contents:nil attributes:nil];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:survey];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[picRatings dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
}
I can then display the results of the .csv file to a UITextView, though it displays it exactly how the array is structured. For example, multiple results display as:
(
1,
2,
3
),
(
1,
2,
3
),
(
1,
2,
3
)
Is there a way I am able to format the array so that it is saved as 1,2,3? And would I be able to add a column header like Picture 1, Picture 2, Picture 3? For example, I would like to display results on the UITextView something like:
Picture 1, Picture 2, Picture 3
1,2,3
1,2,3
1,2,3
I've tried searching but can't seem to find this answer. Any help is appreciated! Thanks.
Edit: I have solved this thanks to jrturton's solution, using the following code:
// Set column titles for .csv
NSArray *columnTitleArray = #[#"Picture 1", #"Picture 2", #"Picture 3"];
NSString *columnTitle = [columnTitleArray componentsJoinedByString:#","];
NSString *columnTitleToWrite = [columnTitle stringByAppendingString:#"\n"];
// Separate ratings into cells
NSString *picRatings = [rated componentsJoinedByString:#","];
NSString *picRatingsToWrite = [picRatings stringByAppendingString:#"\n"];
// Find documents directory
..
Then adding this to the method to make sure column headers are only set when a new file is created:
// Create new file if none exists
if (![[NSFileManager defaultManager] fileExistsAtPath:survey])
{
[[NSFileManager defaultManager] createFileAtPath:survey contents:nil attributes:nil];
// Set title for new file
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:survey];
[fileHandle writeData:[columnTitleToWrite dataUsingEncoding:NSUTF8StringEncoding]];
}
..
Here is a much easier way:
NSMutableString *csv = [NSMutableString string];
NSString *label = ...;
[csv appendFormat:#"%#,\n", label];
NSNumber *keyNum = ...;
[csv appendFormat:#"%d,%d\n", [keyNum intValue], [countNum intValue]];
NSString *filename = #"counts.csv";
NSError *error;
[csv writeToFile:filename atomically:YES encoding:NSUTF8StringEncoding error:&error];
NSMutableString *csv = [NSMutableString string];
NSString *label = #"\n";
[csv appendFormat:#",", label];
NSString *picRatings = [rated componentsJoinedByString:csv];
I'm not sure what you expect appendFormat to be doing in the above code, this will probably be raising a warning since label is not used. You don't need a mutable string in this case either.
To get the contents of an array separated by commas, do this:
NSString *picRatings = [rated componentsJoinedByString:#","];
You also need to add a new line to the end of this string, so you'd want:
NSString *picRatingsToWrite = [picRatings stringByAppendingString:#"\n"];
Write this string to your file and you should be fine.
use this parser and get the data into array and look how you can use data into UITabelViewCell https://github.com/davedelong/CHCSVParser
This question already has an answer here:
Writing a NSArray to file
(1 answer)
Closed 9 years ago.
I have an app which has several ViewControllers. Each ViewController has a question with 2-5 buttons as possible answers. The title of a button is saved to a NSMutableArray called submission. The user can go back to previous questions, change their answer, and the NSMutableArray will be updated accordingly. I need to save this array to file so new results can be saved into a UITableView each time the questionnaire is completed. I have researched and it sounds like a .plist is a good option, as all of my objects in the array are NSStrings.
A sample of my array:
2013-12-17 14:06:34.210 Questionnaire[1724:70b] (
1234,
"Dec 17, 2013",
Yes,
Games,
"Not Applicable",
Yes
)
"1234" is the User ID, the date is the Date of Birth, and the other submissions are the answers to each question.
My ViewControllers look like this:
MainViewController
InfoViewController <-- Array allocated + initialised, inserting ID and DoB
Q1ViewController <-- question
..
Q4ViewController <--question
ENDViewController <-- offers user options for Home or Results
ResultsViewController <-- UITableView ordered by User ID
SavedResultsViewController <-- UITableView showing complete submission
The NSMutableArray gets passed through each ViewController.
My questions: What method of saving to file would best suit my needs? (.plist, filetype, etc). Viewing the results on Excel would be nice (but not essential). Where should the save take place? I was thinking when the last object is inserted into the array on Q4 ViewController, so it would be saved to file when the ENDViewController is popped, is this logical? Do I need to create a new Objective-C file to store the data? I have saw a few tutorials explaining this, declaring each object in a separate NSObject file, although I'm not sure if that is needed as my objects are already stored in the array.
As you've probably guessed I am quite new to Objective-C and iOS programming, so any help offered is greatly appreciated. I am not sure how else to describe my problem, so apologies if the question is still unclear.
Edit: I have learnt a bit more about Objective-C since creating this post, and have decided to save my data to a .csv file. This file is strictly for the purpose of emailing, it doesn't get displayed on the UITableView (I am currently implementing Core Data for that). This solution might help someone in the future, so here it is:
// Set column titles for .csv
NSArray *columnTitleArray = #[#"User ID", #"DoB", #"Question 1", #"Question 2", #"Question 3", #"Question 4"];
NSString *columnTitle = [columnTitleArray componentsJoinedByString:#","];
NSString *columnTitleToWrite = [columnTitle stringByAppendingString:#"\n"];
// Separate submissions from array into cells
NSString *questionResults = [self.submission componentsJoinedByString:#","];
NSString *questionResultsToWrite = [questionResults stringByAppendingString:#"\n"];
// Find documents directory
NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
// Set file name and type
NSString *survey = [docPath stringByAppendingPathComponent:#"result.csv"];
// Create new file if none exists
if (![[NSFileManager defaultManager] fileExistsAtPath:survey]){
[[NSFileManager defaultManager] createFileAtPath:survey contents:nil attributes:nil];
// Set column titles for new file
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:survey];
[fileHandle writeData:[columnTitleToWrite dataUsingEncoding:NSUTF8StringEncoding]];
}
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:survey];
[fileHandle seekToEndOfFile];
[fileHandle writeData:[questionResultsToWrite dataUsingEncoding:NSUTF8StringEncoding]];
[fileHandle closeFile];
You could save your array as a JSON file. This would allow you to view it in a text editor, but not a spreadsheet. The following code will write it out as JSON to a file:
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"yourdata.json"];
NSError *e = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:yourdata options:NSJSONWritingPrettyPrinted error:&e];
if (jsonData) {
[jsonData writeToFile:url.path atomically:YES];
}
You could also save your data as a plist file. The following will write the data to a plist:
NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
url = [url URLByAppendingPathComponent:#"yourdata.plist"];
NSError *e = nil;
NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:yourdata format:NSPropertyListXMLFormat_v1_0 options:0 error:&e];
if (plistData) {
[plistData writeToFile:url.path atomically:YES];
}
Plists can be saved in binary or XML format, and the above will save it as XML format, which means the XML will also be readable with a text editor. Again, this won't be viewable as a spreadsheet. If you want to export your data in CSV format, you'll probably have to write your own code to output it.
You can use standard NSCoding protocol for saving data but in this case you can't read it in excel.
Or you can save it in xml.
Or you can manually create a csv (comma separated values) file and open it anywhere.
Implementation is up to you anyway :)
My code replaces text instead of inserting it starting from 5 symbol:
NSFileHandle *file = [NSFileHandle fileHandleForUpdatingAtPath: filePath];
[file seekToFileOffset: 5];
[file writeData: [value dataUsingEncoding:NSUTF8StringEncoding]];
Is there any way to insert data to text file?
That's because your code set the index position to 5 and start writing from there, thus replacing everything from 5 onwards.
I would copy the content of the file to a variable and modify it from there as a string.
as by the looks of it what you attempt to do is not possible
Update:
Given that what you need is to write from X offset this should do the trick
NSFileHandle *file;
NSMutableData *data;
const char *bytestring = "black dog";
data = [NSMutableData dataWithBytes:bytestring length:strlen(bytestring)];
file = [NSFileHandle fileHandleForUpdatingAtPath: #"/tmp/quickfox.txt"];
if (file == nil)
NSLog(#"Failed to open file");
[file seekToFileOffset: 10];
[file writeData: data];
[file closeFile];
Well this might not be an efficient way, but you could read the entire text file into an NSMutableString and then use insertString:atIndex: and then write it back out. As far as I know there is no way to insert text into an existing file. Similar question
A quick example:
NSString *path = //Your file
NSMutableString *contents = [NSMutableString stringWithContentsOfFile:txtFilePath encoding:NSUTF8StringEncoding error:NULL];
[contents insertString:#"Some string to insert" atIndex:5];
[contents writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:NULL];
Inserting data somewhere in a sequential file would require that the entire file be rewritten. It is possible, however, to add data to the end of a file without rewriting the file.