An efficient way of writing to file, swift - ios

I'm getting data from sensors using bluetooth, I want to append the string of data I get to the end of file.
When I tried the regular approach
if let dir = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first {
let path = NSURL(fileURLWithPath: dir).URLByAppendingPathComponent(self.file)
do {
try text.writeToURL(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch {/* error handling here */}
My app started to slow down until even labels were not updating anymore.
Tried using dispatch_async to do in background thread but still it was slowing down my app.
What approach should I use? I read sth about stream but failed to find some solutions in swift I could rely on

Probably your bluetooth is reading data faster than you are performing your file operations. You can optimize it by appending the text to the file instead of reading all the content on each write operation. You could also reuse the file handler between writes and keep the file open.
This sample is extracted from this answer:
struct MyStreamer: OutputStreamType {
lazy var fileHandle: NSFileHandle? = {
let fileHandle = NSFileHandle(forWritingAtPath: self.logPath)
return fileHandle
}()
lazy var logPath: String = {
let path : NSString = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true).first!
let filePath = (path as NSString).stringByAppendingPathComponent("log.txt")
if !NSFileManager.defaultManager().fileExistsAtPath(filePath) {
NSFileManager.defaultManager().createFileAtPath(filePath, contents: nil, attributes: nil)
}
print(filePath)
return filePath
}()
mutating func write(string: String) {
print(fileHandle)
fileHandle?.seekToEndOfFile()
fileHandle?.writeData(string.dataUsingEncoding(NSUTF8StringEncoding)!)
}
}
Then, you can create a single streamer and reuse it in different writes:
var myStream = MyStreamer()
myStream.write("First of all")
myStream.write("Then after")
myStream.write("And, finally")
In this case, you have the bonus that MyStreamer is also a OutputStreamType, so you can use it like this:
var myStream = MyStreamer()
print("First of all", toStream: &myStream )
print("Then after", toStream: &myStream)
print("And, finally", toStream: &myStream)
Finally I'd recommend you to move 'log.txt' string to a instance variable and pass it as a constructor parameter:
var myStream = MyStreamer("log.txt")
More info about file handler in the Apple Docs.

update #redent84's, to work in Swift 5
Code:
struct MyStreamer{
lazy var fileHandle = FileHandle(forWritingAtPath: logPath)
lazy var logPath: String = {
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .allDomainsMask, true)[0]
let filePath = path + "/log.txt"
if FileManager.default.fileExists(atPath: filePath) == false{
FileManager.default.createFile(atPath: filePath, contents: nil, attributes: nil)
}
print(filePath)
return filePath
}()
mutating func write(_ string: String) {
print(fileHandle?.description ?? "呵呵")
fileHandle?.seekToEndOfFile()
if let data = string.data(using: String.Encoding.utf8){
fileHandle?.write(data)
}
}
}
Usage:
var myStream = MyStreamer()
myStream.write("First of all")
myStream.write("Then after")
myStream.write("And, finally")

try to write file like this..
var paths: [AnyObject] = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
let filePath = paths[0].stringByAppendingString("/filename.mov")
do
{
try NSFileManager.defaultManager().removeItemAtURL(outputURL)
}
catch
{
error as NSError
}
do {
try text.writeToURL(path, atomically: false, encoding: NSUTF8StringEncoding)
}
I mean to say at last is you have to remove first.. If any query you can ask me

Related

Convert String to Array Items

The below code create the file and puts the data in it but it saves as the following:
["123", "456", "", "789"] How do I get a new item for each paragraph? the idea is the song lyrics would save to the file based on where the verses start and end. I am new to swift so there may be a better way to do this
#IBOutlet weak var songName: UITextField!
#IBOutlet weak var newLyrics: UITextView!
#IBAction func saveSong(_ sender: Any) {
let sName = songName.text!
let fileManager = FileManager.default
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let path = documentDirectory.appending("/Lyrics/\(sName).plist")
let lyrics = newLyrics.text.components(separatedBy: "\n\n")
print(lyrics)
if let tDocumentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first {
let filePath = tDocumentDirectory.appendingPathComponent("Lyrics")
if !fileManager.fileExists(atPath: filePath.path) {
do {
try fileManager.createDirectory(atPath: filePath.path, withIntermediateDirectories: true, attributes: nil)
} catch {
NSLog("Couldn't create document directory")
}
}
NSLog("Document directory is \(filePath)")
}
if(!fileManager.fileExists(atPath: path)){
print(path)
var lyricArray = [String]()
lyricArray.append("\(lyrics)")
// any other key values
let lyricData = NSArray(array: lyricArray)
let answer = lyricData.write(toFile: path, atomically: true)
print("File Created? \(answer)")
} else {
print("File Exists")
}
I expect the output to be
Item0 "123"
Item1 "456"
actual output is
Item0 ["123", "456", "", "789"]
You are appending the description of an array to lyricArray with string interpolation (\(...)).
The description of an array is one literal string with format "[\"item1\", \"item2\"]".
You have to append the contents of the array
Replace
lyricArray.append("\(lyrics)")
with
lyricArray.append(contentsOf: lyrics)
or drop lyricArray completely and simply write
let lyricData = NSArray(array: lyrics)
or – highly recommended –
do {
let lyricData = try PropertyListSerialization.data(fromPropertyList: lyrics, format: .xml, options: 0)
} catch { print(error) }
Notes:
Don't use NSArray to handle property lists in Swift. Use PropertyListSerialization.
Never print meaningless literal strings in a catch clause like Couldn't do that. Print the error.
And why do get the path to the documents folder twice with different API (okay, first the path and then the URL)?

Is it possible to output userinput to a .csv using swift?

I am very new to learning to code in Swift. I am trying to make an application that keeps a list of people who are coming in. I want it to log the name they input, time of visit, and the nature of their visit. However, I want this to be able to be exported to a program like Numbers or Excel. I have found some info on storing the inputs from the user but those seem to get deleted if the app is closed. I can't seem to find any other info, but perhaps I'm just searching the wrong info. Any help or guidance is appreciated.
Just store your log in a Array and then with this function you safe it to a .csv file.
func saveCSV(_ name : String,_ customUrl : URL) -> Bool {
let fileName = "\(name).csv"
let b = customUrl.appendingPathComponent(fileName)
var csvText = ""
var id = "1"
var name = "test"
csvText = "ID,Name\n"
let newLine = "\(id),\(name))\n"
csvText.append(newLine)
//or create a loop
// Task is my custom Struct
var array : [Task]
for task in customArray {
let newLine = "\(task.ean),\(task.menge),\(task.name)\n"
csvText.append(newLine)
}
do {
try csvText.write(to: b, atomically: true, encoding: String.Encoding.utf8)
return true
} catch {
print("Failed to create file")
print("\(error)")
return false
}
}
func createDic()->URL?{
let documentsPath1 = NSURL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
// CSV is the folder name
let logsPath = documentsPath1.appendingPathComponent("CSV")
do {
try FileManager.default.createDirectory(atPath: logsPath!.path, withIntermediateDirectories: true, attributes: nil)
return logsPath
} catch let error as NSError {
NSLog("Unable to create directory \(error.debugDescription)")
}
return nil
}
var customUrl = createDic()
and now you can call it :
if saveCSV(userList, customUrl){
print("success")
}
and after this you can do what you want with the .csv file

Read and write data from & to text file in Documents Directory incrementally with Swift [duplicate]

I am trying to append a string into text file. I am using the following code.
let dirs : [String]? = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.AllDomainsMask, true) as? [String]
if (dirs) != nil {
let dir = dirs![0] //documents directory
let path = dir.stringByAppendingPathComponent("votes")
let text = "some text"
//writing
text.writeToFile(path, atomically: true, encoding: NSUTF8StringEncoding, error: nil)
//reading
let text2 = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
println(text2) //prints some text
}
this does not append the string to file. Even if I call this function repeatedly.
If you want to be able to control whether to append or not, consider using OutputStream. For example:
do {
let fileURL = try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
.appendingPathComponent("votes.txt")
guard let outputStream = OutputStream(url: fileURL, append: true) else {
print("Unable to open file")
return
}
outputStream.open()
let text = "some text\n"
try outputStream.write(text)
outputStream.close()
} catch {
print(error)
}
By the way, this is an extension that lets you easily write a String (or Data) to an OutputStream:
extension OutputStream {
enum OutputStreamError: Error {
case stringConversionFailure
case bufferFailure
case writeFailure
}
/// Write `String` to `OutputStream`
///
/// - parameter string: The `String` to write.
/// - parameter encoding: The `String.Encoding` to use when writing the string. This will default to `.utf8`.
/// - parameter allowLossyConversion: Whether to permit lossy conversion when writing the string. Defaults to `false`.
func write(_ string: String, encoding: String.Encoding = .utf8, allowLossyConversion: Bool = false) throws {
guard let data = string.data(using: encoding, allowLossyConversion: allowLossyConversion) else {
throw OutputStreamError.stringConversionFailure
}
try write(data)
}
/// Write `Data` to `OutputStream`
///
/// - parameter data: The `Data` to write.
func write(_ data: Data) throws {
try data.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) throws in
guard var pointer = buffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
throw OutputStreamError.bufferFailure
}
var bytesRemaining = buffer.count
while bytesRemaining > 0 {
let bytesWritten = write(pointer, maxLength: bytesRemaining)
if bytesWritten < 0 {
throw OutputStreamError.writeFailure
}
bytesRemaining -= bytesWritten
pointer += bytesWritten
}
}
}
}
For Swift 2 rendition, see previous revision of this answer.
You can also use FileHandle to append String to your text file. If you just want to append your string the end of your text file just call seekToEndOfFile method, write your string data and just close it when you are done:
FileHandle usage Swift 3 or Later
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// create a new text file at your documents directory or use an existing text file resource url
let fileURL = documentsDirectory.appendingPathComponent("simpleText.txt")
do {
try Data("Hello World\n".utf8).write(to: fileURL)
} catch {
print(error)
}
// open your text file and set the file pointer at the end of it
do {
let fileHandle = try FileHandle(forWritingTo: fileURL)
fileHandle.seekToEndOfFile()
// convert your string to data or load it from another resource
let str = "Line 1\nLine 2\n"
let textData = Data(str.utf8)
// append your text to your text file
fileHandle.write(textData)
// close it when done
fileHandle.closeFile()
// testing/reading the file edited
if let text = try? String(contentsOf: fileURL, encoding: .utf8) {
print(text) // "Hello World\nLine 1\nLine 2\n\n"
}
} catch {
print(error)
}
Please check the below code as its working for me. Just Add the code as it is:
let theDocumetFolderSavingFiles = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let filePath = "/theUserData.txt"
let thePathToFile = theDocumetFolderSavingFiles.stringByAppendingString(filePath)
let theFileManager = NSFileManager.defaultManager()
if(theFileManager.fileExistsAtPath(thePathToFile)){
do {
let stringToStore = "Hello working fine"
try stringToStore.writeToFile(thePathToFile, atomically: true, encoding: NSUTF8StringEncoding)
}catch let error as NSError {
print("we are geting exception\(error.domain)")
}
do{
let fetchResult = try NSString(contentsOfFile: thePathToFile, encoding: NSUTF8StringEncoding)
print("The Result is:-- \(fetchResult)")
}catch let errorFound as NSError{
print("\(errorFound)")
}
}else
{
// Code to Delete file if existing
do{
try theFileManager.removeItemAtPath(thePathToFile)
}catch let erorFound as NSError{
print(erorFound)
}
}
A simple solution that works for me. UPDATE, it looks like I must have gotten this from here, so credit where credit is due:
Append text or data to text file in Swift
Usage:
"Hello, world".appendToURL(fileURL: url)
Code:
extension String {
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)
}
}
}
Check the reading part.
The method cotentsOfFile: is a method of NSString class. And you have use it wrong way.
So replace this line
let text2 = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)
Here you have to use NSString instead of String class.
let text2 = NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: nil)

Read Write array data to plist not working

func readWriteData(){
let paths = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)
let cachePath = paths.first
// let bundleName = NSBundle.mainBundle().infoDictionary?["CFBundleVersion"] as? String
let filePath = NSURL(fileURLWithPath: cachePath!).URLByAppendingPathComponent("cachedProducts.plist").path
var isDir : ObjCBool = false
var error: NSError?
if NSFileManager.defaultManager().fileExistsAtPath(filePath!, isDirectory: &isDir) {
var plistArray = NSArray(contentsOfFile: filePath!)
print(plistArray)//retrieving stored data
}else {
do {
try NSFileManager.defaultManager().createDirectoryAtPath(filePath!, withIntermediateDirectories: false, attributes: nil)
var cachedArray = [AnyObject]()
cachedArray.append("Harsh")
let cachedProductarray : NSArray = cachedArray
cachedProductarray.writeToFile(filePath!, atomically: true)
//Storing data to plist
} catch let error as NSError {
NSLog("\(error.localizedDescription)")
}
}
}
when i try to print plistArray it's returning nil. Please help me with the solution. I tried this using cachesDirectory and Document Directory it's the same scenario with both
The cache directory is supposed to be created at cachePath, not at filePath.
Rather than using the quite primitive writeToFile method of NSArray I'd recommend to use NSPropertyListSerialization in conjunction with NSData's writeToFile:options:error (still better writeToURL...) method to get meaningful error messages.

Reading and saving data into a file [duplicate]

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

Resources