Reputation: 20068
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
Upvotes: 3
Views: 2014
Reputation: 51
To address "NSInvalidArgumentException', reason: : unrecognized selector sent to instance", make sure the superclass of the class you are trying to archive also extends NSCoder.
Upvotes: 0
Reputation: 285064
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
associated type
.propertyListRepresentation
to convert the value to a property list compliant type.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).
Upvotes: 3
Reputation: 546
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
Upvotes: 0