Reputation: 19722
I already have read Read and write data from text file
I need to append the data (a string) to the end of my text file.
One obvious way to do it is to read the file from disk and append the string to the end of it and write it back, but it is not efficient, especially if you are dealing with large files and doing in often.
So the question is "How to append string to the end of a text file, without reading the file and writing the whole thing back"?
so far I have:
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
var err:NSError?
// until we find a way to append stuff to files
if let current_content_of_file = NSString(contentsOfURL: fileurl, encoding: NSUTF8StringEncoding, error: &err) {
"\(current_content_of_file)\n\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}else {
"\(NSDate()) -> \(object)".writeToURL(fileurl, atomically: true, encoding: NSUTF8StringEncoding, error: &err)
}
if err != nil{
println("CANNOT LOG: \(err)")
}
Upvotes: 68
Views: 62788
Reputation: 8143
This one is short and simple:
let data = Data(...)
let path = URL.documentsDirectory.appending(path: "my_file.txt")
do {
let filehandle = try FileHandle(forWritingTo: path)
filehandle.seekToEndOfFile()
try filehandle.write(contentsOf: data)
} catch CocoaError.fileNoSuchFile {
// create if not exists
try data.write(to: path)
}
Upvotes: 0
Reputation: 2722
I use a class where the dateformatter and file-url is only created once and you can use it from anywhere with a single call.
Here's the code for the Logger.swift file:
class Logger {
private static let shared = Logger()
private var fileURL:URL?
private var dateFormatter:DateFormatter?
private init() {
print("Logger -> Init")
//DateFormatter for timestamp
self.dateFormatter = DateFormatter()
self.dateFormatter?.dateFormat = "HH:mm:ss"
//FileURL for log
let fileName = "APPNAME Log - \(Date()).log"
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("Logger -> Documents directory could not be accessed")
return
}
self.fileURL = documentsDirectory.appendingPathComponent(fileName)
print("Logger -> Final file url path: \(String(describing: self.fileURL?.path))")
}
static func log(_ message: String) {
let logger = Logger.shared
guard let fileURL = logger.fileURL,
let dateFormatter = logger.dateFormatter else {
print("Logger -> FileURL/Dateformatter not accessible")
return
}
let timestamp = dateFormatter.string(from: Date())
let stringToWrite = timestamp + ": " + message + "\n"
guard let data = stringToWrite.data(using: String.Encoding.utf8) else {
print("Logger -> Failed to create data from string")
return
}
//Check for existing file
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
let fileHandle = try FileHandle(forWritingTo: fileURL)
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
catch {
print("Logger -> Error creating filehandle: \(error)")
}
return
}
//Create a new file
do {
try data.write(to: fileURL, options: .atomicWrite)
}
catch {
print("Logger -> Error writing to file: \(error)")
}
}
}
And whenever you want to use it, you can simply do something like this:
Logger.log("Some string I want to save in the file!")
As a bonus, here is a quick function that I simply put inside a swift file (not a class, just a swift file where you put this code in at ground level), and it automatically puts the filename and function in the string as well (and prints to the console too)
func DLog(_ message: String, filename: String = #file, function: String = #function, line: Int = #line) {
let string = "[\((filename as NSString).lastPathComponent):\(line)] \(function) - \(message)"
print("\(string)") // Print to DebugConsole
//Write same string to file
Logger.log(string)
}
You can use this DLog function from anywhere like this:
DLog("String to print AND write to a file")
Upvotes: 2
Reputation: 3973
Here is an update of PointZeroTwo's answer, written for Swift 4.1:
extension String {
func appendLine(to url: URL) throws {
try self.appending("\n").append(to: url)
}
func append(to url: URL) throws {
let data = self.data(using: String.Encoding.utf8)
try data?.append(to: url)
}
}
extension Data {
func append(to url: URL) throws {
if let fileHandle = try? FileHandle(forWritingTo: url) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
} else {
try write(to: url)
}
}
}
Upvotes: 11
Reputation: 2376
Here's an update for PointZeroTwo's answer written in Swift 3.0, with one quick note - in the playground testing using a simple filepath works, but in my actual app I needed to build the URL using .documentDirectory (or which ever directory you chose to use for reading and writing - make sure it's consistent throughout your app):
extension String {
func appendLineToURL(fileURL: URL) throws {
try (self + "\n").appendToURL(fileURL: fileURL)
}
func appendToURL(fileURL: URL) throws {
let data = self.data(using: String.Encoding.utf8)!
try data.append(fileURL: fileURL)
}
}
extension Data {
func append(fileURL: URL) throws {
if let fileHandle = FileHandle(forWritingAtPath: fileURL.path) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.write(self)
}
else {
try write(to: fileURL, options: .atomic)
}
}
}
//test
do {
let dir: URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last! as URL
let url = dir.appendingPathComponent("logFile.txt")
try "Test \(Date())".appendLineToURL(fileURL: url as URL)
let result = try String(contentsOf: url as URL, encoding: String.Encoding.utf8)
}
catch {
print("Could not write to file")
}
Upvotes: 57
Reputation: 285210
This is a modern summary of the previous answers as an extension of URL
and with a parameter to append also a (line) separator, default value is a linefeed
character.
If the file doesn't exist it will be created.
The trailing
parameter defines the position of the separator. If it's false
the separator is inserted at the front of the string (default is true
which appends the separator)
extension URL {
func appendText(_ text: String,
addingLineSeparator separator: String = "\n",
trailing: Bool = true) throws {
let newText = trailing ? text + separator : separator + text
let newData = Data(newText.utf8)
do {
let fileHandle = try FileHandle(forWritingTo: self)
defer{ fileHandle.closeFile() }
fileHandle.seekToEndOfFile()
fileHandle.write(newData)
} catch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError {
try newData.write(to: self, options: .atomic)
}
}
}
Upvotes: 0
Reputation: 8143
All answers (as of now) recreate the FileHandle for every write operation. This may be fine for most applications, but this is also rather inefficient: A syscall is made, and the filesystem is accessed each time you create the FileHandle.
To avoid creating the filehandle multiple times, use something like:
final class FileHandleBuffer {
let fileHandle: FileHandle
let size: Int
private var buffer: Data
init(fileHandle: FileHandle, size: Int = 1024 * 1024) {
self.fileHandle = fileHandle
self.size = size
self.buffer = Data(capacity: size)
}
deinit { try! flush() }
func flush() throws {
try fileHandle.write(contentsOf: buffer)
buffer = Data(capacity: size)
}
func write(_ data: Data) throws {
buffer.append(data)
if buffer.count > size {
try flush()
}
}
}
// USAGE
// Create the file if it does not yet exist
FileManager.default.createFile(atPath: fileURL.path, contents: nil)
let fileHandle = try FileHandle(forWritingTo: fileURL)
// Seek will make sure to not overwrite the existing content
// Skip the seek to overwrite the file
try fileHandle.seekToEnd()
let buffer = FileHandleBuffer(fileHandle: fileHandle)
for i in 0..<count {
let data = getData() // Your implementation
try buffer.write(data)
print(i)
}
Upvotes: 3
Reputation: 546
Here is a way to update a file in a much more efficient way.
let monkeyLine = "\nAdding a 🐵 to the end of the file via FileHandle"
if let fileUpdater = try? FileHandle(forUpdating: newFileUrl) {
// Function which when called will cause all updates to start from end of the file
fileUpdater.seekToEndOfFile()
// Which lets the caller move editing to any position within the file by supplying an offset
fileUpdater.write(monkeyLine.data(using: .utf8)!)
// Once we convert our new content to data and write it, we close the file and that’s it!
fileUpdater.closeFile()
}
Upvotes: 22
Reputation: 4117
A variation over some of the posted answers, with following characteristics:
fails silently if the text cannot be encoded or the path does not exist
class Logger {
static var logFile: URL? {
guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
let formatter = DateFormatter()
formatter.dateFormat = "dd-MM-yyyy"
let dateString = formatter.string(from: Date())
let fileName = "\(dateString).log"
return documentsDirectory.appendingPathComponent(fileName)
}
static func log(_ message: String) {
guard let logFile = logFile else {
return
}
let formatter = DateFormatter()
formatter.dateFormat = "HH:mm:ss"
let timestamp = formatter.string(from: Date())
guard let data = (timestamp + ": " + message + "\n").data(using: String.Encoding.utf8) else { return }
if FileManager.default.fileExists(atPath: logFile.path) {
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
fileHandle.closeFile()
}
} else {
try? data.write(to: logFile, options: .atomicWrite)
}
}
}
Upvotes: 36
Reputation: 1377
Update: I wrote a blog post on this, which you can find here!
Keeping things Swifty, here is an example using a FileWriter
protocol with default implementation (Swift 4.1 at the time of this writing):
Note: this is only for text. You could do something similar to write/append Data
.
import Foundation
enum FileWriteError: Error {
case directoryDoesntExist
case convertToDataIssue
}
protocol FileWriter {
var fileName: String { get }
func write(_ text: String) throws
}
extension FileWriter {
var fileName: String { return "File.txt" }
func write(_ text: String) throws {
guard let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw FileWriteError.directoryDoesntExist
}
let encoding = String.Encoding.utf8
guard let data = text.data(using: encoding) else {
throw FileWriteError.convertToDataIssue
}
let fileUrl = dir.appendingPathComponent(fileName)
if let fileHandle = FileHandle(forWritingAtPath: fileUrl.path) {
fileHandle.seekToEndOfFile()
fileHandle.write(data)
} else {
try text.write(to: fileUrl, atomically: false, encoding: encoding)
}
}
}
Upvotes: 4
Reputation: 2314
Here's a version for Swift 2, using extension methods on String and NSData.
//: Playground - noun: a place where people can play
import UIKit
extension String {
func appendLineToURL(fileURL: NSURL) throws {
try self.stringByAppendingString("\n").appendToURL(fileURL)
}
func appendToURL(fileURL: NSURL) throws {
let data = self.dataUsingEncoding(NSUTF8StringEncoding)!
try data.appendToURL(fileURL)
}
}
extension NSData {
func appendToURL(fileURL: NSURL) throws {
if let fileHandle = try? NSFileHandle(forWritingToURL: fileURL) {
defer {
fileHandle.closeFile()
}
fileHandle.seekToEndOfFile()
fileHandle.writeData(self)
}
else {
try writeToURL(fileURL, options: .DataWritingAtomic)
}
}
}
// Test
do {
let url = NSURL(fileURLWithPath: "test.log")
try "Test \(NSDate())".appendLineToURL(url)
let result = try String(contentsOfURL: url)
}
catch {
print("Could not write to file")
}
Upvotes: 16
Reputation: 90117
You should use NSFileHandle, it can seek to the end of the file
let dir:NSURL = NSFileManager.defaultManager().URLsForDirectory(NSSearchPathDirectory.CachesDirectory, inDomains: NSSearchPathDomainMask.UserDomainMask).last as NSURL
let fileurl = dir.URLByAppendingPathComponent("log.txt")
let string = "\(NSDate())\n"
let data = string.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)!
if NSFileManager.defaultManager().fileExistsAtPath(fileurl.path!) {
var err:NSError?
if let fileHandle = NSFileHandle(forWritingToURL: fileurl, error: &err) {
fileHandle.seekToEndOfFile()
fileHandle.writeData(data)
fileHandle.closeFile()
}
else {
println("Can't open fileHandle \(err)")
}
}
else {
var err:NSError?
if !data.writeToURL(fileurl, options: .DataWritingAtomic, error: &err) {
println("Can't write \(err)")
}
}
Upvotes: 38