Peculiar disconnect between storing and reading data
Caveat: I'm normally a Perl programmer and I'm new to Swift. So many of the assertions here may be wrong. OTOH, this means this is a new / outside opinion you might not have heard of!
If I have an object that implements CreateableSecureStorable, I just have to say e.g.
class Thing: ... {
var data: [String: AnyObject] {
return ["username": username!, "password": password!]
}
// ...
func store() {
self.createInSecureStore()
}
}
and all of this will be fished out of my object for me.
If I want to read all of this again, though, I believe I have to say e.g.
func fetch() -> Thing? {
guard let result = self.readFromSecureStore() else { return nil }
var newThing = Thing()
newThing.username = result?.username
newThing.password = result?.password
return newThing
}
If I have more than a trivial amount of things to store this is going to get tiresome very quickly.
Now, if I define data as having a setter I believe I can say this instead:
var data: [String: AnyObject] {
get {
return ["username": username!, "password": password!]
}
set(keychainData) {
self.username = keychainData["username"] as? String
self.password = keychainData["password"] as? String
}
}
func fetch() -> Thing? {
guard let result = self.readFromSecureStore() else { return nil }
let newThing = Thing()
newThing.data = result.data!
return newThing
}
This is at least somewhat better: the two bits of code that convert between the dictionary storage and the object's internal representation are now right beside each other, rather than split between two different parts of the source code.
But it still involves a fair amount of cut and paste.
Ideally I should be able to say e.g. (this is now bordering on pseudo-code)
func mapping() -> [String: AnyObject] {
return [
"username": &username,
"password": &password,
]
}
and all of the serialisation / deserialisation code would know what to pick out of / stuff into a data structure like this. But I image this would thwart a lot of the Swift type-strictness.
I spent some time a while back on this in the Alamofire project (an http manager). The struct-to-dictionary conversion could be done trivially using Mirror, but afaik there is no way to set a struct's property given a string of its name. This is possible with classes vs. structs on iOS/Mac using the Objective-C runtime, but that wouldn't work here.
I'd love to be corrected on this if I'm wrong.
Really interesting points raised. Will have to do a bit more reading on Mirror too—looks useful.
I don't know if it would actually work in the end, or if it solves the problem at hand, but I've been thinking that generics might be able to help us out with this problem.
This does just move the problem of the duplicated serialize/deserialize code to ThingCredentials, but that might be helpful since the user then gets to specify how ThingCredentials works
protocol Serializable {
// (note [String: AnyObject] probably wouldn't end up being the type... probably NSData or something lower)
func serialize() -> [String: AnyObject]
init(sourceData: [String: AnyObject])
}
protocol CreateableSecureStorable {
typealias DataType: Serializable
var data: DataType
}
struct ThingCredentials: Serializable {
let token: String
init(token: String) {
self.token = token
}
init(sourceData: [String: AnyObject]) {
self.token = sourceData["token"]
}
func serialize() -> [String: AnyObject] {
return ["token": token]
}
}
struct Thing: CreateableSecureStorable, Readable {
typealias DataType = ThingCredentials
init?(credentials: ThingCredentials) {
// However you'd init with credentials
}
func fetch() -> Thing? { // (depending on changes to how reading works)
let result = self.readFromSecureStore // Constructs a result type with data set to the result of init(sourceData:)
let data = result.data // type is ThingCredentials
return self.init(credentials: data) // (or maybe you add a method to ThingCredentials to construct a Thing)
}
}
I have it done this way:
protocol MyServiceCredentialsKeychainAccount: GenericPasswordSecureStorable {
}
extension MyServiceCredentialsKeychainAccount {
var service: String { return "MyService" }
var account: String { return "rehsals" }
}
struct MyServiceCredentials: CreateableSecureStorable, DeleteableSecureStorable, MyServiceCredentialsKeychainAccount {
let password: String
var data: [String: AnyObject] {
get {
return ["password": password]
}
}
static var credentials: MyServiceCredentials? {
let deserializator = CospaceCredentialsDeserializator()
return deserializator.deserialize()
}
}
struct MyServiceCredentialsDeserializator: ReadableSecureStorable, MyServiceCredentialsKeychainAccount {
func deserialize() -> MyServiceCredentials? {
guard let resultData = readFromSecureStore()?.data,
let password = resultData["password"] as? String else {
return nil
}
return MyServiceCredentials(password: password)
}
}