Swift, help iterating over keyPaths and setting values

108 Views Asked by At

I've found that it's a little easier to explain what I'm doing by giving too much context for why I'm trying to do it, sorry.

I'm currently trying to add an encryption service to my project. It's nothing that'll get published I think though and I've mostly got it working. The problem that I'm having is that I have my model like this

struct Entity {
    // All types are examples though I think they will be all optionals.
    var prop1: String?
    var prop2: Int?
    var prop3: Bool?
    var encryptedData: [Keypath:EncryptedData]

    static var encryptableKeyPaths: [WritableKeyPath<Entity, Any?>]
}

As an example for what's happening, I can get the encryptionService to take in prop1, create an EncryptedData and put it in the encryptedData dictionary. I can even get the keyPath for the property. I can encrypt all the data and decrypt it just fine and get all the values properly, so I don't need help with that. But I'm struggling with 3 issues.

  1. Getting the KeyPaths to be WritableKeyPaths so I can write to them with the values I need.
  2. Setting the properties to nil once the values are encrypted so I'm not storing extra data.
  3. Setting the properties to their values once their decrypted.

All three of these issues seem to revolve around making the KeyPaths into WritableKeyPaths.

This is the closest attempt I've gotten so far. You can copy the following code right into a playground and run it and it should work. Except it'll crash at the end. There are a couple of issues here, I'm losing the type safety as I have to make all the property types Initializable? which isn't great. Also, see that the values are permanently wrapped. I can't figure out how to prevent that. I had to mark Optional as conforming to Initializable to make this work. Lastly, the variable allStoredProperties doesn't let me write to them. I'm not sure how to properly convert it to WritableKeyPath from PartialKeyPath.

import UIKit

protocol Initializable {}

extension String: Initializable {}
extension Int: Initializable {}
extension Bool: Initializable {}
extension Optional: Initializable {}

protocol KeyPathIterable {
    associatedtype Model

    init()

    static var allKeyPaths: [WritableKeyPath<Model, Initializable?>] { get }
}

extension KeyPathIterable {
    var keyPathReadableFormat: [String: Initializable] {
        var description: [String: Initializable] = [:]
        let mirror = Mirror(reflecting: self)
        for case let (label?, value) in mirror.children {
            description[label] = (value as! Initializable)
        }
        return description
    }

    static var allStoredProperties: [PartialKeyPath<Self>] {
        var members: [PartialKeyPath<Self>] = []
        let instance = Self()
        for (key, _) in instance.keyPathReadableFormat {
            members.append(\Self.keyPathReadableFormat[key])
        }
        return members
    }

    static func setValue<Self: KeyPathIterable, T: Initializable>(on root: inout Self,
                                                                  at keyPath: WritableKeyPath<Self, Initializable?>,
                                                                  withValue value: T?) throws {

        root[keyPath: keyPath] = value
    }
}

struct Foo: KeyPathIterable {
    typealias Model = Foo

    var prop1: Initializable? // I want this to be String?
    var prop2: Initializable? // I want this to be Int?
    var prop3: Initializable? // I want this to be Bool?

    init() {
        self.prop1 = nil
        self.prop2 = nil
        self.prop3 = nil
    }

    static var allKeyPaths: [WritableKeyPath<Foo, Initializable?>] {
        return [\Model.prop1, \Model.prop2, \Model.prop3]
    }
}

var foo = Foo()
foo.prop1 = "Bar"
foo.prop2 = 1
foo.prop3 = true

print(foo.prop1 as Any)

let keyPath = \Foo.prop1

foo[keyPath: keyPath] = "Baz"

print(foo.prop1 as Any)

for path in Foo.allStoredProperties {
    print("-=-=-")
    print(path)
}

print("-=-=-=-=-=-=-=-")

do {
    try Foo.setValue(on: &foo, at: keyPath, withValue: "BazBar" as Initializable?)
} catch {
    print("Should never fail")
}

print(foo.prop1 as Any) // Returns Optional(Optional("BarBaz")) - I want this to be all the way unwrapped.
print("--------------")

let values1: [Initializable] = ["Hello World", 100, false]
do {
    for (path, value) in zip(Foo.allKeyPaths, values1) {
        try Foo.setValue(on: &foo,
                         at: path,
                         withValue: value as Initializable?)
    }
} catch {
    print("Success?")
}
print(foo.prop1 as Any)
print(foo.prop2 as Any)
print(foo.prop3 as Any)
print("----====----====----")


let values2: [Initializable] = ["Howdy", 0, false]
do {
    for (path, value) in zip(Foo.allStoredProperties, values2) {
        try Foo.setValue(on: &foo,
                         at: path as! WritableKeyPath<Foo, Initializable?>,
                         withValue: value as Initializable?)
    }
} catch {
    print("Always fails")
}

print("=-=-=-=-=-=-=-=")
print(foo)

I've looked all over google and youtube and everywhere and I can't seem to get this to work. I'm open to a different architecture if that work accomplish my goals better. Just a little frustrated. Thanks for your help.

0

There are 0 best solutions below