Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-07-06

Code sample: Persisting a session in the Application Support folder in Swift

Persisting a session is when we save internal App data such that on the next startup of the App it shows itself in the same situation as when we last exited the App. This can be done by saving the internal state into a file at 'AppDelegate.applicationWillTerminate' and to reload the file (and apply the settings) on startup.

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

Note that no other "initialisation" calls are necessary. Just access the necessary value directly. The exclamation mark is a byproduct of the naming, the parameter is called "show...", but its usage is to "hide" hence the inverting. (PS: when the user changes something, simply write the new value to the corresponding session persistence parameter, for example: SessionPersistence.showColumn = true)

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?
        ....

This in turn will start the generation of the directory path in which the JSON file with session persistence data should be stored. If a configured directory is present, it will use that. If not, it will default to the application support directory. If it finds the JSON file, the JSON hierarchy will be created. If not, the internal _json stay's nil and a new _json is created. Note that if anything goes wrong, nothing bad will happen. The internal _showColumn variable will simply return the default value:

    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