Skip to content

Instantly share code, notes, and snippets.

@joshavant
Last active July 14, 2016 07:13
Show Gist options
  • Save joshavant/f68b1fe75650f3a118095c0445a37a68 to your computer and use it in GitHub Desktop.
Save joshavant/f68b1fe75650f3a118095c0445a37a68 to your computer and use it in GitHub Desktop.
PATs example
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
}
}
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)
}
}
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
}
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)
}
}
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