Swift - Write an Array to a Text File - arrays

I read into myArray (native Swift) from a file containing a few thousand lines of plain text..
myData = String.stringWithContentsOfFile(myPath, encoding: NSUTF8StringEncoding, error: nil)
var myArray = myData.componentsSeparatedByString("\n")
I change some of the text in myArray (no point pasting any of this code).
Now I want to write the updated contents of myArray to a new file.
I've tried this ..
let myArray2 = myArray as NSArray
myArray2.writeToFile(myPath, atomically: false)
but the file content is then in the plist format.
Is there any way to write an array of text strings to a file (or loop through an array and append each array item to a file) in Swift (or bridged Swift)?

As drewag points out in the accepted post, you can build a string from the array and then use the writeToFile method on the string.
However, you can simply use Swift's Array.joinWithSeparator to accomplish the same with less code and likely better performance.
For example:
// swift 2.0
let array = [ "hello", "goodbye" ]
let joined = array.joinWithSeparator("\n")
do {
try joined.writeToFile(saveToPath, atomically: true, encoding: NSUTF8StringEncoding)
} catch {
// handle error
}
// swift 1.x
let array = [ "hello", "goodbye" ]
let joined = "\n".join(array)
joined.writeToFile(...)

With Swift 5 and I guess with Swift 4 you can use code snippet which works fine to me.
let array = ["hello", "world"]
let joinedStrings = array.joined(separator: "\n")
do {
try joinedStrings.write(toFile: outputURL.path, atomically: true, encoding: .utf8)
} catch let error {
// handle error
print("Error on writing strings to file: \(error)")
}

You need to reduce your array back down to a string:
var output = reduce(array, "") { (existing, toAppend) in
if existing.isEmpty {
return toAppend
}
else {
return "\(existing)\n\(toAppend)"
}
}
output.writeToFile(...)
The reduce method takes a collection and merges it all into a single instance. It takes an initial instance and closure to merge all elements of the collection into that original instance.
My example takes an empty string as its initial instance. The closure then checks if the existing output is empty. If it is, it only has to return the text to append, otherwise, it uses String Interpolation to return the existing output and the new element with a newline in between.
Using various syntactic sugar features from Swift, the whole reduction can be reduced to:
var output = reduce(array, "") { $0.isEmpty ? $1 : "\($0)\n\($1)" }

Swift offers numerous ways to loop through an array. You can loop through the strings and print to a text file one by one. Something like so:
for theString in myArray {
theString.writeToFile(myPath, atomically: false, encoding: NSUTF8StringEncoding, error: nil);
}

Related

Splitting string into array string.components(separtedBy: ",") consumes more time

I have text file which contains 18000 lines which have cities names. Each line has city name, state, latitude, longitude etc. Below is the function which does that, if i don't implement string.components(separtedBy: ", ") loading function is pretty fast but with it implemented it takes time which makes my UI freeze. What is the right way of doing it? Is string.components(separtedBy: ", ") that costly?
I profiled the app, this line is taking string.components(separtedBy: ", ") 1.45s out of 2.09s in whole function.
func readCitiesFromCountry(country: String) -> [String] {
var cityArray: [String] = []
var flag = true
var returnedCitiesList: [String] = []
if let path = Bundle.main.path(forResource: country, ofType: "txt") {
guard let streamReader = StreamReader(path: path) else {fatalError()}
defer {
streamReader.close()
}
while flag {
if let nextLine = streamReader.nextLine() {
cityArray = nextLine.components(separatedBy: ",") // this is the line taking a lot of time, without this function runs pretty fast
if (country == "USA") {
returnedCitiesList.append("\(cityArray[0]) , \(cityArray[1]) , \(cityArray[2])")
} else {
returnedCitiesList.append("\(cityArray[0]) , \(cityArray[1])")
}
//returnedCitiesList.append(nextLine)
} else {
flag = false
}
}
} else {
fatalError()
}
return returnedCitiesList
}
StreamReader used in the code can be found here. It helps to read file line by line
Read a file/URL line-by-line in Swift
This question is not about how to split the string into array Split a String into an array in Swift? , rather why splitting is taking more time in the given function.
NSString.components(separatedBy:) returns a [String], which requires that all of the pieces' content be copied, from the original string, and pasted into new-ly allocated stringss. This slows things down.
You could address the symptoms (UI freezing) by putting this work on a background thread, but that just sweeps the problem under the wrong (the inefficient copying is still there), and complicates things (async code is never fun).
Instead, you should consider using String.split(separator:maxSplits:omittingEmptySubsequences:), which returns [Substring]. Each Substring is just a view into the original string's memory, which stores the relevant range so that you only see that portion of the String which is modeled by the Substring. The only memory allocation happening here is for the array.
Hopefully that should be enough to speed your code up to acceptable levels. If not, you should combine both solutions, and use split off-thread.

How to convert string containing array to array in Swift?

I have a string in Swift that contains an array in it. Is it possible to convert the string into an array? All I have found on the internet is converting "abc" to ["a","b","c"], which I would not like to do.
String: "[\"value1\",\"value2\",\"value3\"]"
Result: ["value1","value2","value3"]
I am getting the string from a web request. The code for the request is here:
func webRequest(uri:String)->String{
var value = "";
let request = URLRequest(url: NSURL(string: uri)! as URL)
do {
let response: AutoreleasingUnsafeMutablePointer<URLResponse?>? = nil
let data = try NSURLConnection.sendSynchronousRequest(request, returning: response)
value = String(data: data, encoding: .utf8)!;
} catch _ {
}
return value;
}
First off, the problem here is not converting your string into an array. The problem is getting the array from the web request in the first place.
Let me update your web request function.
func webRequest(url: URL, completion: ([String]?) -> () { // I have updated this function to be asynchronous
let dataTask = URLSession.shared.dataTask(with: url) {
data, urlResponse, error in
// you might want to add more code in here to check the data is valid etc...
guard let data = data,
let arrayOfStrings = JSONDecoder().decode([String].self, from: data) else {
// something went wrong getting the array of strings so return nil here...
completion(nil)
return
}
completion(arrayOfStrings)
}
dataTask.resume()
}
Using this code instead of the code in your question you now have an asynchronous function that will not block the app and one that will pass your array of strings into the completion.
You can now run it like this...
webRequest(url: someURL) { strings in
guard let strings = strings else {
// strings is nil because something went wrong with the web request
return
}
print(strings)
}
Creating the URL
In your question you have this code... NSURL(string: someString)! as URL
You can change this to... let url = URL(string: someString)
Quick side note
Careful where you find tutorials and using code you find on the web. The code used in this question is very old. (at least 4 or 5 years "out of date").
If you're looking for tutorials to help with Swift then some recommendations are...
Ray Wenderlich
Hacking with swift

How do I append elements to a global array (in a for loop) in Swift?

I have an empty global array. The only simple thing I want to do is add an element to this array. It seems in swift this seemingly simple task is proving to be difficult. I am just left with an empty array and nothing is appending to my global array.
I can see that it prints out values in the for loop. So the values are actually there.
This is some stuff I have declared globally (Yes, I know global variables are bad but I will sort that out later):
struct HouseDetails: Decodable {
let median_price: String
let sale_year: String
let transaction_count: String
let type: String
}
var hsArray: [HouseDetails] = []
and in the viewDidLoad() function I have the data which I am storing in local variable "houses". When I loop through the array it prints median_price, showing that the values are there.
However when I do hsArray.append(h) it seems to do nothing.
let jsonUrlString = "https://data.melbourne.vic.gov.au/resource/i8px-csib.json"
guard let url = URL(string: jsonUrlString)
else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let houses = try JSONDecoder().decode([HouseDetails].self, from: data)
for h in houses {
hsArray.append(h)
print(h.median_price)
}
}
catch let jsonErr {
print("Error with json serialization", jsonErr)
}
}.resume()
Thank you for any help. In other languages I am used to being able to append an element to the end of an existing array, so I am sure it is just a small error.
Firstly, why don't you simply do
hsArray.append(contentsOf: houses)
instead of all that for loop
for h in houses {
hsArray.append(h)
print(h.median_price)
}
The issue might be the time at which you are using hsArray. See if the response is received after you use hsArray.

Refer to array based on string output

I'm learning Swift as I go here, so apologies if this is a silly question.
I'm looking to use the output of one function (String) to determine an input into a different function (Array).
The first function output (String) is then combined with another String to form the name of an already defined Array, which i'd like to use as input to second function. However, despite having the same name, the String is not seen as an array.
I've skipped some of the code, but relevant section below.
// Defined array
let rushProb = [0,11,19,64,78,89,96,98,99,100]
// Define probability and outcome function - PlayType
func findPlay(prob: [Int], outcome: [String]) -> String {
if let index = prob.firstIndex(where: { $0 > Int.random(in: 1...100) }) {
return outcome[index]
}
else {
return "na"
}
}
// This is successfully output as "rush"
let playSel = findPlay(prob: scen1Prob, outcome: scenPlay)
// This then creates "rushProb"
let playSelProb = playSel+"Prob"
// I want this to ultimately be findYards(prob: rushProb)
findYards(prob: playSelProb)
Well, you could use a dictionary where the key replaces your array name, and the value is the array. Then, you'd use the name you create to look up the array value in the dictionary:
let arrays = ["rushProb": [0,11,19,64,78,89,96,98,99,100],
"fooProb" : [0,15,29,44,68,78,86,92,94,100]]
// This is successfully output as "rush"
let playSel = findPlay(prob: scen1Prob, outcome: scenPlay)
// This then creates "rushProb"
let playSelProb = playSel+"Prob"
// look up the array that corresponds to "rushProb"
if let array = arrays[playSelProb] {
findYards(prob: array)
}

How do I move String values from an array to a tuple without copying?

I have a fixed size array of Strings: [String; 2]. I want to turn it into a (String, String). Can I do this without copying the values?
The piece of code that I'm working on in particular is the following:
let (basis, names_0, names_1) = if let Some(names) = self.arg_name {
(ComparisonBasis::Name, names[0], names[1])
} else {
(ComparisonBasis::File, self.arg_file[0], self.arg_file[1])
};
types:
self.arg_name: Option<[String; 2]>
self.arg_file: Vec<String>
Right now I'm getting errors
cannot move out of type `[std::string::String; 2]`, a non-copy fixed-size array [E0508]
and
cannot move out of indexed content [E0507]
for the two arms of the if
You've omitted a fair amount of context, so I'm taking a guess at a few aspects. I'm also hewing a little closer to the question you asked, rather than the vaguer one implied by your snippets.
struct NeverSpecified {
arg_names: Option<[String; 2]>,
arg_file: Vec<String>,
}
impl NeverSpecified {
fn some_method_i_guess(mut self) -> (String, String) {
if let Some(mut names) = self.arg_names {
use std::mem::replace;
let name_0 = replace(&mut names[0], String::new());
let name_1 = replace(&mut names[1], String::new());
(name_0, name_1)
} else {
let mut names = self.arg_file.drain(0..2);
let name_0 = names.next().expect("expected 2 names, got 0");
let name_1 = names.next().expect("expected 2 names, got 1");
(name_0, name_1)
}
}
}
I use std::mem::replace to switch the contents of the array, whilst leaving it in a valid state. This is necessary because Rust won't allow you to have a "partially valid" array. There are no copies or allocations involved in this path.
In the other path, we have to pull elements out of the vector by hand. Again, you can't just move values out of a container via indexing (this is actually a limitation of indexing overall). Instead, I use Vec::drain to essentially chop the first two elements out of the vector, then extract them from the resulting iterator. To be clear: this path doesn't involve any copies or allocations, either.
As an aside, those expect methods shouldn't ever be triggered (since drain does bounds checking), but better paranoid than sorry; if you want to replace them with unwrap() calls instead, that should be fine..
Since Rust 1.36, you can use slice patterns to bind to all the values of the array at once:
struct NeverSpecified {
arg_names: Option<[String; 2]>,
arg_file: Vec<String>,
}
impl NeverSpecified {
fn some_method_i_guess(mut self) -> (String, String) {
if let Some([name_0, name_1]) = self.arg_names.take() {
(name_0, name_1)
} else {
let mut names = self.arg_file.drain(0..2);
let name_0 = names.next().expect("expected 2 names, got 0");
let name_1 = names.next().expect("expected 2 names, got 1");
(name_0, name_1)
}
}
}
See also:
Method for safely moving all elements out of a generic array into a tuple with minimal overhead

Resources