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
6
  • Make your struct conform to developer.apple.com/documentation/foundation/nscoding. Commented Aug 18, 2017 at 8:25
  • Try to constraint T to NSCoding? Commented Aug 18, 2017 at 8:26
  • @Sweeper: T is an enum, I can't constraint it to NSCoding, because is of "non-class type" Commented Aug 18, 2017 at 8:27
  • If you want to make an enum as T, I suggest you can change all the T to AnyHashable. AnyHashable can suit any kinds of enums, which I think can solve your problem. Use AnyHashable, you can delete the Generic, and then save the object to UserDefaults Commented Aug 18, 2017 at 9:07
  • @pluto: can u provide an example, it will be easier for me to understand Commented Aug 18, 2017 at 10:51

3 Answers 3

3
+150

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).

Sign up to request clarification or add additional context in comments.

6 Comments

It's not clear for me what propertyListRepresentation should do ?
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.
I still get the same error on runtime. And on compile time still getting the purple warning specified in my question.
I updated the answer. The solution to consider also enums is a bit extensive but it works.
The bottleneck is the effort to make NSCoding generic.
|
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

1 Comment

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'"
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.