I am working with the property lists for the NSPasteboard
object. The property list is the Dictionary
in the form of this:
var source: Dictionary<String, Any> = ["value": 1, "text": "Text"]
The code above works well.
What I would like to get is this one:
var source: Dictionary<CustomKeySourceType, Any> = [.value: 1, .text: "Text"]
I attempted to build the solution like this one:
class KeySource {
struct Key : Hashable, Equatable, RawRepresentable {
var rawValue: String
static let a = KeySource.Key(rawValue: "a")
static let b = KeySource.Key(rawValue: "b")
static let c = KeySource.Key(rawValue: "c")
public init(_ rawValue: String){
self.rawValue = rawValue
}
public init(rawValue: String){
self.rawValue = rawValue
}
}
}
...
var source: Dictionary<KeySource.Key, Any> = [.a: "a", .b: 1, .c: 3.33]
It compiles the code well, but rise an error in runtime:
var pbd = NSPasteboardItem(pasteboardPropertyList: source, ofType: .experimental)
...*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Could not write property list with invalid format to the pasteboard. The object contains non-property list types: __SwiftValue'...
The explicit usage of the property list permitted types in the code fails with the same error message.
var source: Dictionary<KeySource.Key, Any> = [.a: NSString("a"), .b: NSNumber(1), .c: NSNumber(3.33)]
The NSDictionary
substitution has no positive effect:
var source: NSDictionary = [KeySource.Key.a: NSString("a"), KeySource.Key.b: NSNumber(1), KeySource.Key.c: NSNumber(3.33)]
It produces the same runtime error.
The only way to make it work is to return to the String
key type:
var source: Dictionary<String, Any> = [KeySource.Key.a.rawValue: NSString("a"), KeySource.Key.b.rawValue: NSNumber(1), KeySource.Key.c.rawValue: NSNumber(3.33)]
This solution needs to be lighter. I would like to have keys written briefly, like .a
, .b
, and .c
form.
Please, help me to build right key source!
Xcode 12.4
Deployment target 10.15
You need to pass a property list compatible dictionary, otherwise you'll run into the exception/crash:
A property list is itself an array or dictionary that contains only NSData, NSString, NSArray, NSDictionary, NSDate, and NSNumber objects.
It's not difficult to create one, though, from your higher-level dictionary:
extension Dictionary where Key: RawRepresentable, Key.RawValue == String {
var rawKeys: [Key.RawValue: Value] {
reduce(into: [:]) { $0[$1.key.rawValue] = $1.value }
}
}
With the help of the above extension, you can convert your dictionary at the call site:
NSPasteboardItem(pasteboardPropertyList: source.rawKeys, ofType: .experimental)
Note that you still need to guarantee the validity of the values, in order to make sure they comply with the properly list allowed values.
If you need to also support the reverse - decoding the property list into a typed dictionary, you could implement an extension over NSPasteboardItem
that will handle this:
extension NSPasteboardItem {
func typedPropertyList<Key: RawRepresentable, Value>(forType type: NSPasteboard.PasteboardType) -> [Key: Value]? where Key.RawValue == String {
guard let dict = propertyList(forType: type) as? [String: Value] else { return nil }
return dict.reduce(into: [:]) {
if let wrappedKey = Key(rawValue: $1.key) {
$0[wrappedKey] = $1.value
}
// ramaining question here is how to handle the failure path, we could
// - just ignore it (like above), or,
// - throw an exception, or
// - return nil from the function
}
}
}
But if you do this, then you might want to also overload the NSPasteboardItem
initializer, do you'd have a symmetry for these two operations:
extension NSPasteboardItem {
convenience init?<Key: RawRepresentable, Value>(typedPropertyList: [Key: Value], ofType type: NSPasteboard.PasteboardType) where Key.RawValue == String {
let rawKeys = typedPropertyList.reduce(into: [:]) { $0[$1.key.rawValue] = $1.value }
self.init(pasteboardPropertyList: rawKeys, ofType: type)
}
}
Usage example:
let source: [KeySource.Key: Any] = [.a: "a", .b: 1, .c: 3.33]
let pbd = NSPasteboardItem(typedPropertyList: source, ofType: .experimental)
let decoded: [KeySource.Key: Any]? = pbd?.typedPropertyList(forType: .experimental)
print(source, decoded)