在Swift中将文本或数据追加到文本文件中。

66

我已经阅读了从文本文件中读写数据

我需要在文本文件末尾添加数据(字符串)。
一种明显的方法是从磁盘读取文件,将字符串附加到其末尾并将其写回,但这样做效率不高,特别是当您处理大文件并且经常进行此操作时。

所以问题是“如何将字符串附加到文本文件的末尾,而无需读取整个文件并将其全部写回”?

目前为止我有:

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

@John 你是什么意思? - Ky -
10个回答

56

以下是对PointZeroTwo在Swift 3.0中回答的更新,简要说明一下 - 在使用简单文件路径进行播放测试时可行,但在我的实际应用程序中,我需要使用.documentDirectory构建URL(或者您选择用于读写的任何目录 - 确保在整个应用程序中保持一致):

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

谢谢 PointZeroTwo。


38

您应该使用NSFileHandle,它可以定位到文件末尾

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

16
只需在 Xcode 中点击那些带有白点的小红圆圈,就能让转换为 Swift 3 变得非常容易。您会感到惊讶。 - Chris

35

以下是一些已发布答案的变体,具有以下特点:

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

1
嗨@atineoSE,你有关于在FileManager的Write方法被弃用后该使用什么的信息吗?看起来var writeabilityHandler: ((FileHandle) -> Void)?是异步版本。 - Yigit Alp Ciray
运行得非常好。谢谢! - KamyFC

21

这里有一种更高效的方式来更新文件。

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

16

这是适用于Swift 2的版本,使用String和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")
}

11
为了保持与 @PointZero Two 精神一致,这里提供他的代码的更新版本,适用于 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)
        }
    }
}

4

更新:我写了一篇关于这个的博客文章,你可以在这里找到!

为了保持Swifty,这里有一个例子,使用默认实现的FileWriter协议(在撰写本文时为Swift 4.1):

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

我正在尝试使用这个,但我不知道该怎么做... "让你的实体(类、结构体、枚举)符合这个协议并调用写入函数(顺便说一下,它会抛出异常!)"。我该如何实际调用它来保存视图控制器中文本视图中的文本? - nc14
这将两个概念联系在一起:遵守协议和使用协议扩展来提供默认功能。首先,您的实体应符合协议(例如,class MyClass: FileWriter)。现在,由于FileWriter协议上有一个具有默认实现的扩展名,因此在此示例中的MyClass实体可以免费获得写入功能!所以,您只需在MyClass的实例上调用该函数即可(例如,let myClassInstance = MyClass(); try! myClassInstance.write("hello"))。 - jason z
另外,如果您想要更详细的解释和示例,请查看我在上面回答中包含链接的博客文章。 - jason z

3

目前所有答案都会在每次写操作时重新创建FileHandle。这对于大多数应用程序可能没有问题,但这也相当低效:每次创建FileHandle都会进行系统调用,并访问文件系统。

为了避免多次创建文件句柄,请使用类似以下的方法:

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



这对我没有起作用。 - K17
你刷新了文件句柄吗? - Berik

0
这是一个现代的摘要,作为对之前答案的扩展,使用一个参数来附加一个(行)分隔符,默认值是一个换行符。
如果文件不存在,它将被创建。
"trailing"参数定义了分隔符的位置。如果它是"false",分隔符将插入在字符串的前面(默认是"true",即在字符串后面附加分隔符)。
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)
        }
    }
}

1
测试时,使用NSFileNoSuchFileError而不是魔法数字4会更好吗?或许作为额外的检查,可以再加上域名的判断,以防万一:atch let error as NSError where error.domain == NSCocoaErrorDomain && error.code == NSFileNoSuchFileError - undefined
@Larme 没错,谢谢。 - undefined

0
自从上次回答以来已经过了几年,尽管@atineoSE的答案完美无缺,但我使用了一个类,其中的日期格式化程序和文件URL只创建一次,您可以通过单个调用在任何地方使用它。
以下是Logger.swift文件的代码:
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)")
        }
    }

}

每当你想使用它的时候,你可以简单地像这样做:
Logger.log("Some string I want to save in the file!")

作为一个额外的好处,这里有一个快速函数,我只是把它放在一个Swift文件中(不是一个类,只是一个Swift文件,在这个文件的最底层放入这段代码),它会自动将文件名和函数名放入字符串中(并且也会打印到控制台)。
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)
}

你可以在任何地方像这样使用这个DLog函数:
DLog("String to print AND write to a file")

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接