Reading and Writing Text and CSV files in Swift

The power behind all computing is data. We collect data, process data, present data  and most importantly store data. Without storage, the others are meaningless. In a live app, we can store our data in collection types such as dictionaries and arrays.  But that only works when the app  is working. When the app closes, the data disappears. We have several ways of storing that data for the time the app stops. For small amounts of data we have NSUserdefaults.  For larger sets of data this doesn’t work.

One of the oldest, and simplest ways of  storing persistent data is a text file. This is a data file stored on disk or persistent memory of characters. Sometimes it is a single string, sometimes a series of strings.  In this lesson, we’ll look at how to store data in a text file, and how to use a text file to store with the most oldest and a still often used  methods: the comma separated value, or CSV file

Set Up The Project

Create a new project named TextFileCSVDemo Using Swift as a the language and a Universal device.

We’ll do this project in the simulator using a text view. This will allow for scrolling through largest data sets. To make it easy to use the simulator and not get messy with keyboards, we’ll tell the simulator to use  the external keyboard and not display the internal keyboard.

Go to the storyboard and drag four buttons and a the text view on the storyboard.  Roughly place them like this:

2016-05-22_20-40-28

Title the buttons. Reset, Load,Save, and Report. Select all four buttons and click the stack view button stack view button on the lower right of the storyboard. The buttons will group into a stack view.  Set the attribute for the stack view like this:

2016-05-22_20-45-42

Click the auto layout pin button pinMenuButton.  Click off Constrain to Margins.  Set the margins for the top to 0 point, the left to 0 points and the right to 0 points.  Click on the  height. Set to 50 points. Be sure to tab out of the text fields. Set the Update frames to Items of new constraints like this:

2016-05-22_20-46-10

Add the four constraints. Select the text view.  Set the attributes like this:

2016-05-22_20-50-08

Click the auto layout pin button pinMenuButton. Click off Constrain to margins. Set the margins for the top to 10 points, the left to 0 points, the right to 0 points and the bottom for 20 points.  Set the Update frames to Items of new constraints like this:

2016-05-22_20-52-39

 

Add the constraints. This should give you a layout like this:

2016-05-22_20-54-33

Open the assistant editor.  Close whatever panes you need to give you some space. Control drag the text view to the code and make an outlet named textView. Control drag from the Report button to make an action called reportData. Control drag from the Reset button to make an action called resetData. Control drag from the Read button to make an action called readData. Control drag from the Write button to make an action called writeData. You should now have the following outlets and actions in your code.

@IBOutlet weak var textView: UITextView!
@IBAction func reportData(sender: UIButton) {
}
@IBAction func resetData(sender: UIButton) {
}
@IBAction func readData(sender: UIButton) {
}
@IBAction func writeData(sender: UIButton) {
}

Reading a Data File

Let’s learn how to read data first. We’ll need a file to read.
Close the assistant editor. Make a new file by pressing Command-N. Select an Other file that is blank. Call this file data.txt, saving the file in the TextFileCSVDemo group.
Add the following to the file:

Title,data1,data2
Hello Pizza!,24,42
Ice Cream,16,32

This data has no meaning whatsoever, but we’ll use it as our first set of test data. Go to the viewController class.

The First Rule of Data Files

Add the following function

func readDataFromFile(file:String) -> String!{

} 

Our function will return an optional value of String. The first rule of reading external files is to never trust them. They may be wrong, they may not exist. If anything goes wrong, we’ll return nil so the code calling this function knows to act accordingly.

if…let and guard…let

The never trust a data file rule means we need to make a short detour into error handling. Our first stop is detecting a nil value. The basic way is this

var a:String? = nil
if a != nil {
   let b = a!
   print b
} else { 
   print ("That was nil")
}

However this has a few very subtle problems. Without going into too much detail,  the variable a may be too weak to withstand ARC in certain situations, (see here for the classic example) and gets cleaned up before it can pass its value to b.

A better approach is to use optional chaining which assigns the value of b. This tends to make a stronger value, one that keeps around in those same situations. There are two version of this assignment. One uses the if..let clause

if let b = a {
   print(b)
} else {
   print ("That was nil")
}

if A is nil the if clause is false and we do the else clause. If true, we execute the statements with b The Master-Detail template uses this method to avoid nil popping up unexpectedly.

There is a slight problem with this. The identifier b only exists within the scope of the if clause. When you have several values to unwrap this can become a tangle of code. The latest way to unwrap a value is guard. You use it like this:

guard let b = a else{
    print ("That was nil")
    break
}
print(b)

With guard, we declare the constant  like any constant, open to the scope it is declared in. However our else clause will break execution so will need a break, return or continue

For the Big Errors do-catch

We will encounter big errors, ones that if left alone we can’t recover from. Not finding a file would be an example. For those types of errors, we have do...try...catch. Some functions have built-in error systems, using the keyword throws and throw to indicate errors. When we encounter a function that uses throws, we need to use the do-catch clause. Suppose we have a function readMe which reads a file

func readme() throws -> String{
// stuff in here including a error type
}

When we use this function, it needs a syntax like this

do {
   let myData =  try readme()
} catch {
   //handle my error
}
//now use data

Since throws associates itself with an error type, you can catch specific errors and handle them differently. A file not found could be handled differently than a loss of connection for example. If you don’t care what the error is, you can also use try? as a quicker method. The try? casts the result to an optional where nil means there was an error, and you get a value otherwise.

let myData =  try? readme()
if goodData == nil {
    //error handling here
}

Getting a File Path from the Bundle

With that background, we are ready to look at our demo code again.

func readDataFromFile(file:String)-> String!{
        guard let filepath = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
            else {
                return nil
        }
}

We use guard on the method pathForResource(File:type:) This method finds a file with a specific extension and returns its full path. It returns nil if the file is not found.

We need to explain bundles a bit here. Bundles are groupings of files the system keeps track of. There is always one bundle mainBundle where your app resides, and many of its resources such as the images in Assets.xcassets. You can make more bundles if you wish, but for our purposes it’s best to keep everything in the main bundle. The code NSBundle.mainBundle() gets the main bundle, and then we use the pathForResource method to search for our files.

With a file name, we can read the data in that file.

func readDataFromFile(file:String)-> String!{
    guard let filepath = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
        else {
           return nil
    }
    do {
       let contents = try String(contentsOfFile: filepath, usedEncoding: nil)
       return contents
    } catch {
        print("File Read Error for file \(filePath)")
        return nil
    }
}

The String initializer can read a file into a text string. However it does throw errors, so we use do-catch to handle the errors. When I get an error, I print to the console an error message, and return nil. Otherwise, we return the contents of the file as a string.

Add this function to the readData action.

@IBAction func readData(sender: UIButton) {
    textView.text = readDataFromFile("data")       
}

Since the text property is optional, we don’t need to do a lot here.

Running and Configuring the Simulator

Build and run. You get a screen like this:

2016-05-18_06-51-46

Click on the text and the keyboard will most likely show up.

2016-05-18_06-52-17

Rotate the device with Command-Left Arrow. You’ll find the keyboard obscures way too much of our text.

2016-05-18_06-52-35

In the menu for the Simulator ,  click Hardware>Keyboard. Make sure Uses the Same Layout as OS X and Connect Hardware Keyboard are checked.

2016-05-18_06-53-27

You’ll notice the third choice here is Toggle Software Keyboard.  You’ll also notice the shortcut is Command-K.  Click it and the keyboard disappears. (note: I changed the background to #EEEEEE for visibility in the post, yours should be white)

2016-05-18_07-13-33

For these lessons formatting for the keyboard is a pain, but we will be needing to type into the text box. We’ve set up the simulator to use our hardware keyboard and not the software keyboard.  You can always get the keyboard back with a Command-K.  You can also hide it with the keyboard button, if it is visible.

2016-05-18_07-00-30

Now that we’ve set up our simulator, tap the Read button. We read the text file and get our data file.

2016-05-18_07-15-24

Writing Data

Writing data is slightly more complicated than reading data. The complexity is in the path name. Instead of just checking it exists and returning nil if it doesn’t, we need to generate the path if it does not exist.

We have the same problem with our data. if textView.text is nil,  we cant save anything.

Add this code to ViewController

func writeDataToFile(file:String)-> Bool{
    // check our data exists
    guard let data = textView.text else {return false}
    //get the file path for the file in the bundle
    // if it doesn't exist, make it in the bundle
    var fileName = file + ".txt"
    if let filePath = NSBundle.mainBundle().pathForResource(file, ofType: "txt"){
        fileName = filePath
    } else {
        fileName = NSBundle.mainBundle().bundlePath + fileName
    }
}

Just for a variation, I returned a Bool from this function. I could have used an optional or a string or thrown an error here. This is one of those choices you as a programmer need to make, based on what you know about the rest of the code using this function. If this is going in code already existing, it needs to match the error handling of that code. While you’ll hear “there’s this right way” or “that right way”  to do something,  that’s pretty much bunk. Here’s rule two of Data work: Whatever you do make sure it fits the context of the other APIs and code you are using. This is also why I’m not a big fan of 3rd party libraries for data work. You’ll invariably need so much customization you might as well write it yourself for your specific case. Since this is a tutorial I’m arbitrary in my choices, but do some homework before you start coding a real app.

Line 3 checks my data exists. If I don’t have any, I return false and leave the function. Line 6 makes a filename by appending an extension .txt to my file name. I then get the filePath for that file like I did in the readDataFromFile method. If there I assign it to fileName. If not I get the path of the bundle and append the filename to it.

We now have a file path for our file and we know we have data. Next we write the file with a string function writeTofile. Change the method to this:

func writeDataToFile(file:String)-> Bool{
 // check our data exists
    guard let data = textView.text else {return false}
    print(data)
    //get the file path for the file in the bundle
    // if it doesnt exisit, make it in the bundle
    var fileName = file + ".txt"
    if let filePath = NSBundle.mainBundle().pathForResource(file, ofType: "txt"){
        fileName = filePath
    } else {
       fileName = NSBundle.mainBundle().bundlePath + fileName
    }
    //write the file, return true if it works, false otherwise.
    do{
       try data.writeToFile(fileName, atomically: true, encoding: NSUTF8StringEncoding )
        return true
    } catch{
        return false
    }
}

writeToFile throws errors, so we must use do-catch. We return false if a problem occurs and true if everything is successful.

This method has three parameters we should talk about. The fileName we’ve already covered, and we know how to get the path.  atomically writes to a temporary copy of the data then replaces the original with the new copy. This is a safety measure. Writing data to storage can take time, and if something were to happen to interrupt that process, you don’t corrupt your data that way. In a big file it can mean a huge use of resources, and might be false if storage is an issue. I tend to go for the safe instead of sorry school here, and keep it true unless something tells me I’m going to have trouble. In files that big, I might look at other alternatives for saving files.

The next parameter, encoding is the code set for the string characters. The simple answer is that different data uses different ways to code strings. For the longer answer I suggest this explanation from Joel On Software. The point here is another application of rule two: make sure you know the context this code needs to be. I used a pretty standard one here in UTF8, but that might not always be the case.

Change the action writeData to this:

    @IBAction func writeData(sender: UIButton) {
        if writeDataToFile("data") {
            print("data written")
        } else {
            print("data not written")
        }
    }

Data writes are invisible to the user, and often the developer. It’s important to give feedback. I used print to send the message to the console. You might use an alert or a simple icon for success and an alert for failure, but some feedback is necessary.

I’ll need a quick way of clearing the text on textView to prove I actually saved and loaded something. Change the resetData action to this:

@IBAction func resetData(sender: UIButton) {
        textView.text = "Nope, no Pizza here"
    }

Build and run. We get our Lorem Ipsum text.

2016-05-18_07-13-33

Tap the write button, you’ll see the console will tell you

Data Written

Tap the Reset button.

2016-05-22_21-20-42

Tap the Read button. Our text loads

2016-05-22_21-20-51

 

Introducing CSV

While that’s the basics of text files, we don’t often have just text in a text file. Instead, we use a variety of arrangements to store other types of data than text. One of the oldest of these methods is the CSV, or comma separated value file. We’ve already made one In Xcode. Look at the data.txt file.

Title,data1,data2
Hello Pizza!,24,42
Ice Cream,16,32

This is a CSV file in all but extension, .CSV.  As the name implies, we separated out three values per line by a comma. The CSV file represents this table:

Title data1 data2
Hello Pizza! 24 42
Ice Cream 16 32

There is no real standard for CSV that everyone follows. You’ll find several variations. There are a few rules that tend to be true. A row of data usually ends with a newline character \n. The elements, which I’ll often call fields of the row have some character to delimit them. The most popular is the comma, but tab characters and the pipe character | can get used. Once again, it’s important to know what you are dealing with. Notice the first line of our CSV file has a description of the column of data. That is helpful, and you’ll find it often in data sets, but it isn’t always there. When writing my own, I tend to include it. Sometimes its the only documentation about this file.

A string file is a beautiful thing, but a workable data structure is better. There’s many different data structures we can use. Usually the data will be some sort of array. You can have individual rows associated with structs, classes, or dictionaries. In some cases it makes sense to use an array for each field. I’ll use dictionaries in this example.

In the declaration part of the code add this:

var  data:[[String:String]] = []
var  columnTitles:[String] = []

For our first iteration, I’ll leave everything a string. I made a second array columnTitles to keep track of the keys we’ll assign in the dictionary.

Cleaning the Rows

Before  I do anything else, there some preventative cleaning I have to do. Suppose our data looks like this:

Title,data1,data2
Hello Pizza!,24,42

Ice Cream,16,32

To the system it appears like this:

Title,data1,data2\n
Hello Pizza!,24,42\n
\n
Ice Cream,16,32\n

The \n is the newline character. There’s also a return character \r that sometimes shows up. We’d like to get rid of the blank rows and change all the \r to \n. This will make reading and converting our data file a lot easier.

Add the following function to ViewController:

func cleanRows(file:String)->String{
        var cleanFile = file
        cleanFile = cleanFile.stringByReplacingOccurrencesOfString("\r", withString: "\n")
        cleanFile = cleanFile.stringByReplacingOccurrencesOfString("\n\n", withString: "\n")
        return cleanFile
    }
    

There is wonderfully handy string method stringByReplacingOccurrencesOfString:withString: that lets us replace the \r in line 3. Line 4 remove any double returns \n\n in our file with \n. This is one way to get rid of blank lines. We’ll come to the other one shortly.

We’ll start our CSV conversion to a data structure by doing this cleaning

func convertCSV(file:String){
        let rows = cleanRows(file)
}

Why do we want only /n? So we can use it as a separation character with another handy string method componentsSeparatedByString. Change the code to this:

func convertCSV(file:String){
        let rows = cleanRows(file).componentsSeparatedByString("\n")
}

The function componentsSeparatedByString returns an array of strings broken up for a single string. It uses the parameter as the string that breaks up the other strings, in this case a newline \n. We assign that to the constant rows. We’ll check that the file has real data, and if not print an error message. If we do have real data,  we’ll take the first row of titles and make an array to use for keys in our dictionary. In the next lesson, we’ll be building on this for reading the fields in a row. For now add the following function to break the row strings into string arrays.

func getStringFieldsForRow(row:String, delimiter:String)-> [String]{
        return row.componentsSeparatedByString(delimiter)
    }

We’ll use two nested loops to populate the data we’ve now broken apart. Change the method to this:

func convertCSV(file:String){
        let rows = cleanRows(file).componentsSeparatedByString("\n")
        if rows.count > 0 {
            data = []
            columnTitles = getStringFieldsForRow(rows.first!,delimiter:",")
            for row in rows{
                let fields = getStringFieldsForRow(row,delimiter: ",")
                if fields.count != columnTitles.count {continue}
                var dataRow = [String:String]()
                for (index,field) in fields.enumerate(){
                    let fieldName = columnTitles[index]
                    dataRow[fieldName] = field
                }
                data += [dataRow]
            }
        } else {
            print("No data in file")
        }
    }

Line 3 checks if the input string has any rows. If we don’t, we send a message that there is no rows and exit. If we do, we clear the property data in line 4. Line 5 sets the property columnTitles with the contents of the first row. We’ll use the field names in this row as the keys of our dictionary entries. Line 6 sets is a loop through rows. In that loop, we break a row into an array of fields. We check if we have the right number of fields. If not, we skip this row. You could do more than that, and in our next lesson we’ll look at a variation of CSV that will handle this in a more robust manner. We create a dictionary to place the keys and values in, then run another loop over the fields. You may not be familiar with this loop structure using enumerate(). Instead of a single value, we have an index of the value and is value stored in a tuple This loop uses the index to find a key for our dictionary, and add the value to the dictionary in line 14 . When we finish  the second loop, we add the dictionary to the array data,finishing a row, then looping back for the next row.

We’ll need a way to output our data, to prove we actually got it. Add the following method.

func printData(){
    convertCSV(textView.text)
    var tableString = ""
    var rowString = ""
    print("data: \(data)")
    for row in data{
        rowString = ""
        for fieldName in columnTitles{
            guard let field = row[fieldName] else{
                print("field not found: \(fieldName)")
                continue
            }
            rowString += String(format:"%@     ",field)
        }
        tableString += rowString + "\n"
    }
    textView.text = tableString
} 

This does pretty much the same as the  conversion method, except in reverse. It takes the data and makes it a text file again, using two loops. It takes whatever is in the text view and makes it a CSV file, then reports the results.

Change reportData to this:

@IBAction func reportData(sender: UIButton) {
                printData()
        
    }

Build and run. Tap Read to read in our data.

2016-05-18_07-15-24

Tap Report and our badly formatted report shows.

2016-05-23_06-00-05

There’s a few more tricks we can do with CSV to get data in. In our next lesson we’ll learn some more about CSV. We’ll look at dealing with strings that have commas, and  using data types in our data, and formatting that chart a lot better.

The Whole Code

//
//  ViewController.swift
//  TextFileCSVDemo
//
//  Created by Steven Lipton on 5/17/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

class ViewController: UIViewController {
    //MARK: Outlets and properties
    var  data:[[String:String]] = []
    var  columnTitles:[String] = []
    
    @IBOutlet weak var textView: UITextView!
    
    @IBAction func reportData(sender: UIButton) {
        printData()
    }
    @IBAction func resetData(sender: UIButton) {
        textView.text = "Nope, no Pizza here"
    }
    @IBAction func readData(sender: UIButton) {
         textView.text = readDataFromFile("data")
    }
    @IBAction func writeData(sender: UIButton) {
        if writeDataToFile("data") {
            print("data written")
        } else {
            print("data not written")
        }
    }
    //MARK: - Instance methods
    
    //MARK: CSV Functions
    func cleanRows(file:String)->String{
        //use a uniform \n for end of lines.
        var cleanFile = file
        cleanFile = cleanFile.stringByReplacingOccurrencesOfString("\r", withString: "\n")
        cleanFile = cleanFile.stringByReplacingOccurrencesOfString("\n\n", withString: "\n")
        return cleanFile
    }
    
    func getStringFieldsForRow(row:String, delimiter:String)-> [String]{
        return row.componentsSeparatedByString(delimiter)
    }
 
    func convertCSV(file:String){
        let rows = cleanRows(file).componentsSeparatedByString("\n")
        if rows.count > 0 {
            data = []
            columnTitles = getStringFieldsForRow(rows.first!,delimiter:",")
            for row in rows{
                let fields = getStringFieldsForRow(row,delimiter: ",")
                if fields.count != columnTitles.count {continue}
                var dataRow = [String:String]()
                for (index,field) in fields.enumerate(){
                    dataRow[columnTitles[index]] = field
                }
                data += [dataRow]
            }
        } else {
            print("No data in file")
        }
    }
    
    func printData(){
        convertCSV(textView.text)
        var tableString = ""
        var rowString = ""
        print("data: \(data)")
        for row in data{
            rowString = ""
            for fieldName in columnTitles{
                guard let field = row[fieldName] else{
                    print("field not found: \(fieldName)")
                    continue
                }
                rowString += field + "\t"
            }
            tableString += rowString + "\n"
        }
        textView.text = tableString
    }
    
    //MARK: Data reading and writing functions
    func writeDataToFile(file:String)-> Bool{
        // check our data exists
        guard let data = textView.text else {return false}
        print(data)
        //get the file path for the file in the bundle
        // if it doesnt exist, make it in the bundle
        var fileName = file + ".txt"
        if let filePath = NSBundle.mainBundle().pathForResource(file, ofType: "txt"){
            fileName = filePath
        } else {
            fileName = NSBundle.mainBundle().bundlePath + fileName
        }
        
        //write the file, return true if it works, false otherwise.
        do{
            try data.writeToFile(fileName, atomically: true, encoding: NSUTF8StringEncoding )
            return true
        } catch{
            return false
        }
    }
    
    func readDataFromFile(file:String)-> String!{
        guard let filepath = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
            else {
                return nil
        }
        do {
            let contents = try String(contentsOfFile: filepath, usedEncoding: nil)
            return contents
        } catch {
            print ("File Read Error")
            return nil
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //textView.text = readDataFromFile("data")
    }


}


15 thoughts on “Reading and Writing Text and CSV files in Swift”

  1. Hi Steven,

    Great tutorial!

    How do you deal with multiple csv files in a bundle particularly of unknown filename? Is there a generic way of loading these files without naming them in the pathForResource line?

    Thanks,

    Jeremy

  2. I’m curious to know if you have code that opens a text file and allows one line at a time to be read, rather loading the entire file into one big string? Today I figured out how to associate an external file with “stdin” and use readLine(), but not sure how to deal with files that have \r line endings instead of \n.

      1. Thanks! Seems like a fundamental thing a programming language should provide… I miss the days of ThinkPascal, alas. :)

  3. With the upgrade of Xcode to Version 8.0 (8A218a), the statement:

    do {
    let contents = try String(contentsOfFile: filepath, usedEncoding: nil)
    return contents
    }

    reports that “nil” cannot be used as it is seen as in immutable value of ‘_’.

    The error shows as:
    cannot pass immutable value of type ‘_’ as inout argument

    Not sure how to handle this one. Any ideas?

    1. Apparently Apple made the encoding literal when I wasn’t looking. Since we’re using simple data in the example anything should work. Try changing nil to String.Encoding.macOSRoman or String.Encoding.ascii. Type String.Encoding. to see the other options or you can check the list of encodings here

  4. I have a rather large txt file that contains information similar to the information presented below albeit in a more simplistic manner. The columns in the file are separated by tabbed white space, though the distance between the column varies, the character count on each column for the same category is the same.

    I have been able to add all of the items on this list into an array using separatedByCharacter “ “ and then filtering out all of the array items that contain nil, but I don’t know how to catalogue, reference, group, combine, etc. the data in order to make it useful. As you can see, all of the items are related in one way or another. The first column represents the category each item, and each has several attributes that relates them to items in other categories. Fruits and vegetables have a color, size, quantity, and weight per unit attributes; these will be placed in matching color boxes that can carry different amounts of items based on the size and weight of the items. A person will then be assigned to carry a box based on their weightlifting capabilities.

    Eventually, I’d like to be able to query the data based on different attributes. For example. Who carried the most weight? How many trips were done per person, or per box color? Etc.

    FRUIT WATER GRE LRG 0003 050
    FRUIT     BANAN     YEL    MED 0017 010
    FRUIT     STRAW    RED   SML 0005 005
    FRUIT LEMON YEL SML 0024 005
    VEGIE REDPE RED MED 0008 001
    VEGIE GRENP GRE MED 0009 001
    BOX RED 006 012 018
    BOX YEL 010 020 030
    BOX GRE 003 006 009
    PERSON JOHN TALL STRG
    PERSON JIMM MED WEAK
    PERSON DAVD MED STRG

    1. I was going to answer that question abot nine months ago, except I got a really good opportunity that took a lot of time away. I will, if plans go right, in the next few months be writing about SQLite and Core data which are your two best options to manage that data.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s