Swift 5: How to save results of NSFetchRequest to File - swift

I am new to programming in general and have started with Swift. I have a feeling what I'm attempting to do is a bit outside of my scope, but I've come so far so here's the ask:
I am adding a tracker to a program for macOS X I've already created. The end user inputs a number and hits "Add to tracker" which then takes that number, the timestamp from the button click and writes that to the appropriate entity in Core Data. Everything works perfectly, my NSTable displays the data and I my batch delete works, but I cannot for the life of me work out the best way to take the results from the NSFetchRequest and print them to a text file.
Here is the code for my fetch request that occurs when the "print" button is hit:
#IBAction func printTracker(_ sender: Any) {
fetchRequest.propertiesToFetch = ["caseDate","caseNumber"]
fetchRequest.returnsDistinctResults = true
fetchRequest.resultType = NSFetchRequestResultType.dictionaryResultType
do {
let results = try context.fetch(fetchRequest)
let resultsDict = results as! [[String:String]]
} catch let err as NSError {
print(err.debugDescription)
}
}
After the "resultsDict" declaration is where I just can't seem to come to a workable solution for getting it to string, then to txt file.
If I add a print command to the console as is, I can see that resultsDict pulls correctly with the following format:
[["caseNumber": "12345", "caseDate": "3/22/21, 5:48:18 PM"]]
Ideally I need it in plaintext more like
"3/22/21, 5:48:18 PM : 12345"
Any advice or help on the conversion would be greatly appreciated.

A simple way if there is not a huge amount of data returned is to create a string from the fetched data and then write that string to disk
First create the string by getting the values from the dictionary and adding them in the right order into a string and joining the strings with a new line character
let output = results.reduce(into: []) { $0.append("\($1["caseDate", default: ""]) : \($1["caseNumber", default: ""])") }
.joined(separator: "\n")
Then we can write them to file, here I use the Document directory as the folder to save the file in
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let path = paths[0].appendingPathComponent("results.txt")
do {
try String(output).write(to: path, atomically: true, encoding: .utf8)
} catch {
print("Failed to write to file, error: \(error)")
}

Related

How can I write to a file, line by line in Swift 4

I need help figuring out how to write repeatedly to the same, open, output file.
I am using Swift 4.2. My searches and tests have turned up only code that writes a single text string to a file and then closes the file. The next opening overwrites the last one. An example is shown below.
The problem is that I need to be able to write large numbers of records (say, 1.5 million) and perform calculations on each record just before it is written to a file. That’s not feasible when the code will only write once before closing. I'm calling this "writing line by line", much like the opposite, to "read line by line."
I tried to find an option in various Swift write statements and SO posts, but everything seems to be geared toward writing once then closing the file. I tried an open for append, but that did not work and anyway it seems inefficient to open, close, reopen-append each time I want to write to a file. I tried some C code in Swift, using open(… and freopen(… but could not get something that the compiler wouldn't complain about. Hopefully, there is a way to do this all in Swift. The following code works nicely for one write.
let file0 = “test_file.txt”
let s0 = ("This is a test line of text")
do {
try s0.write(to: NSURL(fileURLWithPath: file0) as URL, atomically: false, encoding: String.Encoding.utf8)
} catch {
print("Problem writing to file0")
}
How can I adapt this code snippet to write a string, and then another and another etc, and before closing the file when it’s all done? If not with this, is there Swift code that will do the job?
Following are the essential code components needed to write to a file, line-by-line in Swift. First is some file management code to create a file if it does not exist, then there is code to print a series of example statements, followed by code to print to the file in a loop, and finally close the file. This code worked correctly in Swift 4.2. The difference between this and the method in the question is that the write statements in this code use a method of fileHandle! and the question shows a method of a Swift string.
print("Swift_Write_to_File_Test_1")
var outFilename: NSString = "test_file.txt"
// Begin file manager segment
// Check for file presence and create it if it does not exist
let filemgr = FileManager.default
let path = filemgr.urls(for: FileManager.SearchPathDirectory.documentDirectory, in: FileManager.SearchPathDomainMask.userDomainMask).last?.appendingPathComponent(outFilename as String)
if !filemgr.fileExists(atPath: (path?.absoluteString)!) {
filemgr.createFile(atPath: String(outFilename), contents:Data(" ".utf8), attributes: nil)
}
// End file manager Segment
// Open outFilename for writing – this does not create a file
let fileHandle = FileHandle(forWritingAtPath: outFilename as String)
if(fileHandle == nil)
{
print("Open of outFilename forWritingAtPath: failed. \nCheck whether the file already exists. \nIt should already exist.\n");
exit(0)
}
var str: NSString = "2. Test string from NSString.\n";
var str0: String = "3. Test string from a Swift String.\n"
var str1: NSString = "4. Test string from NSString.\n";
fileHandle!.write("1. Text String in-line with code statement.\n".data(using: .utf8)!)
fileHandle!.write(String(str).data(using: .utf8)!)
fileHandle!.write(str0.data(using: .utf8)!)
fileHandle!.write(String(str1).data(using: .utf8)!)
fileHandle!.write("5. Text String in-line with code statement.\n".data(using: .utf8)!)
fileHandle!.write("6. Text in a loop follows: \n".data(using: .utf8)!)
for i in 0...5
{
//Assemble a string then write it to the file.
var s0: String = ""
s0 = String(i)
//s0.append(" ... some text here.\n") // See improvement below
s0 += " ... some text here.\n" // This is a better than .append
fileHandle!.write(s0.data(using: .utf8)!)
}
// End of file-writing segment
fileHandle!.closeFile()
This worked for me in Swift 5:
func writeFile() -> Bool
{
let outFilename: String = "test_file.txt"
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
let outFilePath = documentsURL!.appendingPathComponent(outFilename).path
let fileManager = FileManager.default
// If file exists, remove it
if fileManager.fileExists(atPath: outFilePath)
{
do { try fileManager.removeItem(atPath: outFilePath) }
catch { return false }
}
// Create file and open it for writing
fileManager.createFile(atPath: outFilePath, contents:Data(" ".utf8), attributes: nil)
let fileHandle = FileHandle(forWritingAtPath: outFilePath)
if fileHandle == nil
{
return false
}
else
{
// Write data
fileHandle!.write("Test line 1\n".data(using: .utf8)!)
fileHandle!.write("Test line 2\n".data(using: .utf8)!)
fileHandle!.write("Test line 3\n".data(using: .utf8)!)
// Close file
fileHandle!.closeFile()
return true
}
}

How to Get Test Coverage for Guard Statement Fall-through

I started writing iOS unit tests today with the BDD approach. I have a question regarding guard statements and getting to 100% code coverage.
I have the following code, which handles the conversion of Data into Customer objects.
internal final class func customer(from data: Data) -> Customer? {
do {
guard let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: []) as? Dictionary<String, Any> else {
return nil
}
var customerFirstName: String? = nil
var customerLastName: String
if let firstName = jsonDictionary["first_name"] as? String {
customerFirstName = firstName
}
guard let lastName = jsonDictionary["last_name"] as? String else {
return nil
}
customerLastName = lastName
return Customer(firstName: customerFirstName, lastName: customerLastName)
} catch {
return nil
}
}
When our backend was created, some customers were given just a last name, which contained their first and last names. That is why the customer's first name is optional; their full name may be the value for last_name.
In my code, the customer's first name is optional while their last name is required. If their last name is not returned in the received JSON from a network request, then I do not create the customer. Also, if the Data cannot be serialized into a Dictionary, then the customer is not created.
I have two JSON files, both of which contain customer information that I am using to test both scenarios.
One contains no first name in the JSON:
{
"first_name": null,
"last_name": "Test Name",
}
The other contains a first name in the JSON:
{
"first_name": "Test",
"last_name": "Name",
}
In my unit test, using Quick and Nimble, I handle the creation of a Customer when the first name is not available and when it is:
override func spec() {
super.spec()
let bundle = Bundle(for: type(of: self))
describe("customer") {
context("whenAllDataAvailable") {
it("createsSuccessfully") {
let path = bundle.path(forResource: "CustomerValidFullName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).toNot(beNil())
}
}
context("whenMissingLastName") {
it("createsUnsuccessfully") {
let path = bundle.path(forResource: "CustomerMissingLastName", ofType: "json", inDirectory: "ResponseStubs")!
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let customer = DataTransformer.customer(from: data)
expect(customer).to(beNil())
}
}
}
}
This ensures that I am creating a Customer when the first name is missing or present in the returned JSON.
How can I get to 100% code coverage of this method, using BDD, when my code does not hit the else clauses of the guard statements since the data is able to be turned into valid JSON objects? Should I just add another .json file with data that cannot be transformed into a JSON object to ensure that a Customer is not created as well as a .json file that contains a missing last_name to ensure that a Customer is not created?
Am I just over-thinking the "100% code coverage" concept? Do I even need to have the else clauses of the guard statements tested? Do I even have the appropriate approach using the BDD method?
Just write whatever JSON you want — malformed in every way you can think of. Examples:
You can hit your exception-handling with something that isn't correct JSON.
You can hit your very first guard with something that is a JSON array, not a dictionary.
As the saying goes, you only need to cover code that you want to be correct. 😉
TDD and BDD are related. In TDD, you'd write a failing test first. Then, you'd write code that passes that test as quickly as you can. Finally, you'd clean up your code to make it better. It looks like you're adding tests after-the-fact.
By the way, your tests would be much clearer if you didn't use external files, but put the JSON straight into your tests. Here's a screencast showing how I TDD the beginnings of JSON conversion. The screencast is in Objective-C but the principles are the same: https://qualitycoding.org/tdd-json-parsing/
100% code coverage with if let.
At times, its not feasible to forcefully prepare a malformed object to enforce the execution hitting return or return nil used in guard statements.
This is the case when you have 3rd party SDKs integrated and respective 3rd party objects get created runtime within the method.
For example :
func aMethod() {
guard let response = 3rdPartyResponse<3rdPartyInput>.createControl(with: .create(with: .someCase)) as? 3rdPartyResponse<3rdPartyInput> else { return }
}
In this case, its very hard, at times not possible to hit the return.
But If code coverage is the main criteria, you can go with using an if let for such cases If lets does give 100% code coverage

Using Xcode 7/Swift 2 writeToPath to Resources file the call does not fail but no data is written

I am using Xcode 7.3.1 and Swift 2.0. I am using the following code sample:
func writeToResourcesDataDir() {
if let path = NSBundle.mainBundle().pathForResource("TestData", ofType: ".json") {
let str = "Test String"
do {
try str.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding)
print("writeToFile successful")
} catch {
print("writeToFile failed")
}
} else {
print("Path does not exist")
}
}
Running under Xcode in the see the "writeToFile successful" message.But, also using the simulator, I can display the TestData in the Resources directory and the file does not have the string.I also used a terminal window in Mac to look at the files in the Resources directory and the TestData file is empty (0 bytes).I know I am in the correct Resources directory because there is another file in the directory that has correct data that is used for running the other parts of the program.
I have spent several days now looking at other google entries about data from writeToFile not working and I have tried out every fix or things to try I have found.
Can anyone help?
I added code to accept the boolean return from the call to writeToFile and it returns a false. I'm not sure why a false is returned but the catch isn't invoked.I am not sure how to get the error code that goes with this writeToFile in Swift 2.0.
I am also wondering if this is a write permissions problem.Should I be using the Documents directory instead of the Data directory?
Try something like this. This is swift 2.3 and xcode 8.
let filename = "yourjsonfile"
let documentDirectoryURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: true)
let filePath = documentDirectoryURL.URLByAppendingPathComponent(filename)
let fileExist = filePath?.checkResourceIsReachableAndReturnError(nil)
if (fileExist == true) {
print("Found file")
} else {
print("File not found")
}

Problems importing large file in Core Data with background thread

I am running into some trouble importing a large .csv file in OS X using Core Data on a background thread.
My simplified data model is a Dataset which has a to-many relationship to many Entries. Each entry is a line in the .csv file (which has a bunch of attributes that I'll leave out for brevity). Following what I have read about efficiently importing lots of data, along with how to make a progress indicator work properly, I have created a managed object context for the purposes of going the import.
I am having trouble with two things:
I need to hang on to a reference to the new Dataset at the end of the import, because I need to select it in the popup. this will be done in the main thread, but for efficiency (and to make my NSProgressIndicator work) the new dataset is created on the background thread, with the background MOC.
From what I read, batching the import, so that the background MOC saves and resets, is the best way to stop the import from eating up too much memory. That does not turn out to be the case so far - it looks like gigs of memory is being used even for files in the tens of megabytes. Also, once I reset my import MOC, it cannot find the data for the inDataset, and so cannot create the relationship between all subsequent Entries and the Dataset.
I've posted the simplified code below. I have tried refreshObject:mergeChanges, without good results. Can anyone point me at what I am doing wrong?
let inFile = op.URL
dispatch_async(dispatch_get_global_queue(priority, 0)) {
//create moc
let inMOC = NSManagedObjectContext()
inMOC.undoManager = nil
inMOC.persistentStoreCoordinator = self.moc.persistentStoreCoordinator
var inDataset : inDataset = Dataset(entity: NSEntityDescription.entityForName("Dataset", inManagedObjectContext: inMOC)!, insertIntoManagedObjectContext: inMOC)
//set up NSProgressIndicator here, removed for clarity
let datasetID = inDataset.objectID
mocDataset = self.moc.objectWithID(datasetID) as! Dataset
let fileContents : String = (try! NSString(contentsOfFile: inFile!.path!, encoding: NSUTF8StringEncoding)) as String
let fileLines : [String] = fileContents.componentsSeparatedByString("\n")
var batchCount : Int = 0
for thisLine : String in fileLines {
let newEntry : Entry = Entry(entity: NSEntityDescription.entityForName("Entry", inManagedObjectContext: inMOC)!, insertIntoManagedObjectContext: inMOC)
//Read in attributes of this entry from this line, removed here for brevity
newEntry.setValue("Entry", forKey: "type")
newEntry.setValue(inDataset, forKey: "dataset")
inDataset.addEntryObject(newEntry)
dispatch_async(dispatch_get_main_queue()) {
self.progInd.incrementBy(1)
}
batchCount++
if(batchCount > 1000){
do {
try inMOC.save()
} catch let error as NSError {
print(error)
} catch {
fatalError()
}
batchCount = 0
inMOC.reset()
inDataset = inMOC.objectWithID(datasetID) as! Dataset
// fails here, does not seem to be able to find the data associated with inDataset
}
}// end of loop for reading lines
//save whatever remains after last batch save
do {
try inMOC.save()
} catch let error as NSError {
print(error)
} catch {
fatalError()
}
inMOC.reset()
dispatch_async(dispatch_get_main_queue()) {
//This is done on main queue after background queue has read all line
// I thought the statement just below would refresh mocDataset, but no luck
self.moc.refreshObject(mocDataset, mergeChanges: true)
//new dataset selected from popup
let datafetch = NSFetchRequest(entityName: "Dataset")
let datasets : [Dataset] = try! self.moc.executeFetchRequest(datafetch) as! [Dataset]
self.datasetController.addObjects(datasets)
let mocDataset = self.moc.objectWithID(datasetID) as! Dataset
//fails here too, mocDataset object has data as a fault
let nDarray : [Dataset] = [mocDataset]
self.datasetController.setSelectedObjects(nDarray)
}
}

Writing to log file seems to overwrite previous save

As an exercise, I'm trying to write a logging class that logs strings to a text file. I've got my application to write and read from the file. However, if I try to log multiple times it seems to only pick up the most recent log.
Attempt
writing
private let file = "logfile.txt"
func write(text: String) {
let path = getDocumentsDirectory()
do {
try text.writeToFile(path, atomically: false, encoding: NSUTF8StringEncoding)
}
catch let error {
print("error: \n \(error)")
}
}
reading
func read() {
let path = getDocumentsDirectory()
do {
let text2 = try String(contentsOfFile: path, encoding: NSUTF8StringEncoding)
print("From Log: \n \(text2)")
}
catch let error {
print("error: \n \(error)")
}
}
func getDocumentsDirectory() -> String {
guard let dir : NSString = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first else {
return ""
}
let documentsDirectory : String = dir.stringByAppendingPathComponent(file)
return documentsDirectory
}
Result
When I try to read my file I only get the last line saved.
Question
If my goal is to endlessly append new logs to a file, and to read the log file in bulk. What changes to my code do I need to make?
more details:
I'm writing to the file on application load:
Logger.shared.write("instance one writes")
Logger.shared.write("instance one writes again")
Logger.shared.write("instance one writes yet again")
and then attempting to read:
Logger.shared.read()
output:
From Log:
instance one writes yet again
The writeToFile(_:atomically:encoding:) method provided by Foundation replaces the contents of the given file. There are several ways of appending to files:
Plain ol’ fopen (with mode "a") and fwrite.
NSOutputStream, such as NSOutputStream(toFileAtPath: mypath, append: true), using stream.write(bytes, len) to write data.
Perhaps the easiest, NSFileHandle, such as NSFileHandle(forWritingAtPath: mypath), using seekToEndOfFile() and writeData().