Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Helper class to easily store and retrieve Codable structs from/to disk. https://medium.com/@sdrzn/swift-4-codable-lets-make-things-even-easier-c793b6cf29e1
import Foundation
public class Storage {
fileprivate init() { }
enum Directory {
// Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud.
case documents
// Data that can be downloaded again or regenerated should be stored in the <Application_Home>/Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.
case caches
}
/// Returns URL constructed from specified directory
static fileprivate func getURL(for directory: Directory) -> URL {
var searchPathDirectory: FileManager.SearchPathDirectory
switch directory {
case .documents:
searchPathDirectory = .documentDirectory
case .caches:
searchPathDirectory = .cachesDirectory
}
if let url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first {
return url
} else {
fatalError("Could not create URL for specified directory!")
}
}
/// Store an encodable struct to the specified directory on disk
///
/// - Parameters:
/// - object: the encodable struct to store
/// - directory: where to store the struct
/// - fileName: what to name the file where the struct data will be stored
static func store<T: Encodable>(_ object: T, to directory: Directory, as fileName: String) {
let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
let encoder = JSONEncoder()
do {
let data = try encoder.encode(object)
if FileManager.default.fileExists(atPath: url.path) {
try FileManager.default.removeItem(at: url)
}
FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
} catch {
fatalError(error.localizedDescription)
}
}
/// Retrieve and convert a struct from a file on disk
///
/// - Parameters:
/// - fileName: name of the file where struct data is stored
/// - directory: directory where struct data is stored
/// - type: struct type (i.e. Message.self)
/// - Returns: decoded struct model(s) of data
static func retrieve<T: Decodable>(_ fileName: String, from directory: Directory, as type: T.Type) -> T {
let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
if !FileManager.default.fileExists(atPath: url.path) {
fatalError("File at path \(url.path) does not exist!")
}
if let data = FileManager.default.contents(atPath: url.path) {
let decoder = JSONDecoder()
do {
let model = try decoder.decode(type, from: data)
return model
} catch {
fatalError(error.localizedDescription)
}
} else {
fatalError("No data at \(url.path)!")
}
}
/// Remove all files at specified directory
static func clear(_ directory: Directory) {
let url = getURL(for: directory)
do {
let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
for fileUrl in contents {
try FileManager.default.removeItem(at: fileUrl)
}
} catch {
fatalError(error.localizedDescription)
}
}
/// Remove specified file from specified directory
static func remove(_ fileName: String, from directory: Directory) {
let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
if FileManager.default.fileExists(atPath: url.path) {
do {
try FileManager.default.removeItem(at: url)
} catch {
fatalError(error.localizedDescription)
}
}
}
/// Returns BOOL indicating whether file exists at specified directory with specified file name
static func fileExists(_ fileName: String, in directory: Directory) -> Bool {
let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
return FileManager.default.fileExists(atPath: url.path)
}
}
@ZhangYiJiang

This comment has been minimized.

Copy link

commented Feb 12, 2018

Since https://developer.apple.com/documentation/foundation/filemanager/1410695-createfile says that createFile(atPath:contents:attributes:) will overwrite any existing files, the fileExists + removeFile check at line 46 is unnecessary, no?

@TheBasicMind

This comment has been minimized.

Copy link

commented Jun 8, 2018

Saoud, I would suggest defining each method containing a fatalError instruction as throwable and throwing/re-throwing any errors or failures encountered. Persistence, obviously, is used to persist data between sessions and as the code stands any issues in saving or file system errors are guaranteed to be greeted with a repeatedly crashing app or process that is unlikely to make it out of initialisation (when opening persisted files will tend to happen).

@reimond

This comment has been minimized.

Copy link

commented Oct 17, 2018

Great code but also agree about throwing an error rather than fatal error

@reimond

This comment has been minimized.

Copy link

commented Oct 17, 2018

I hope you don't mind my changes

class StorageHelper{
    
    //MARK: - Variables
    enum StorageHelperError:Error{
        case error(_ message:String)
    }
    
    enum Directory {
        // Only documents and other data that is user-generated, or that cannot otherwise be recreated by your application, should be stored in the <Application_Home>/Documents directory and will be automatically backed up by iCloud.
        case documents
        
        // Data that can be downloaded again or regenerated should be stored in the <Application_Home>/Library/Caches directory. Examples of files you should put in the Caches directory include database cache files and downloadable content, such as that used by magazine, newspaper, and map applications.
        case caches
    }
    
    
    
    //MARK: - Functions
    /** Store an encodable struct to the specified directory on disk
    *  @param object      The encodable struct to store
    *  @param directory   Where to store the struct
    *  @param fileName    What to name the file where the struct data will be stored
    **/
    static func store<T: Encodable>(_ object: T, to directory: Directory, as fileName: String) throws {
        
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        
        let encoder = JSONEncoder()
        do {
            let data = try encoder.encode(object)
            if FileManager.default.fileExists(atPath: url.path) {
                try FileManager.default.removeItem(at: url)
            }
            FileManager.default.createFile(atPath: url.path, contents: data, attributes: nil)
        }
        catch {
            throw(error)
        }
        
    }
    
    /** Retrieve and convert an Object from a file on disk
    *  @param fileName    Name of the file where struct data is stored
    *  @param directory   Directory where Object data is stored
    *  @param type        Object type (i.e. Message.self)
    *  @return decoded    Object model(s) of data
    **/
    static func retrieve<T: Decodable>(_ fileName: String, from directory: Directory, as type: T.Type) throws -> T{
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        
        if !FileManager.default.fileExists(atPath: url.path) {
            throw StorageHelperError.error("No data at location: \(url.path)")
        }
        
        if let data = FileManager.default.contents(atPath: url.path) {
            let decoder = JSONDecoder()
            do {
                let model = try decoder.decode(type, from: data)
                return model
            } catch {
                throw(error)
            }
        }
        else {
            throw StorageHelperError.error("No data at location: \(url.path)")
        }
    }
    
    /** Remove all files at specified directory **/
    static func clear(_ directory: Directory) throws {
        
        let url = getURL(for: directory)
        do {
            let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
            for fileUrl in contents {
                try FileManager.default.removeItem(at: fileUrl)
            }
        }
        catch {
            throw(error)
        }
        
    }
    
    /** Remove specified file from specified directory **/
    static func remove(_ fileName: String, from directory: Directory) throws {
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        if FileManager.default.fileExists(atPath: url.path) {
            do {
                try FileManager.default.removeItem(at: url)
            } catch {
                throw(error)
            }
        }
    }
    
    
    //MARK: Helpers
    /** Returns BOOL indicating whether file exists at specified directory with specified file name **/
    static fileprivate func fileExists(_ fileName: String, in directory: Directory) -> Bool {
        
        let url = getURL(for: directory).appendingPathComponent(fileName, isDirectory: false)
        
        return FileManager.default.fileExists(atPath: url.path)
    
    }
    
    /** Returns URL constructed from specified directory **/
    static fileprivate func getURL(for directory: Directory) -> URL {
        
        var searchPathDirectory: FileManager.SearchPathDirectory
        
        switch directory {
        case .documents:
            searchPathDirectory = .documentDirectory
        case .caches:
            searchPathDirectory = .cachesDirectory
        }
        
        if let url = FileManager.default.urls(for: searchPathDirectory, in: .userDomainMask).first {
            return url
        } else {
            fatalError("Could not create URL for specified directory!")
        }
        
    }
    
}
@brunomunizaf

This comment has been minimized.

Copy link

commented May 1, 2019

I'd love to see this thread-safe :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.