Adrian
Adrian

Reputation: 20068

How to save a generic custom object to UserDefaults?

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

Answers (3)

Zack
Zack

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

vadian
vadian

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

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

Upvotes: 3

pluto
pluto

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

Related Questions