Last active
July 14, 2016 07:13
-
-
Save joshavant/f68b1fe75650f3a118095c0445a37a68 to your computer and use it in GitHub Desktop.
PATs example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
enum ApplicationSupportFileServiceError: ErrorType { | |
case WriteFailed | |
case CouldntGetApplicationSupportDirectoryURL // it would be super weird for this to happen | |
} | |
typealias ApplicationSupportFileServiceKeyType = String // value represents a filename | |
struct ApplicationSupportFileService: KeyedItemPersistenceServiceable { | |
let fileManager: NSFileManager | |
// -Writes `data` to /Library/Application Support/<name> | |
// -Adds NSURLIsExcludedFromBackupKey property to file | |
// | |
// Note: This is sufficient for file sizes that will fit in RAM. If that's | |
// not a good idea, implement a stream. | |
// More info: http://stackoverflow.com/a/32041166/5531248 | |
// | |
func set(data: NSData, key filename: ApplicationSupportFileServiceKeyType) throws { | |
do { | |
let applicationSupportDirectoryURL = try self.createApplicationSupportDirectoryIfNecessary() | |
let fileURL = applicationSupportDirectoryURL.URLByAppendingPathComponent(filename) | |
let success = data.writeToURL(fileURL, atomically: true) | |
if !success { throw ApplicationSupportFileServiceError.WriteFailed } | |
try fileURL.setResourceValue(NSNumber(bool: true), forKey: NSURLIsExcludedFromBackupKey) | |
} catch let error { | |
throw error | |
} | |
} | |
func get(filename: ApplicationSupportFileServiceKeyType) throws -> NSData? { | |
let applicationSupportDirectoryURL = try self.createApplicationSupportDirectoryIfNecessary() | |
let fileURL = applicationSupportDirectoryURL.URLByAppendingPathComponent(filename) | |
return NSData(contentsOfURL: fileURL) | |
} | |
func remove(filename: ApplicationSupportFileServiceKeyType) throws { | |
let applicationSupportDirectoryURL = try self.createApplicationSupportDirectoryIfNecessary() | |
let fileURL = applicationSupportDirectoryURL.URLByAppendingPathComponent(filename) | |
try fileManager.removeItemAtURL(fileURL) | |
} | |
} | |
private extension ApplicationSupportFileService { | |
// returns URL for Application Support directory, as a convenience | |
private func createApplicationSupportDirectoryIfNecessary() throws -> NSURL { | |
do { | |
let applicationSupportDirectoryURL = try self.applicationSupportDirectoryURL() | |
try fileManager.createDirectoryAtURL(applicationSupportDirectoryURL, withIntermediateDirectories: true, attributes: nil) | |
return applicationSupportDirectoryURL | |
} catch let error { | |
throw error | |
} | |
} | |
private func applicationSupportDirectoryURL() throws -> NSURL { | |
guard | |
let applicationSupportDirectoryURL = fileManager.URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask).first | |
else { throw ApplicationSupportFileServiceError.CouldntGetApplicationSupportDirectoryURL } | |
return applicationSupportDirectoryURL | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import KeychainAccess | |
typealias KeychainServiceKeyType = String | |
struct KeychainService: KeyedItemPersistenceServiceable { | |
private let keychain: Keychain | |
init(accessGroupIdentifier identifier: String) { | |
keychain = Keychain(accessGroup: identifier).accessibility(.AfterFirstUnlock) | |
} | |
// MARK: - KeyedItemPersistenceServiceable | |
func set(value: NSData, key: KeychainServiceKeyType) throws { | |
try keychain.set(value, key: key) | |
} | |
func get(key: KeychainServiceKeyType) throws -> NSData? { | |
return try keychain.getData(key) | |
} | |
func remove(key: KeychainServiceKeyType) throws { | |
return try keychain.remove(key) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol KeyedItemPersistenceServiceable { | |
associatedtype ValueType | |
associatedtype KeyType | |
func set(value: ValueType, key: KeyType) throws | |
func get(key: KeyType) throws -> ValueType? | |
func remove(key: KeyType) throws | |
} | |
protocol KeyedItemPersistenceCoordinatable { | |
associatedtype ValueType | |
associatedtype KeyType | |
var key: KeyType { get } | |
// if this returns nil, the item is not persent in the persistent storage | |
func fetchItem() throws -> ValueType? | |
func persistItem(item: ValueType) throws | |
func removeItem() throws | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
enum KeyedNSCodingDataPersistenceCoordinatorError: ErrorType { | |
case ExistingDataCouldNotBeUnarchived | |
} | |
class KeyedNSCodingDataPersistenceCoordinator<I: NSCoding, S: KeyedItemPersistenceServiceable where S.ValueType == NSData>: KeyedItemPersistenceCoordinatable { | |
typealias ValueType = I | |
typealias KeyType = S.KeyType | |
private(set) var key: S.KeyType | |
private(set) var service: S | |
required init(key: S.KeyType, service: S) { | |
self.key = key | |
self.service = service | |
} | |
func fetchItem() throws -> I? { | |
do { | |
if let data = try service.get(key) { | |
var output: I? = nil | |
SwiftTryCatch.`try`({ | |
output = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? I | |
}, catch: nil, finally: nil) | |
if output == nil { | |
assert(false, "Persisted data wasn't able to be unarchived, for some reason. Look into this.") | |
throw KeyedNSCodingDataPersistenceCoordinatorError.ExistingDataCouldNotBeUnarchived | |
} else { | |
return output | |
} | |
} else { | |
return nil | |
} | |
} catch let error { | |
throw error | |
} | |
} | |
func persistItem(item: I) throws { | |
let data = NSKeyedArchiver.archivedDataWithRootObject(item) | |
try service.set(data, key: key) | |
} | |
func removeItem() throws { | |
try service.remove(key) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Thing: NSObject, NSCoding { ... } | |
struct ThingDefaults { | |
// INTERESTING PART: | |
// Here, I can statically change `KeychainService` to `ApplicationSupportFileService` and then | |
// `persistenceCoordinator` will morph to use `ApplicationSupportFileService`'s implementation, purely | |
// based on the configuration of its type. | |
// | |
// This is all because KeyedNSCodingDataPersistenceCoordinator is implemented using type constraints | |
// that allow for those two types to be swapped in. i.e.: | |
// <I: NSCoding, S: KeyedItemPersistenceServiceable where S.ValueType == NSData> | |
// | |
// Now, we get compiler-enforcement of all the polymorphic elements of this design (i.e. ValueType and KeyType), | |
// instead of hoping the dependency implementations will work like that at runtime (as would be the case if you | |
// use Plain Old Objective-C Protocols for something like this). | |
// | |
var persistenceCoordinator: KeyedNSCodingDataPersistenceCoordinator<Thing, KeychainService>! | |
var selectedThing: Thing? { | |
if let thing = try? persistenceCoordinator.fetchItem() { | |
return thing | |
} else { | |
return nil | |
} | |
} | |
func updateSelectedThing(thing: Thing) { | |
do { | |
try persistenceCoordinator.persistItem(thing) | |
} catch { | |
// ... | |
} | |
} | |
func removeSelectedThing() { | |
let _ = try? persistenceCoordinator.removeItem() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment