3

This is my generic class:

open class SMState<T: Hashable>: NSObject, NSCoding {
    open var value: T

    open var didEnter: ( (_ state: SMState<T>) -> Void)?
    open var didExit:  ( (_ state: SMState<T>) -> Void)?

    public init(_ value: T) {
        self.value = value
    }

    convenience required public init(coder decoder: NSCoder) {
        let value = decoder.decodeObject(forKey: "value") as! T

        self.init(value)
    }

    public func encode(with aCoder: NSCoder) {
        aCoder.encode(value, forKey: "value")
    }
}

Then I want to do this:

    let stateEncodeData = NSKeyedArchiver.archivedData(withRootObject: currentState)
    UserDefaults.standard.set(stateEncodeData, forKey: "state")

In my case currentState is of type SMState<SomeEnum>.

But when I call NSKeyedArchiver.archivedData, Xcode (9 beta 5) shows a message in purple saying:

Attempting to archive generic Swift class 'StepUp.SMState<StepUp.RoutineViewController.RoutineState>' with mangled runtime name '_TtGC6StepUp7SMStateOCS_21RoutineViewController12RoutineState_'. Runtime names for generic classes are unstable and may change in the future, leading to non-decodable data.

I am not exactly sure what it tries to say. Is not possible to save a generic object ?

Is there any other way to save a generic custom object ?

edit:

Even if I use AnyHashable instead of generics I get the same error on runtime when calling NSKeyedArchiver.archivedData:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: : unrecognized selector sent to instance
Adrian
  • 19,440
  • 34
  • 112
  • 219

3 Answers3

3

If you want to make the generic class adopt NSCoding and the generic type T is going to be encoded and decoded then T must be one of the property list compliant types.

Property list compliant types are NSString, NSNumber, NSDate and NSData


A possible solution is to create a protocol PropertyListable and extend all Swift equivalents of the property list compliant types to that protocol

The protocol requirements are

  • An associated type.
  • A computed property propertyListRepresentation to convert the value to a property list compliant type.
  • An initializer init(propertyList to do the contrary.

public protocol PropertyListable {
    associatedtype PropertyListType
    var propertyListRepresentation : PropertyListType { get }
    init(propertyList : PropertyListType)
}

Here are exemplary implementations for String and Int.

extension String : PropertyListable {
    public typealias PropertyListType = String
    public var propertyListRepresentation : PropertyListType { return self }
    public init(propertyList: PropertyListType) { self.init(stringLiteral: propertyList) }
}

extension Int : PropertyListable {
    public typealias PropertyListType = Int
    public var propertyListRepresentation : PropertyListType { return self }
    public init(propertyList: PropertyListType) { self.init(propertyList) }
}

Lets declare a sample enum and adopt PropertyListable

enum Foo : Int, PropertyListable {
    public typealias PropertyListType = Int

    case north, east, south, west

    public var propertyListRepresentation : PropertyListType { return self.rawValue }
    public init(propertyList: PropertyListType) {
        self.init(rawValue:  propertyList)!
    }
}

Finally replace your generic class with

open class SMState<T: PropertyListable>: NSObject, NSCoding {
    open var value: T

    open var didEnter: ( (_ state: SMState<T>) -> Void)?
    open var didExit:  ( (_ state: SMState<T>) -> Void)?

    public init(_ value: T) {
        self.value = value
    }

    convenience required public init(coder decoder: NSCoder) {
        let value = decoder.decodeObject(forKey: "value") as! T.PropertyListType
        self.init(T(propertyList: value))
    }

    public func encode(with aCoder: NSCoder) {
        aCoder.encode(value.propertyListRepresentation, forKey: "value")
    }
}

With this implementation you can create an instance and archive it

let currentState = SMState<Foo>(Foo.north)
let stateEncodeData = NSKeyedArchiver.archivedData(withRootObject: currentState)

and unarchive it again

let restoredState = NSKeyedUnarchiver.unarchiveObject(with: stateEncodeData) as! SMState<Foo>
print(restoredState.value)

The whole solution seems to be cumbersome but you have to fulfill the restriction that NSCoding requires property list compliant types. If you don't need a custom type like an enum the implementation is much easier (and shorter).

vadian
  • 274,689
  • 30
  • 353
  • 361
  • It's not clear for me what propertyListRepresentation should do ? – Adrian Aug 20 '17 at 12:16
  • In all mentioned classes in the answer it's supposed to `return self`. In case of the enum return the (property list compliant) raw value. The crucial part of your class is `NSCoder`. The generic type must be supported by `NSCoder`. – vadian Aug 20 '17 at 12:19
  • I still get the same error on runtime. And on compile time still getting the purple warning specified in my question. – Adrian Aug 20 '17 at 12:51
  • I updated the answer. The solution to consider also enums is a bit extensive but it works. – vadian Aug 20 '17 at 16:28
  • The bottleneck is the effort to make `NSCoding` generic. – vadian Aug 20 '17 at 16:52
  • Yep, it's good knowledge what u wrote here, I will award the bounty when it will be eligible. Thanks – Adrian Aug 20 '17 at 16:57
0
open class SMState: NSObject, NSCoding {
    open var value: AnyHashable

    open var didEnter: ( (_ state: SMState) -> Void)?
    open var didExit:  ( (_ state: SMState) -> Void)?

    public init(_ value: AnyHashable) {
        self.value = value
    }

    convenience required public init(coder decoder: NSCoder) {
        let value = decoder.decodeObject(forKey: "value") as! AnyHashable

        self.init(value)
    }

    public func encode(with aCoder: NSCoder) {
        aCoder.encode(value, forKey: "value")
    }
}

Now this SMState class is like SMState<T: Hashable>, you can send any kinds of enum types in this SMState Class.

Then you can use this SMState Class as what you want without the Generic

enum A_ENUM_KEY {
    case KEY_1
    case KEY_2
} 

let stateEncodeData = NSKeyedArchiver.archivedData(withRootObject: currentState)
UserDefaults.standard.set(stateEncodeData, forKey: "state")

In this case, currentState is of type SMState, and SMState.value is SomeEnum, because Any Enums are AnyHashable

pluto
  • 516
  • 6
  • 9
  • Unfortunately I still have the same issue. The app crashes when calling archivedData with the following error: 'Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: : unrecognized selector sent to instance'" – Adrian Aug 18 '17 at 19:10
0

To address "NSInvalidArgumentException', reason: : unrecognized selector sent to instance", make sure the superclass of the class you are trying to archive also extends NSCoder.

Zack
  • 41
  • 3