Prior to iOS 16, FileManager needed to be used to save text files to the document directory of your app. Although FileManager needs to be used for some tasks, such as getting the contents of a directory, moving, copying, deleting files, and so on, the ability to save a text file is now available in the String class.
Files are stored in the documents directory of your app, which is sandboxed. If you delete your app, then those files will be deleted with it. With this in mind, make sure you understand the implications of this, as you may need to work with FileManager to save documents in iCloud if you want to keep them safe for your user. We’ll cover this in a different tutorial.
Creating the View
The view for this tutorial will be a simple TextEditor that takes the text input and stores it in a text file. The file name will be taken from the first line of the TextEditor.
Under the text editor, we’ll have a Save Note button which can be tapped to save the note to the documents director.
struct ContentView: View {
@State private var noteText: String = ""
@State private var message: String?
var body: some View {
VStack {
TextEditor(text: $noteText)
.border(Color.gray, width: 1)
.frame(height: 200)
.padding()
Button("Save Note") {
saveNote()
}
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 8))
if let message = message {
Text(message)
.foregroundColor(.green)
.padding()
}
}
.padding()
}
We declare two @State property wrappers, one called noteText which is used to store the text of the note. The message one is used to store an info message for the user.
The body is comprised of a VStack that utilises a TextEditor that stores its contents in noteText.
That is followed by a Button that calls saveNote() when pressed.
Below that we have an if let statement that checks if the message property contains anything. If it is nil, then the body of the code is skipped. However, if message does contain a string, then it is displayed in green on the view.
We’ll now take a look at the two methods of ContentView that are as follows:
func saveNote() {
let lines = noteText.split(separator: "\n", maxSplits: 1)
guard let firstLine = lines.first, !firstLine.isEmpty else {
message = "First line is empty! Cannot save."
return
}
let fileName = "\(firstLine).txt"
let content = noteText
saveFile(fileName: fileName, content: content)
}
func saveFile(fileName: String, content: String) {
let fileURL = URL.documentsDirectory.appendingPathComponent(fileName)
do {
try content.write(to: fileURL, atomically: true, encoding: .utf8)
message = "Saved: \(fileName)"
print("File saved: \(fileURL)")
} catch {
message = "Error saving file"
print("Error saving file: \(error)")
}
}
The first method, saveNote(), is called when the button is pressed. In this code we split the text to get the first line which is to be used as the file name. Note that there is little to no error checking on the file name, so if used in production you will need to make sure the file name is valid and adjust as needed.
Next, we check if there is a first line. If not, we store a message in the message property, and because message uses @State, the view is refreshed and the message will show up in green on the view.
Next, we save the file name and grab the whole content of the message.
We then call the saveFile method which accepts a file name and content as parameters.
The saveFile method gets the documents directory and then appends the fileName onto it to give the full path. URL.documentsDirectory was added in iOS 16 (documentation) and provides a simple solution for our needs.
We create a file URL on line 14 by appending the file name onto the end of the documents directory.
Next, we use a do/try/catch to try and write the file. If all is succesful, message is changed to update the view with the filename, and the fileURL is logged to the console. If there’s a problem, then message is updated to say there is an error and the error message is logged to the console. Note that “content” is a String which has the writeTo method that allows us to save the file.
The file is written with this line:
try content.write(to: fileURL, atomically: true, encoding: .utf8)
content is a string that we want to write to a file. We tell it where to write to by passing in the fileURL created earlier. We specify the encoding will be utf8.
Atomically being set to true means that the file is written to a temporary file first, and then copied over to replace a file that is there. For this example it is fine to use true or false here. If you have an app that deals with lots of writes, such as log files, then you might consider setting it to false for performance reasons. This is mostly outside of the scope of this tutorial.
Closing
In finishing, this tutorial is rather simple. The String class makes it simple to work with saving a file in a single call. If you are wanting to save strings as files, then this is the way to go. However, if you want to read the contents of a directory or make modifications, then you will need to use the FileManager class which has more comprehensive ways to work with the file system. Also, as noted near the beginning of this tutorial, if you want to make sure files are safe after deletion, then you will need to consider FileManager and enable iCloud, which will be covered soon.
Leave a Reply
You must be logged in to post a comment.