NEW: Learn SwiftData for free with my all-new book! >>

BucketList: Possible extension to FileManager

Forums > 100 Days of SwiftUI

Paul mentioned we should think about a generic way to store files to disk using the home directory. Here's what I came up with. I intentionally let the methods throw so that I can deal with the errors "outside" of the call. Of course, wrapping it in do{} catch is possible and return optionals instead, for example. It was not specified for the task.

extension FileManager {

    func documentsDirectory() -> URL {
       self.urls(for: .documentDirectory, in: .userDomainMask)[0]
    }

    // loads a JSON file from the document directory
    func decodeFromFile<T:Decodable>(name: String) throws -> T {
        // construct filename
        let directory = documentsDirectory()
        let fullFileName = directory.appendingPathComponent(name)

        // load file into data
        let data = try Data(contentsOf: fullFileName)

        // decode data
        let decodedJson = try JSONDecoder().decode(T.self, from: data)

        // return decoded data
        return decodedJson
    }

    // stores a JSON file to the documents directory
    func encodeToFile<T:Encodable>( name: String, content : T ) throws {
        // encode to data
        let jsonEncoded = try JSONEncoder().encode(content)

        // build filename
        let directory = documentsDirectory()
        let fullFileName = directory.appendingPathComponent(name)

        // write data to file
        try jsonEncoded.write(to: fullFileName, options: .atomic)
    }
}

1      

Here mine. Looking forward to see how Paul does it.

extension FileManager {
    private func getDocumentsDirectory() -> URL {
        let paths = self.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }

    func encode<T: Encodable>(_ input: T, to file: String) {
        let url = getDocumentsDirectory().appendingPathComponent(file)
        let encoder = JSONEncoder()

        do {
            let data = try encoder.encode(input)
            let jsonString = String(decoding: data, as: UTF8.self)
            try jsonString.write(to: url, atomically: true, encoding: .utf8)
        } catch {
            fatalError("Failed to write to Documents \(error.localizedDescription)")
        }
    }

    func decode<T: Decodable>(_ type: T.Type,
                              from file: String,
                              dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate,
                              keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys
    ) -> T {
        let url = getDocumentsDirectory().appendingPathComponent(file)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = dateDecodingStrategy
        decoder.keyDecodingStrategy = keyDecodingStrategy

        do {
            let data = try Data(contentsOf: url)
            let loaded = try decoder.decode(T.self, from: data)
            return loaded
        } catch {
            fatalError("Failed to decode \(file) from directory \(error.localizedDescription)")
        }
    }
}

Call site

//To write to Documents
let user = User(name: "Nigel")
FileManager.default.encode(user, to: "message.txt")

//To fetch from Documents
let fetchUser = FileManager.default.decode(User.self, from: "message.txt")
print(fetchUser.name)

2      

Great solution as well. You catch all the potential errors and throw fatal errors assuming that things "should be in place". I picked the approach that the methods are flexible for use cases when the file might or might not be there and the program does not fail.

However, I see one clear difference I would like to ask your input.

You do not write the data into the file but convert it to String. Is there any reason to a String being beneficial? I see it more as another "variable" to be aware of as you add UTF8 as a magic constant, for example.

1      

@Bnerd  

Can someone please explain how the above (either solution) can work if the message.txt already exists? i.e. it was saved using

.write(to: url, atomically: true, encoding: .utf8)

as Paul did in his demonstration.

   

@Bnerd  

Found it in case anyone was wondering the same..my code (it's for loading only):

extension FileManager {
    func loadTextFromFile<T: Codable>(_ file: String) -> T { 
        // Url to our file
        let url = self.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(file)

        //Encoding to JSON
        let encoder = JSONEncoder()

        guard let data = try? encoder.encode(String(contentsOf: url)) else {
            fatalError("Could not encode to JSON String")
        }

        let decoder = JSONDecoder()
        //Decoding from Json
        guard let loaded = try? decoder.decode(T.self, from: data) else { //DECODE the DATA from JSON format into a T
            fatalError("Failed to decode \(file).")
        }
        return loaded
    }
}

And is being called like this:

ar body: some View {
        Text("Welcome to Bucket List")
            .onTapGesture {

                //***Challenge Day 68 code.***
                let x: (String) = FileManager.default.loadTextFromFile("message.txt")
        }
    }

1      

2 years later, did paul ever present a solution here?

1      

Hacking with Swift is sponsored by Judo

SPONSORED Let’s face it, SwiftUI previews are limited, slow, and painful. Judo takes a different approach to building visually—think Interface Builder for SwiftUI. Build your interface in a completely visual canvas, then drag and drop into your Xcode project and wire up button clicks to custom code. Download the Mac App and start your free trial today!

Try now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.