Append text or data to text file in Swift

SwiftAppendText Files

Swift Problem Overview


I already have read https://stackoverflow.com/questions/24097826/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)")
    }

Swift Solutions


Solution 1 - Swift

Here's an update for PointZeroTwo's answer 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")
 }

Thanks PointZeroTwo.

Solution 2 - Swift

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)")
    }
}

Solution 3 - Swift

A variation over some of the posted answers, with following characteristics:

  • based on Swift 5

  • accessible as a static function

  • appends new entries to the end of the file, if it exists

  • creates the file, if it doesn't exist

  • no cast to NS objects (more Swiftly)

  • 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)
             }
         }
     }
    

Solution 4 - Swift

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()
}

Solution 5 - Swift

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")
}

Solution 6 - Swift

In order to stay in the spirit of @PointZero Two. Here an update of his code 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)
        }
    }
}

Solution 7 - Swift

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):

  • To use this, have your entity (class, struct, enum) conform to this protocol and call the write function (fyi, it throws!).

  • Writes to the document directory.

  • Will append to the text file if the file exists.

  • Will create a new file if the text file doesn't exist.

  • 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)
              }
          }
      }
    

Solution 8 - Swift

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)
}


Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAliView Question on Stackoverflow
Solution 1 - SwiftdavidrynnView Answer on Stackoverflow
Solution 2 - SwiftMatthias BauchView Answer on Stackoverflow
Solution 3 - SwiftatineoSEView Answer on Stackoverflow
Solution 4 - SwiftHiį»n Đį»—View Answer on Stackoverflow
Solution 5 - SwiftPointZeroTwoView Answer on Stackoverflow
Solution 6 - SwiftLuc-OlivierView Answer on Stackoverflow
Solution 7 - Swiftjason zView Answer on Stackoverflow
Solution 8 - SwiftBerikView Answer on Stackoverflow