kangarooChris kangarooChris - 6 months ago 25
Swift Question

I want to write a string to a local file using writeToURL in swift

I have a cards array with text in the front and back. now I want to store this data in an csv file, which I created and added to my project. The following code creates the array of strings separated by a comma and then tries to write it to the file. But the file remains empty. There are no errors showing in the code. Any idea what I am doing wrong here?

let path = NSBundle.mainBundle().pathForResource("cardsFile", ofType:"csv")
for i in 0...cards.count - 1{
let card = [cards[i].front,cards[i].back]
let cardRow:String = card.joinWithSeparator(",")
print(cardRow)///confirmed - carRow is not empty
do {
try cardRow.writeToURL(NSURL(fileURLWithPath: path!), atomically: false, encoding: NSUTF8StringEncoding)
}
catch {}
}

Rob Rob
Answer

No errors are showing because you're catching the error and not doing anything. Add print(error) in the catch block:

do {
    try cardRow.writeToURL(NSURL(fileURLWithPath: path!), atomically: false, encoding: NSUTF8StringEncoding)
}
catch {
    print(error)
}

The issue is that you're trying to write to a file in the bundle, but the bundle is read-only. You should try writing to the Documents folder:

let fileURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false)
    .URLByAppendingPathComponent("cardsFile.csv")

By the way, writeToFile will replace the output file. If you want to write data to a file, appending data as you go along, you might want to use NSOutputStream:

let fileURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false).URLByAppendingPathComponent("cardsFile.csv")

guard let stream = NSOutputStream(URL: fileURL, append: false) else {
    print("unable to open file")
    return
}

stream.open()

for card in cards {
    let data = "\(card.front),\(card.back)\n".dataUsingEncoding(NSUTF8StringEncoding)!
    guard stream.write(UnsafePointer<UInt8>(data.bytes), maxLength: data.length) > 0 else {
        print("unable to write to file")
        break
    }
}

stream.close()

If the goal is to just save this locally, I wouldn't use a CSV format. I'd use a native format, such as a NSKeyedArchiver instead. So, first making Card conform to the NSCoding protocol by implementing init?(coder:) and encodeWithCoder():

class Card: NSObject, NSCoding {
    let front: String
    let back: String

    init(front: String, back: String) {
        self.front = front
        self.back = back
        super.init()
    }

    required convenience init?(coder aDecoder: NSCoder) {
        guard let front = aDecoder.decodeObjectForKey("front") as? String else { return nil }
        guard let back = aDecoder.decodeObjectForKey("back") as? String else { return nil }
        self.init(front: front, back: back)
    }

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(front, forKey: "front")
        aCoder.encodeObject(back, forKey: "back")
    }

    override var description: String { get { return "<Card front=\(front); back=\(back)>" } }
}

Then, when you want to save:

let fileURL = try! NSFileManager.defaultManager().URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false)
    .URLByAppendingPathComponent("cardsFile.bplist")

if !NSKeyedArchiver.archiveRootObject(cards, toFile: fileURL.path!) {
    print("error saving archive")
}

And when you want to read it from the file:

guard let cards2 = NSKeyedUnarchiver.unarchiveObjectWithFile(fileURL.path!) as? [Card] else {
    print("problem reading archive")
    return
}

print(cards2)