Corona Reference: Read/Write to Files

Most apps will benefit from the ability to read and write data from various sources, such as a JSON, XML, or SQLite database, or simply a text file. Explaining just one form of these databases is an entire course on its own, so we will avoid any real depth here. We'll mostly use a plain text file for our examples, but if you have experience with these other formats, the methods are similar.

You may want to read or write to a file when saving or retrieving high scores, saving preferences and settings, composing email or texts within your app, or loading data from a remote social media server such as Twitter or Facebook. Some of these are more complicated examples than we'll try, but the same principles could be applied to any app.

Directories

When saving information, you need to create a file to write the information to. Unlike typical operating systems, you won't have much of a choice in where to put this file. There are four choices for iOS, and three for Android:
  • system.DocumentsDirectory This is the usual choice for saving data to a file that is generated by the app itself - such as a high score table - and is available for iOS or Android. Any files that exist in this directory will remain until the app is deleted from the device, and its contents will not be altered unless directed to (a high score or preferred setting, for instance, will remain when the app is used again, even if it is closed). iOS devices will back up any files in this directory unless you prevent it.
  • system.TemporaryDirectory A file here can also be accessed and altered by the app itself, but any changes to it will be lost when the app is closed. Any information you want saved for the app's next use should NOT be kept here, but this is a good choice for any data that you only need to save for a single session. This directory is available for both platforms.
  • system.CachesDirectory Files here are kept longer than those in the temporary directory, but are not backed up and can be lost.
  • system.ResourceDirectory Files in this directory are in the core project directory (along with your main.lua file). While this is the default file path, it is read-only: data cannot be saved to a file in this directory. If you have tables of data that would take a long time to generate, but will never be changed, this is a good location for your file. For instance, a word find game might have a dictionary of allowable words kept here. Android devices do not have this directory, however.

Opening Files

Before you can add to or read from a file, you'll need to provide a path to the file. Start by giving a name to the file path itself:
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
Next, use the io.open function to open the file. You'll need two arguments: the name of the path you just defined, and a mode to open the file in. The mode is given as a string, and will be one of the following options:
  • "r" Read mode. This is the default mode, if one is not specified. The file will be read from the beginning.
  • "w" Write mode. Any data in the file WILL BE OVERWRITTEN. If the file did not yet exist, it will be created.
  • "a" Append mode. Any data in the file will be preserved, and new data will be added to the end of the file. If the file did not yet exist, it will be created.
  • "r+" Update mode. This is a read/write mode that preserves any data in the file. New data will be added to the beginning of the file.
  • "w+" Update mode. This is a read/write mode that does NOT preserve data. If the file did not yet exist, it will be created.
  • "a+" Append mode. Any data in the file will be preserved, and new data may be added to the end of the file. If the file did not yet exist, it will be created.

Once our path is named, we can open the file and give it a variable name for later use. If the file did not yet exist, and we chose a mode and directory that allows it, a new file will be created.
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local file = io.open( myPath, "a+" )

Writing to Files

Once you've specified a path for your file, and opened it with a write-enabled mode, you can add data to it (note that in some modes, existing data will be overwritten). This is done using the write method.

Using the example from above, we'll add some strings to our file:
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "a+" )

local firstString = "Here's the first thing in our file"
local secondString = "And here's another"

myFile:write( firstString )
myFile:write( secondString )
Note that even though these strings were added to the file separately, they will not automatically be placed into one line. If we were to read the file right now, it would have only one line: "Here's the first thing in our fileAnd here's another". To prevent this, and make sure each string is placed on its own line (so we can read in one line at a time, as we'll see next), place a line break symbol at the end of each string. This will not appear in the file itself, it is just used as a "return" character.
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "a+" )

local firstString = "Here's the first thing in our file\n"
local secondString = "And here's another\n"

myFile:write( firstString )
myFile:write( secondString )
Most importantly, once we're done with our file, we need to close it. Leaving a file open after being accessed can cause data to be lost or altered unintentionally. To close the file, use the io.close function, and set the variable name of the file equal to nil:
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "a+" )

local firstString = "Here's the first thing in our file\n"
local secondString = "And here's another\n"

myFile:write( firstString )
myFile:write( secondString )

io.close( myFile )
myFile = nil

Reading Files

Assuming the path you've chosen contains a (non-empty) file, you can extract data from it using the read method. This method takes an argument that specifies how the data should be read. This argument is a string that can be one of the following:
  • "*l" Read line mode. This is the default mode, and will read the next line of the file, starting with the first. When no lines of the file remain, nil will be returned.
  • "*a" Read all mode. This will cause the entire file to be read, starting from the current position. If the file has not yet been read using "*l" mode or "*a" mode, the file will be read from the beginning.
  • "*n" Read number mode. This will cause the entire file to be read, starting from the current position, but any strings will be returned as numbers. This would be appropriate for a score table, for example.

Continuing the example from above, we'll read back the strings from our file one at a time:
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "r" )

local firstFileEntry = myFile:read( "*l" )
local secondFileEntry = myFile:read( "*l" )
The value of firstFileEntry, after running this code, would be the string "Here's the first thing in our file", since that was the first line we added before. The value of secondFileEntry would be "And here's another".

If we want to read in the whole file at once, we can instead use the "*a" mode:
local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "r" )

local myFileContents = myFile:read( "*a" )

print( myFileContents )
would print the contents of the entire file to the terminal, with line breaks preserved.

In either case, but sure to close the file again when you are finished reading in your data.

Reading Lines

    We also have the option of reading in each line of a file at once. This is useful when reading in a catalog, dictionary, score table, or any other list of data arranged in lines, where we need to access all of the data in the file. Begin by specifying the path and giving a name to the file, opening it with a read-enabled mode. A for loop can then be used to access each line of the file. In the sample below, we'll print each line to the terminal, but your for loop can be adjusted for other purposes.
    local myPath = system.pathForFile( "myFile.txt", system.DocumentsDirectory )
    local myFile = io.open( myPath, "r" )
    
    for i in myFile:lines() do
       print( i )
    end
    
    io.close( myFile )
    myFile = nil
    Note that we still need to close the file when we're done.

The Project Sandbox

To see the files you've created in your documents directory, open your app in the simulator. Under "File" in the Simulator menu, you'll see a "Show Project Sandbox" option. Choosing this will open a file navigator on your computer. Look for the location of your main.lua file (for instance, under Desktop or the name of your app folder). If you look in the Documents subfolder, you should see any text files created by your app. The mobile device sandbox file system helps prevent apps from interfering with one another and keeps malicious files from infecting devices.

Example: Word Find

We'll create a scene with a grid of letters. The user can select a chain of neighboring letters to build a word, and they'll receive a point each time they find a word in our dictionary. Unless you're using a very small collection of valid words, you'll likely want to create the dictionary in a separate file that you can read in when the app opens. The dictionary file we'll use for the example is below; to follow the example, save the file (without changing its name) to the Documents folder in your app sandbox (see above):


The code below creates a grid of letters. You can tap on sequences of neighboring letters to build a word. Tap on a letter that's already been selected to end the word, and check to see if the word you've created is in the dictionary. If so, you'll get a point. The words you create are saved in another text file, also in the Documents Directory.
local myPath = system.pathForFile( "wordList.txt", system.DocumentsDirectory )
local myFile = io.open( myPath, "r" )

local dictionary = {}

for l in myFile:lines() do
    table.insert(dictionary, l)
end

io.close( myFile )
myFile = nil

local function checkWord( possibleword )
    for _, w in ipairs(dictionary) do
        if w == possibleword then
            return true
        end
    end
    return false
end

print(dictionary[2])



local alphabetTable = { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" }

local w = display.contentWidth
local h = display.contentHeight

local letterSpace = w/31
local letterTable = {}

local currentWord = ""
local score = 0

local currentWordDisplay = display.newText( currentWord, w/2, letterSpace*31 + 100, Arial, 80 )
local scoreDisplay = display.newText( "Score: "..score, w/2, h - 30, Arial, 40 )

local lastRow = 0
local lastColumn = 0

local function addLetter( event )
    if event.target.selected == false then
        if currentWord == "" then
            currentWord = currentWord..event.target.text
            currentWordDisplay.text = currentWord
            event.target.selected = true
            event.target:setFillColor( 0, 1, 0 )
            transition.to( event.target, { xScale = 1.8, yScale = 1.8, time = 100 })
            lastRow = event.target.id[1]
            lastColumn = event.target.id[2]
        elseif event.target.id[1] == lastRow + 1 or event.target.id[1] == lastRow or event.target.id[1] == lastRow - 1 then
            if event.target.id[2] == lastColumn + 1 or event.target.id[2] == lastColumn or event.target.id[2] == lastColumn - 1 then
                currentWord = currentWord..event.target.text
                currentWordDisplay.text = currentWord
                event.target.selected = true
                event.target:setFillColor( 0, 1, 0 )
                transition.to( event.target, { xScale = 1.8, yScale = 1.8, time = 100 })
                lastRow = event.target.id[1]
                lastColumn = event.target.id[2]
            end
        end
    else
        if checkWord( currentWord ) == true then 
            currentWordDisplay.text = currentWordDisplay.text.." is a good word!"
            score = score + 1
            scoreDisplay.text = "Score: "..score
            print( currentWord.." is a word, score increased" )
            local myPath = system.pathForFile( "usedWords.txt", system.DocumentsDirectory )
            local myFile = io.open( myPath, "a" )
            myFile:write( currentWord.."\n" )
            io.close( myFile )
            myFile = nil
        else
            currentWordDisplay.text = "Sorry, "..currentWordDisplay.text.." is not a word"
            print( currentWord.." is not a word" )
        end
        currentWord = ""
        for i = 1, 30*30 do
            letterTable[i].selected = false
            letterTable[i]:setFillColor( 1, 1, 1 )
            transition.to( letterTable[i], { xScale = 1, yScale = 1, time = 100 })
        end
    end
end


for i = 1, 30 do
	for j = 1, 30 do
		local newLetter = display.newText( alphabetTable[math.random(1,26)], letterSpace*j, letterSpace*i, Arial, 30 )
		newLetter.id = { i, j }	
		newLetter.selected = false	
		newLetter:addEventListener( "tap", addLetter )
		table.insert( letterTable, newLetter )
	end
end