2

In connection with my previous questions, I decided to subclass NSArrayController in order to achieve the desired behavior.

class NSPresetArrayController: NSArrayController {
    override func addObject(_ object: Any) {
        if let preset = object as? Preset {
            super.addObject(["name": preset.name, "value": preset.value])
        } else {
            super.addObject(object)
        }
    }
}

This works, but what if I wanted something that works for any Encodable class, and not just one with two properties called name and value?

Basically, the problem is creating a dictionary from a class, where the keys are the property names, and the values are the values of these properties.

I tried writing something like this:

class NSPresetArrayController: NSArrayController {
    override func addObject(_ object: Any) {
        if let encodableObject = object as? Encodable {
            let data = try! PropertyListEncoder().encode(encodableObject)
            let any = try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)

            super.addObject(any)
        }
    }
}

However, I get a compile error:

Cannot invoke 'encode' with an argument list of type '(Encodable)'
1. Expected an argument list of type '(Value)'

How do I fix this so it compiles?

jscs
  • 63,694
  • 13
  • 151
  • 195
swineone
  • 2,296
  • 1
  • 18
  • 32
  • This boils down to casting the object from `Any` to the "real" type...but I honestly don't know how to do that. I'm sure someone does, though. Assuming that it's possible. – jscs Jan 27 '19 at 20:19
  • 1
    You cannot fix this. The argument of `encode` must be a concrete type, not a protocol. Cocoa Bindings which are widely related to Objective-C and Swift generics don't work together. You have to find a way without bindings or without generics. – vadian Jan 27 '19 at 20:21
  • Well, you could statically enumerate the types you will be encoding... :barf: – jscs Jan 27 '19 at 20:24

2 Answers2

3

The problem is that protocols don't always conform to themselves. PropertyListEncoder's encode(_:) method expects a Value : Encodable argument:

func encode<Value : Encodable>(_ value: Value) throws -> Data

However the Encodable type itself is currently unable to satisfy this constraint (but it might well do in a future version of the language).

As explored in the linked Q&A (and also here), one way to work around this limitation is to open the Encodable value in order to dig out the underlying concrete type, which we can substitute for Value. We can do this with a protocol extension, and use a wrapper type in order to encapsulate it:

extension Encodable {
  fileprivate func openedEncode(to container: inout SingleValueEncodingContainer) throws {
    try container.encode(self)
  }
}

struct AnyEncodable : Encodable {
  var value: Encodable
  init(_ value: Encodable) {
    self.value = value
  }
  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    try value.openedEncode(to: &container)
  }
}

Applied to your example:

class NSPresetArrayController : NSArrayController {
  override func addObject(_ object: Any) {
    guard let object = object as? Encodable else { 
      // Not encodable, maybe do some error handling.
      return 
    }
    do {
      let encoded = try PropertyListEncoder().encode(AnyEncodable(object))
      let cocoaPropertyList = try PropertyListSerialization.propertyList(from: encoded, format: nil)
      super.addObject(cocoaPropertyList)
    } catch {
      // Couldn't encode. Do some error handling.
    }
  }
}
Hamish
  • 78,605
  • 19
  • 187
  • 280
0

The type of the value that you pass to encode(_:) has to be a concrete type that implements Encodable. This means you need to recover the object's real type from the Any that you have. In order to cast, you must have a statically-specified type to which you are casting. You can't say object as! type(of: object), in other words; you have to say object as? MyClass (or in a generic context you can say object as? T).

Therefore, I believe that the only way to get around this is to statically enumerate the types you are working with, like so:

import Foundation

struct S : Encodable {
    let i: Int
}

struct T : Encodable {
    let f: Float
}

struct U : Encodable {
    let b: Bool
}

func plistObject(from encodable: Any) -> Any? {
    let encoded: Data?
    switch encodable {
        case let s as S:
            encoded = try? PropertyListEncoder().encode(s)
        case let t as T:
            encoded = try? PropertyListEncoder().encode(t)
        case let u as U:
            encoded = try? PropertyListEncoder().encode(u)
        default:
            encoded = nil
    }

    guard let data = encoded else { return nil }

    return try? PropertyListSerialization.propertyList(from: data,
                                                       options: [],
                                                       format: nil)
}

Needless to say, this is rather gross. It's inflexible, repetitive boilerplate. I'm not sure I can actually recommend its use. It's an answer to the literal question, not necessarily a solution to the problem.

jscs
  • 63,694
  • 13
  • 151
  • 195
  • You can open the `Encodable` value using a protocol extension in order to get the underlying concrete type with which to satisfy the generic placeholder, see my answer :) – Hamish Jan 28 '19 at 14:07