3

I have a FileHelper class where I've implemented 3 methods whose job is to write a Dictionary contents to a file. Those methods are:

func storeDictionary(_ dictionary: Dictionary<String, String>, inFile fileName: String, atDirectory directory: String) -> Bool {
    let ext = "txt"
    let filePath = createFile(fileName, withExtension: ext, atDirectory: directory)
    /**** //If I use this method, file is created and dictionary is saved
    guard (dictionary as NSDictionary).write(to: filePath!, atomically: true) else {
        return false
    }
    */
    guard NSKeyedArchiver.archiveRootObject(dictionary, toFile: (filePath?.absoluteString)!) else {
        return false
    }
    return true
}
func createFile(_ file: String, withExtension ext: String, atDirectory directory: String) -> URL? {
    let directoryPath = createDirectory(directory)
    let filePath = directoryPath?.appendingPathComponent(file).appendingPathExtension(ext)

    if !FileManager.default.fileExists(atPath: (filePath?.absoluteString)!) {
        let success = FileManager.default.createFile(atPath: (filePath?.absoluteString)!, contents: nil, attributes: nil)
        print("\(success)") //** here is the issue I investigated. Always prints false.
    }

    return filePath
}
func createDirectory(_ directory: String) -> URL? {
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let directoryPath = documentsDirectory.appendingPathComponent(directory)

    do {
        try FileManager.default.createDirectory(at: directoryPath, withIntermediateDirectories: true, attributes: nil)
    } catch let error as NSError {
        fatalError("Error creating directory: \(error.localizedDescription)")
    }
    return directoryPath
}

When I call FileHelper().storeDictionary(aValidDictionary, inFile: "abc", atDirectory: "XYZ") to write the dictionary, it fails with this procedure. But if I use

guard (dictionary as NSDictionary).write(to: filePath!, atomically: true) else {
    return false
}

it works.

What's wrong with NSKeyedArchiver.archiveRootObject(_:toFile:) method??

And why FileManager.default.createFile(atPath: (filePath?.absoluteString)!, contents: nil, attributes: nil) always returns false?

nayem
  • 7,285
  • 1
  • 33
  • 51

2 Answers2

7

First of all filePath?.absoluteString returns the entire – even percent escaped – string including the file:// scheme and the method expects a path without the scheme (filePath?.path - the naming is a bit confusing ;-) ).

I recommend to save a [String:String] dictionary as property list file. It's not necessary to create the file explicitly.

I changed the signatures of the methods slightly in the Swift-3-way. Further there is no need to use any optional type.

func store(dictionary: Dictionary<String, String>, in fileName: String, at directory: String) -> Bool {
    let fileExtension = "plist"
    let directoryURL = create(directory:directory)
    do {
        let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0)
        try data.write(to: directoryURL.appendingPathComponent(fileName).appendingPathExtension(fileExtension))
        return true
    }  catch {
        print(error)
        return false
    }
}

func create(directory: String) -> URL {
    let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let directoryURL = documentsDirectory.appendingPathComponent(directory)

    do {
        try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
    } catch let error as NSError {
        fatalError("Error creating directory: \(error.localizedDescription)")
    }
    return directoryURL
}

PS: Instead of returning a Bool you could make the store method can throw and handle the error in the calling method:

func store(dictionary: Dictionary<String, String>, in fileName: String, at directory: String) throws {
    let fileExtension = "plist"
    let directoryURL = create(directory:directory)

    let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0)
    try data.write(to: directoryURL.appendingPathComponent(fileName).appendingPathExtension(fileExtension))
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • Well, I understand. But what you meant by **filePath?.path - the naming is a bit confusing ;-)**, I didn't get that – nayem Mar 15 '17 at 12:57
  • Your `filePath` is actually `fileURL`, an `(NS)URL` instance. A `path` implies to be `(NS)String`. – vadian Mar 15 '17 at 12:59
  • Oh, my bad. Being silly . Another thing I want to know **is removing `_` from the method signature _Swift-3_ way?** Then when to use `_`s? – nayem Mar 15 '17 at 13:05
  • You have to pass `_` if you want to ignore the corresponding (external) parameter label. However the Swift 3 convention is to pass all parameter labels. Sometimes they are ignored for Objective-C compatibility reasons. – vadian Mar 15 '17 at 13:08
  • Glad to know that it's (`_`) basically used for the persons who are coming from Objective-C background to make themselves feel like Objective-C conventions. Right? – nayem Mar 15 '17 at 13:13
  • Sort of. But that's not the primary purpose. – vadian Mar 15 '17 at 13:14
  • One issue. `FileManager.default.fileExists(atPath: fileURL.absoluteString)` or `FileManager.default.fileExists(atPath: fileURL.path)` always returns false. I think the method signature `fileExists(atPath:)` stands for `fileDoesNotExist(atPath:)`. **Reverse of conventional way**. Can you take a look? – nayem Mar 20 '17 at 10:02
  • `fileExists(atPath` returns `true` if the file exists. But using my suggestion you don't need to create the file explicitly. Once again, never use `absoluteString` in the local file system. – vadian Mar 20 '17 at 10:15
  • Yes. That problem was solved. But I got into a new problem. That is, I tried to load my data and print it. For that reason, I tried `fileExists(atPath:)` method. But it seems, the method always returns false irrespective of whether the file actually exists in that location or not. I think this is a bug in _Swift 3_ – nayem Mar 20 '17 at 10:18
  • It's certainly not a bug. Consider that absolute paths point to different locations in sandboxed and non-sandboxed apps. – vadian Mar 20 '17 at 10:23
2

Here's a swift 5 extension that should save any Dictionary where the Key and Value are Codable

extension Dictionary where Key: Codable, Value: Codable {
    static func load(fromFileName fileName: String, using fileManager: FileManager = .default) -> [Key: Value]? {
        let fileURL = Self.getDocumentsURL(on: .cachesDirectory, withName: fileName, using: fileManager)
        guard let data = fileManager.contents(atPath: fileURL.path) else { return nil }
        do {
            return try JSONDecoder().decode([Key: Value].self, from: data)
        } catch(let error) {
            print(error)
            return nil
        }
    }

    func saveToDisk(on directory: FileManager.SearchPathDirectory,
                    withName name: String,
                    using fileManager: FileManager = .default) throws {

        let fileURL = Self.getDocumentsURL(on: .cachesDirectory, withName: name, using: fileManager)
        let data = try JSONEncoder().encode(self)
        try data.write(to: fileURL)
    }

    private static func getDocumentsURL(on directory: FileManager.SearchPathDirectory,
                                 withName name: String,
                                 using fileManager: FileManager) -> URL {

        let folderURLs = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)
        let fileURL = folderURLs[0].appendingPathComponent(name)
        return fileURL
    }
}

Usage:

let myDict = [MyKey: MyValue].load(from: diskDirectory, andFileName: diskFileName) // load
try myDict.saveToDisk(on: diskDirectory, withName: diskFileName) // save
Declan McKenna
  • 4,321
  • 6
  • 54
  • 72