Since this is something that has to be done time and again (i.e. for every App we create) I was looking for a solution that could be reused by every new App. The solution I came up with is presented below.
The requirements were:
- No "init" phase where the module has to be called to reload the contents. I.e. automatic and autonomous initialisation when necessary.
- Available system wide without the need to pass through a reference from one instance to the next.
- Store the data in a human readable format (in case it must be edited - though editing is of course at the users risk!)
- Provide default values when the file operations fail for whatever reason.
- The storage location (folder) should be configurable.
- The storage location should default to the application support directory when not configured by the programmer.
- It should be fast.
- It should be easy to maintain (add new parameters)
That was a whole list, but with Swift it is easier than ever to fulfil each of these requirements. This is my solution:
final class SessionPersistence {
// MARK: - Session Persistence Parameters
// ===========================================================================================
// When adding members, be sure to also set default values for them in 'writeSessionDefaults'.
//
private static let SHOW_COLUMN = "ShowColumn"
private static var _showColumn: Bool = { return json[SHOW_COLUMN].boolValue ?? true }()
static var showColumn: Bool {
get { return _showColumn }
set {
_showColumn = newValue
json[SHOW_COLUMN].boolValue = newValue
}
}
private static let SHOW_TAGS = "ShowTags"
private static var _showTags: Bool = { return json[SHOW_TAGS].boolValue ?? true }()
static var showTags: Bool {
get { return _showTags }
set {
_showTags = newValue
json[SHOW_TAGS].boolValue = newValue
}
}
//
// When adding members, be sure to also set default values for them in 'writeSessionDefaults'.
// ===========================================================================================
// MARK: - Housekeeping below this line
/// Specifies the path for the directory in which the session persistence file will be created. Note that the application must have write access to this directory and the rights to create this directory (sandbox!). If this variable is to nil, the session persistence file(s) will be written to /Library/Application Support/<<<Application Name>>>/SessionPersistence. Do not use '~' signs in the path, expand them first if necessary.
///
/// Note: When debugging in xcode, the app support directory is in ~/Library/Containers/<<<bundle identifier>>>/Data/Library/Application Support/<<<app name>>>/SessionPersistence.
static var sessionPersistenceDirectoryPath: NSString?
static private var sessionPersistenceDir: NSString? = {
// Get the path to the session persistence directory, prioritize a configured directory, but take the default when it is not supplied.
if let tmp = (sessionPersistenceDirectoryPath ?? applicationSupportSessionPersistenceDirectory) as? String {
let fileManager = NSFileManager.defaultManager()
var error: NSError?
// Make sure the directory exists
if fileManager.createDirectoryAtPath(tmp, withIntermediateDirectories: true, attributes: nil, error: &error) {
return tmp
} else {
let message = "Could not create directory \(tmp), error = " + (error?.localizedDescription ?? "Unknown reason")
log.atLevelError(id: 0, source: "SessionPersistence.sessionPersistenceDir", message: message)
return nil
}
} else {
return nil
}
}()
static private var applicationSupportSessionPersistenceDirectory: String? = {
let fileManager = NSFileManager.defaultManager()
var error: NSError?
// Retrieve the path to the application support directory
if let applicationSupportDirectory =
fileManager.URLForDirectory(
NSSearchPathDirectory.ApplicationSupportDirectory,
inDomain: NSSearchPathDomainMask.UserDomainMask,
appropriateForURL: nil,
create: true,
error: &error
)?.path {
let appName = NSProcessInfo.processInfo().processName
let dirName = applicationSupportDirectory.stringByAppendingPathComponent(appName)
// Add the session persistence directory to the path of the application support directory
return dirName.stringByAppendingPathComponent("SessionPersistence")
} else {
return nil
}
}()
// The primary store for session persistence
static private var json: SwifterJSON = {
var _json: SwifterJSON?
// Try to locate the file with session persistence info
let sessionPersistenceFilePath = SessionPersistence.sessionPersistenceDir?.stringByAppendingPathComponent("SessionPersistence.json")
// If the file exists, try to read the data from it
if sessionPersistenceFilePath != nil {
let (jsonOrNil, errorOrNil) = SwifterJSON.createJSONHierarchyFromFile(sessionPersistenceFilePath!)
if jsonOrNil != nil { _json = jsonOrNil! }
}
// If the file did not exist or it could not be read, then create a new one. Note that this new default object will at some point during app execution overwrite an existing file should there happen to be one.
if _json == nil {
log.atLevelInfo(id: 0, source: "SessionPersistence.json", message: "Creating a new session persistence item")
_json = SwifterJSON()
}
return _json!
}()
static func save() {
let sessionPersistenceFilePath = SessionPersistence.sessionPersistenceDir?.stringByAppendingPathComponent("SessionPersistence.json")
if sessionPersistenceFilePath != nil {
json.writeJSONHierarchyToFile(sessionPersistenceFilePath!)
} else {
log.atLevelError(id: 0, source: "SessionPersistence.save", message: "Could not save session persistence information at path: \(sessionPersistenceFilePath)")
}
}
private init() {} // No need to allow instance creation
}
Looks complex?
It's not all that difficult to understand. Lets start by simply accessing one of the two session persistence bool's (in a window's awakeFromNib method):
toggleableColumn.hidden = !SessionPersistence.showColumn
On the first access to any session persistence parameter, the value will be read from the private value:
private static var _showColumn: Bool = { return json[SHOW_COLUMN].boolValue ?? true }()
static var showColumn: Bool {
get { return _showColumn }
set {
_showColumn = newValue
json[SHOW_COLUMN].boolValue = newValue
}
}
in this case _showColumn. But since this value is used for the first time, the initialisation of that value is performed. I.e. it tries to read a value from the private JSON hierarchy. Since that hierarchy is not yet present, it will execute the getter/constructor for var json:
static private var json: SwifterJSON = {
var _json: SwifterJSON?
....
return json[SHOW_COLUMN].boolValue ?? true
The indirection of using the _showColumn instead of a getter/constructor for showColumn is for performance. The private variable has to access the JSON hierarchy, and that takes more time than simply returning a variable. Hence doing this only once is advantageous. If the getter of the public variable would access the JSON hierarchy, the JSON lookup would have to be performed for every read. The private variable thus provides a kind of caching mechanism. The only drawback is that the setter must update two variables. But imo that is an acceptable price to pay.
Adding more parameters to the session persistence object is easy: repeat the given pattern for any new parameter.
At this point you may think: "But the default JSON parameter is never created?". And that is correct, at least explicitly. It is however implicitly created when the user changes something that warrants updating of the session persistence parameter(s). It is a property of SwifterJSON that any object that is not present is automatically created. As long as a parameter is not present in the JSON file, the private variable returns the default value. But as soon as a session persistence parameter is updated it is also created. And from then on it will be present in the session persistence file as well.
Happy coding
Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH
We don't get the world we wish for... we get the world we pay for.
No comments:
Post a Comment