Blog > Komentarze do wpisu
Importing CSV data file into an iOS app via email attachment - part 1 [English]
Data export/import capabilities are very important in most of utility apps. Below I’d like to demonstrate how to implement a data import from a CSV (Comma Separated Values) file via email attachment. In this tutorial we will create a small application that imports data from a CSV file into an array of objects and displays them using a table view. Click ...CZYTAJ DALEJ... below to read on.

 

Difficulty level: intermediate.



Data export/import capabilities are very important in most of utility apps. Below I’d like to demonstrate how to implement a data import from a CSV (Comma Separated Values) file via email attachment.



In this tutorial we will create a small application that imports data from a CSV file into an array of objects and displays them using a table view.



>>> START HERE <<<



Our file will hold some basic information about US presidents. Its structure is very, very simple (see below) but the approach used can be applied to more complex data as well:


-    number,
-    name,
-    Wikipedia URL
-    took office date,
-    left office date,
-    party,
-    home state.



Here are a few sample rows (first row represents a header):



No,President Name,Wikipedia URL,Took office,Left office,Party,Home State
1,George Washington,http://en.wikipedia.org/wiki/George_Washington,30/04/1789,4/03/1797,Independent,Virginia
2,John Adams,http://en.wikipedia.org/wiki/John_Adams,4/03/1797,4/03/1801,Federalist,Massachusetts
3,Thomas Jefferson,http://en.wikipedia.org/wiki/Thomas_Jefferson,4/03/1801,4/03/1809,Democratic-Republican,Virginia
4,James Madison,http://en.wikipedia.org/wiki/James_Madison,4/03/1809,4/03/1817,Democratic-Republican,Virginia
5,James Monroe,http://en.wikipedia.org/wiki/James_Monroe,4/03/1817,4/03/1825,Democratic-Republican,Virginia
6,John Quincy Adams,http://en.wikipedia.org/wiki/John_Quincy_Adams,4/03/1825,4/03/1829,Democratic-Republican/National Republican,Massachusetts


 

The screenshot below shows the working app displaying imported data.

 

iOS working app

Note 1. File handling was introduced in iOS 3.2 and the code demonstrated here should work on all devices running iOS 3.2 or newer. The project was tested on the devices running iOS versions 4.3.1 and 5.0.



Note 2. To build this project I’m using the most recent Xcode 4.2 with some of the features introduced in that version especially ARC (Automatic Reference Counting). If you’re using older Xcode you can still follow the tutorial, but remember to implement proper memory management.

 

 

Memory management in a nutshell

If you create an object with alloc or copy send release or autorelease message to it at the end of the function. If you create the object any other way do nothing.





Let’s starts with creating a new project using an empty application template:

 

Xcode - empty app

 

Let’s name it US Presidents:

 

Importing CSV data file into an iOS app via email attachment

 

The screenshot below shows files initially included in our project.

 

Importing CSV data file into an iOS app via email attachment

 

Now let’s create a new view controller. In Xcode select menu File -> New -> New File... or press ⌘ + N and from iOS Cocoa Touch select UIViewController subclass.

 

Importing CSV data file into an iOS app via email attachment

 

Let’s call our controller TVCPresidents and make sure it inherits from UITableViewController.

 

Importing CSV data file into an iOS app via email attachment

 

Now we have to modify our app delegate so it uses our new TVCPresidents controller.



In AppDelegate.h import TVCPresidents.h file and add a new property called viewController. Our AppDelegate.h file now looks as follows:




 

#import 
#import "TVCPresidents.h"

@interface AppDelegate : UIResponder 

@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) TVCPresidents *viewController;

@end

 

In AppDelegate.m we also have to synthesize our property (within implementation):

 

@synthesize viewController = _viewController;

 

and modify didFinishLaunchingWithOptions method:

 

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.viewController = [[TVCPresidents alloc] init]; //arc
    //in iOS4+ we can simply do this:
    //self.window.rootViewController = self.tabBarController;
    //but in iOS3 we have to do this:
    [self.window addSubview:self.viewController.view];
    [self.window makeKeyAndVisible];
    
    return YES;
}

 

we can also remove numberOfSectionsInTableView method as we won’t need it.

 

Ok, we can now start our application now (we can ignore a compilation warning(s), if any, for time being):

 

Importing CSV data file into an iOS app via email attachment

It’s working but it isn’t very exciting...



Move on then. Let’s configure our app so it gets open when we tap on a CSV file email attachment. It’s a two step process:

 

1. First we have to register our application so it can handle files of a particular type (CSV in this case). We’re doing this by adding CFBundleDocumentTypes key to our app’s Info.plist file.
2. For all non system-defined UTIs (Uniform Type Identifiers) we also need to setup UTExportedTypeDeclarations so system can be made aware of them.

 

Let’s open application Info.plist file:

 

Importing CSV data file into an iOS app via email attachment

 

Default Info.plist content is shown below:

 

Importing CSV data file into an iOS app via email attachment

 

It’s represented by the following XML file (if you fancy open it in a different editor than Xcode :):

 

Importing CSV data file into an iOS app via email attachment

 

Let’s add CFBundleDocumentTypes. Click RMB on the white space below default settings and select Add Row:

 

Importing CSV data file into an iOS app via email attachment

 

From the drop down list select “Document types”. The key matches CFBundleDocumentTypes in XML file as we’ll see later.

 

Importing CSV data file into an iOS app via email attachment

 

Together with our key one item - “Item 0” - is automatically created. It has two following sub keys:
- Document Type Name
- Handler Rank

 

We have to add two more rows (use a small + icon, placed on the right hand side of the “Document Type” row) to create a new entry:
- Role
- Document Content Type UTIs

 

The screenshot below shows all items with matching values:

 

Importing CSV data file into an iOS app via email attachment

 

In XML it looks like this:

Importing CSV data file into an iOS app via email attachment

 

Now let’s define UTExportedTypeDeclarations. We have to add another row to the .plist file. Select “Exported Type UTIs” from the key’s drop down list.



We have to set it up as shown on the screen below:

 

Importing CSV data file into an iOS app via email attachment

 

And in XML we see now the following:

Importing CSV data file into an iOS app via email attachment

 

Our complete Info.plist looks like this:

Importing CSV data file into an iOS app via email attachment

 

Now our app starts to receive notifications about defined file type - CSV. We can test it quickly by tapping an email attachment.

 

Importing CSV data file into an iOS app via email attachment      Importing CSV data file into an iOS app via email attachment

 

Importing CSV data file into an iOS app via email attachment

 

 

Note. Because iOS recognizes and can handle CSV files, the usual tap on a CSV email attachment will cause a Quick Look function to fire. If we want to open the file in our app we have to tap on it and hold for a second or two.

 

When we pick “Open in “US Presidents”” our app starts but nothing actually happens. We’ll have to add a few more lines of code to handle the file.



Let’s add a new method to out table view controller - TVCPresidents:



To the header (.h) file we have to add:

 

-(void) handleOpenURL:(NSURL *)url;

 

So it now looks as follows:

 

#import 

@interface TVCPresidents : UITableViewController

- (void)handleOpenURL:(NSURL *)url;

@end

 

And to our implementation file (.m) we have to add its body:

 

-(void) handleOpenURL:(NSURL *)url {
    NSError *outError;
    NSString *fileString = [NSString stringWithContentsOfURL:url 
                                                    encoding:NSUTF8StringEncoding error:&outError];
    NSLog(fileString);
}

 

Now let’s go back to our app delegate code and modify the didFinishLaunchingWithOptions method. In AppDelegate.m file add the following at the end (before the RETURN statement of course :).

 

NSURL *url = (NSURL *)[launchOptions valueForKey:UIApplicationLaunchOptionsURLKey];
if (url != nil && [url isFileURL]) {
   [self.viewController handleOpenURL:url];                
}

 

Note. The code above works fine for iOS 3.2 +. If your app supports newer systems iOS 4.0+ only you may use self.window.rootViewController instead of self.viewController here.

 

We also have to add two new methods to AppDelegate.m file:

 

-(BOOL) application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation
{
    if (url != nil && [url isFileURL]) {
        [self.viewController handleOpenURL:url];                
    }     
    return YES;
}

//Please note: This method is deprecated. It’s added here for backward compatibility
-(BOOL) application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    
    if (url != nil && [url isFileURL]) {
        [self.viewController handleOpenURL:url];                
    }     
    return YES;
    
}

 

 

Voila. Our app can now read files sent to us as email attachments. Because of the temporary NSLog call in our main handleOpenURL method we can clearly see the file content in app console log when we run our app as before (via email attachment):

 

Importing CSV data file into an iOS app via email attachment

 

To fully understand what is happening when application starts take a look at the diagram below. Remember this is just an extract from all of the UIApplication delegate messages :)

 Importing CSV data file into an iOS app via email attachment

 

We almost there. We can see the contents of the file but we still have to process it.
In order to do this we’ll use the NSString class category described here:



http://www.macresearch.org/cocoa-scientists-part-xxvi-parsing-csv-data



Let’s add it to our project.

 

To add a category to our project from Xcode menu select File -> New -> New File... or press ⌘ + N and from iOS Cocoa Touch select Objective-C category:

 

Importing CSV data file into an iOS app via email attachment

 

Let’s stick to the original name: ParsingExtensions

 

Importing CSV data file into an iOS app via email attachment

 

Two new files will be added to our project:



NSString+ParsingExtensions.h
NSString+ParsingExtensions.m



Add the method declaration to the header file:


 

-(NSArray *) csvRows;

 

And method implementation to the .m file:

 

-(NSArray *)csvRows {
    NSMutableArray *rows = [NSMutableArray array];
    
    // Get newline character set
    NSMutableCharacterSet *newlineCharacterSet = (id)[NSMutableCharacterSet whitespaceAndNewlineCharacterSet];
    [newlineCharacterSet formIntersectionWithCharacterSet:[[NSCharacterSet whitespaceCharacterSet] invertedSet]];
    
    // Characters that are important to the parser
    NSMutableCharacterSet *importantCharactersSet = (id)[NSMutableCharacterSet characterSetWithCharactersInString:@",\""];
    [importantCharactersSet formUnionWithCharacterSet:newlineCharacterSet];
    
    // Create scanner, and scan string
    NSScanner *scanner = [NSScanner scannerWithString:self];
    [scanner setCharactersToBeSkipped:nil];
    while ( ![scanner isAtEnd] ) {        
        BOOL insideQuotes = NO;
        BOOL finishedRow = NO;
        NSMutableArray *columns = [NSMutableArray arrayWithCapacity:10];
        NSMutableString *currentColumn = [NSMutableString string];
        while ( !finishedRow ) {
            NSString *tempString;
            if ( [scanner scanUpToCharactersFromSet:importantCharactersSet intoString:&tempString] ) {
                [currentColumn appendString:tempString];
            }
            
            if ( [scanner isAtEnd] ) {
                if ( ![currentColumn isEqualToString:@""] ) [columns addObject:currentColumn];
                finishedRow = YES;
            }
            else if ( [scanner scanCharactersFromSet:newlineCharacterSet intoString:&tempString] ) {
                if ( insideQuotes ) {
                    // Add line break to column text
                    [currentColumn appendString:tempString];
                }
                else {
                    // End of row
                    if ( ![currentColumn isEqualToString:@""] ) [columns addObject:currentColumn];
                    finishedRow = YES;
                }
            }
            else if ( [scanner scanString:@"\"" intoString:NULL] ) {
                if ( insideQuotes && [scanner scanString:@"\"" intoString:NULL] ) {
                    // Replace double quotes with a single quote in the column string.
                    [currentColumn appendString:@"\""]; 
                }
                else {
                    // Start or end of a quoted string.
                    insideQuotes = !insideQuotes;
                }
            }
            else if ( [scanner scanString:@"," intoString:NULL] ) {  
                if ( insideQuotes ) {
                    [currentColumn appendString:@","];
                }
                else {
                    // This is a column separating comma
                    [columns addObject:currentColumn];
                    currentColumn = [NSMutableString string];
                    [scanner scanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:NULL];
                }
            }
        }
        if ( [columns count] > 0 ) [rows addObject:columns];
    }
    
    return rows;
}

 

As we are adding stuff let’s also add a class (President) that will hold information about our presidents. So one more time from Xcode menu select File -> New -> New File... or press ⌘ + N and from iOS Cocoa Touch select Objective-C class:

 

Importing CSV data file into an iOS app via email attachment

 

 

Importing CSV data file into an iOS app via email attachment

 

The class is very simple:

 

//  President.h

#import 

@interface President : NSObject

@property (nonatomic, retain) NSNumber 	*no;
@property (nonatomic, retain) NSString 	*name;
@property (nonatomic, retain) NSString 	*wikiurl;
@property (nonatomic, retain) NSDate 	*tookOffice;
@property (nonatomic, retain) NSDate 	*leftOffice;
@property (nonatomic, retain) NSString 	*party;
@property (nonatomic, retain) NSString 	*homeState;

@end

 

//  President.m

#import "President.h"

@implementation President

@synthesize no 		= _no;
@synthesize name 		= _name;
@synthesize wikiurl 	= _wikiurl;
@synthesize tookOffice 	= _tookOffice;
@synthesize leftOffice 	= _leftOffice;
@synthesize party 	= _party;
@synthesize homeState 	= _homeState;

@end

 

Again. If you’re not using ARC please remember about adding proper memory management.

 

Now let’s import our category into our table view controller class (TVCPresidents.h):

 

 

#import "NSString+ParsingExtensions.h"

 

And declare an array that will hold imported data. Add:

 

@property (nonatomic, retain) NSArray *importedRows;

 

Moving now to TVCPresidents.m import our new class President.h

 

#import "President.h"

 

and synthesize our array:

 

@synthesize importedRows = _importedRows;

 

We’ll also need a method to copy data from CSV arrays into our importedRows array.

 

-(NSArray *) csvArray2PresidentsArray:(NSArray *)csvArray {
    int i = 0;
    NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
    [dateFormat setDateFormat:@"yyyy-MM-dd"];
    
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];     
    
    NSMutableArray *ma = [[NSMutableArray alloc] init];
    
    for (NSArray *row in csvArray) {
        
        //1st row is a header - skip
        if (i > 0) {
            
            President *_president = [[President alloc] init];
            
           	_president.no =  [numberFormatter numberFromString:[row objectAtIndex:0]];
            _president.name = [row objectAtIndex:1];
            _president.wikiurl = [row objectAtIndex:2];
            _president.tookOffice = [dateFormat dateFromString:[row objectAtIndex:3]];
            _president.leftOffice = [dateFormat dateFromString:[row objectAtIndex:4]];
            _president.party = [row objectAtIndex:5];
            _president.homeState = [row objectAtIndex:6];
            
            [ma addObject:_president];
        }
        i++;
    }
    
    return (NSArray *) ma;
}

 

This method is also a great place to validate all data stored in CSV file. I do not do any validation here. csvArray is an array of arrays. The screenshots below shows how it is stored in memory.

 

 

Now let’s modify our handleOpenURL method:

 

-(void) handleOpenURL:(NSURL *)url {
    NSError *outError;
    NSString *fileString = [NSString stringWithContentsOfURL:url 
                                                    encoding:NSUTF8StringEncoding error:&outError];
    
    if (fileString != nil) {
        self.importedRows = [self csvArray2PresidentsArray:[fileString csvRows]];
    }

    [self.tableView reloadData];
}

 

And finally, to see results on the screen we’ll have to update two UITableView delegate methods in our table view controller:

 

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    // Return the number of rows in the section.
    return [self.importedRows count];
}

 

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
    [dateFormat setDateStyle:NSDateFormatterShortStyle];
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
    }
    
    // Configure the cell...
    President *p = (President *)[self.importedRows objectAtIndex:indexPath.row];
    cell.textLabel.text = [NSString stringWithFormat:@"%d - %@", [p.no intValue], p.name];
    cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ - %@", 
                                 [dateFormat stringFromDate:p.tookOffice], 
                                 (p.leftOffice != nil ? [dateFormat stringFromDate:p.leftOffice] : @"incumbent")
                                 ];
    return cell;
}

 

Now when we try to open a CSV attachment with our app we should see the following:

 

iOS working app

Full project code can be found on GitHub (https://github.com/m0rt1m3r/US-Presidents).

 

Hope you’ll find this tutorial useful. Let me know if you have any questions or comments.


 

Tip: If you’d like to test the import in the simulator you can add a CSV file to your project and try to simulate appropriate call inside one of the methods e.g.: viewDidLoad

 

-(void)viewDidLoad
{
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;
 
    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
    
    NSError *outError = nil;
    NSString *fullPath = [[NSBundle mainBundle] pathForResource:@"USPresidents"  ofType:@"csv"];
    NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:&outError];
    self.importedRows = [self csvArray2PresidentsArray:[fileString csvRows]];
}

 

Note: When I was testing the app on the device I run into a problem below. I’m not sure exactly what is causing it but the only solution that worked for me seems to be a complete restart of the device.

Couldn't register Damian-s.US-Presidents with the bootstrap server. Error: unknown error code.
This generally means that another instance of this process was already running or is hung in the debugger.(gdb)


References and further reading:




 1

 

Uniform Type Identifiers Overview

 

 2

 

Uniform Type Identifiers Reference

 

 3

 

UIApplication Delegate Messaging

 

 4

 

Cocoa for Scientists (Part XXVI): Parsing CSV Data

 

 5

 

Writing a parser using NSScanner (a CSV parsing example)

 

 6

 

Common Format and MIME Type for Comma-Separated Values (CSV) Files

 

 7

 

How To Import and Export App Data Via Email in your iOS App

 

wtorek, 13 grudnia 2011, m0rt1m3r

Related Posts Plugin for WordPress, Blogger...

Polecane wpisy





PowerBuilder Tetris
D - Tetris



Programowanie iOS

C# ToolBox

SQL / TSQL / PLSQL ToolBox

Linux / Unix ToolBox





Zaprzyjaznione Strony

Sprite Bandits

Cake Time