0

I am creating the app, where I need to have a list of recent 10 searches and save it (to show consistent info between app launches).

In order to that I have created my implementation of linked list (LinkedList and Node classes) and some kind of wrapper class which maintain it to be list of recent 10 strings. I made all of these 3 classes conform NSCoding protocol and it works when it is time to save it as NSUserDefaults. Unfortunately when I try to load it, app crashes with error:

Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_TtGC26Informacje_o_organizacjach4NodeSS_) for key (head); the class may be defined in source code or a library that is not linked'

That's the code of all 3 classes:

class Node

public class Node<T>: NSObject, NSCoding {
var value: T

var next: Node<T>?
var previous: Node<T>?


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

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

    next = aDecoder.decodeObject(forKey: "next") as? Node<T>
    previous = aDecoder.decodeObject(forKey: "previous") as? Node<T>
}

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

    aCoder.encode(next, forKey: "next")
    aCoder.encode(previous, forKey: "previous")
}
}

class LinkedList

public class LinkedList<T>: NSObject, NSCoding {
fileprivate var head: Node<T>?
private var tail: Node<T>?

override init() {
    head = nil
    tail  = nil
}

public var isEmpty: Bool {
    return head == nil
}

public var first: Node<T>? {
    return head
}

public var last: Node<T>? {
    return tail
}

public var count: Int {
    var node = head
    var count = 0

    while node != nil {
        count = count + 1
        node = node?.next
    }

    return count
}

public func removeLast() {
    if let lastNode = last {
        remove(node: lastNode)
    }
}

public func appendFirst(value: T) {
    let newNode = Node(value: value)

    if let headNode = head {
        headNode.previous = newNode
        newNode.next = headNode
    } else {
        tail = newNode
    }

    head = newNode
}

public func append(value: T) {
    let newNode = Node(value: value)

    if let tailNode = tail {
        newNode.previous = tailNode
        tailNode.next = newNode
    } else {
        head = newNode
    }

    tail = newNode
}

public func nodeAt(index: Int) -> Node<T>? {
    if index >= 0 {
        var node = head
        var i = index

        while node != nil {
            if i == 0 { return node }
            i -= 1
            node = node!.next
        }
    }

    return nil
}

public func removeAll() {
    head = nil
    tail = nil
}

public func remove(node: Node<T>) -> T {
    let prev = node.previous
    let next = node.next

    if let prev = prev {
        prev.next = next
    } else {
        head = next
    }

    next?.previous = prev

    if next == nil {
        tail = prev
    }

    node.previous = nil
    node.next = nil

    return node.value
}

public required init?(coder aDecoder: NSCoder) {
    head = aDecoder.decodeObject(forKey: "head") as? Node<T>
    tail = aDecoder.decodeObject(forKey: "tail") as? Node<T>
}

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

class Recents

public class Recents: NSObject, NSCoding {
fileprivate var list: LinkedList<String>

override init() {
    list = LinkedList<String>()
}

public func enqueue(_ element: String) {
    if let node = search(for: element) {
        list.remove(node: node)
    } else {
        if list.count >= 10 {
            list.removeLast()
        }
    }

    list.appendFirst(value: element)
}

func search(for value: String) -> Node<String>? {
    var curr = list.first

    while curr != nil {
        if curr?.value == value {
            return curr
        }

        curr = curr?.next
    }

    return nil
}

public func count() -> Int {
    return list.count
}

public func nodeAt(index: Int) -> String {
    return list.nodeAt(index: index)!.value
}

public var isEmpty: Bool {
    return list.isEmpty
}

public required init(coder aDecoder: NSCoder) {
    list = aDecoder.decodeObject(forKey: "list") as! LinkedList<String>
}

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


I use this code to load and save data into NSUserDefaults:

    func saveRecents() {
        let savedData = NSKeyedArchiver.archivedData(withRootObject: recents)
        let defaults = UserDefaults.standard
        defaults.set(savedData, forKey: "recents")
    }

    func loadRecents() {
        let defaults = UserDefaults.standard

        if let savedRecents = defaults.object(forKey: "recents") as? Data {
            recents = NSKeyedUnarchiver.unarchiveObject(with: savedRecents) as! Recents
        }
    }

Where's the problem?

dengApro
  • 3,848
  • 2
  • 27
  • 41
emil
  • 35
  • 2
  • 15

2 Answers2

0

The error message you're getting:

Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '*** -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (_TtGC26Informacje_o_organizacjach4NodeSS_) for key (head); the class may be defined in source code or a library that is not linked'

suggests that NSKeyedUnarchiver is having difficulty finding the class with the given name. As you can see, the Objective-C name of your class is quite mangled (_TtGC26Informacje_o_organizacjach4NodeSS_), due to it using a Swift-only feature (in this case, generics). The thing is that, AFAIK, this mangling is not stable, and you need a class name in your archive format that you can guarantee won't change in the future. So what I'd do is to provide a stable Objective-C name using the @objc() attribute, to ensure that the class name will stay constant. It doesn't really matter what the name is, so long as it's unique and doesn't change.

@objc(MyApp_Node) public class Node<S>: NSObject, NSCoding {

I can't guarantee that this will fix the problem, but it might, and it's something you need to do regardless if you're going to rely on NSCoding.

EDIT: It turns out that @objc doesn't work on a class with generic parameters. So the answer is simply that NSCoding support and a generic parameter are mutually exclusive. You will need to sacrifice either one or the other (fortunately, Swift 4 has Codable which you could probably use instead of NSCoding).

Charles Srstka
  • 16,665
  • 3
  • 34
  • 60
  • Your answer is mostly correct, but unfortunately, generic subclasses of `NSObject` cannot have a specified `@objc` name: `error: generic subclasses of '@objc' classes cannot have an explicit '@objc' because they are not directly visible from Objective-C` – Itai Ferber Sep 15 '17 at 21:29
  • Ah, you're right. In that case, `NSCoding` and generic parameters are probably simply not compatible with each other. – Charles Srstka Sep 15 '17 at 23:18
0

The class name that you see in the error message, _TtGC26Informacje_o_organizacjach4NodeSS_, is a mangled Swift class name. All generic classes, even ones inheriting from NSObject, get this mangled name at runtime, and this is the name that the Objective-C runtime (and thus NSKeyedArchiver/NSKeyedUnarchiver) see.

The issue at hand is that these class names are dynamically generated when your generic class is instantiated. That means that if you attempt to decode one of your linked list types before ever instantiating an instance of that type, the class will never have been registered with the Objective-C runtime, and you'll get the error message you're seeing, because the class does not exist.

For most types, Charles's answer is correct — adopt an explicit @objc name for the class which overrides the mangling and gives it a stable name. However, generic classes cannot have @objc names assigned to them, since each instantiated type is its own class:

import Foundation

@objc(MyX) // error: generic subclasses of '@objc' classes cannot have an explicit '@objc' because they are not directly visible from Objective-C
class X<T> : NSObject {
    @objc
    func foo() {
        NSLog("%@", self.className)
    }
}

let x = X<Int>()
x.foo()

This means that unless you instantiate your class before attempting to unarchive it, it simply won't be available in the runtime.

Your options are unfortunately somewhat limited:

  1. Somehow instantiate an instance of the class before attempting to unarchive (NOT recommended as the mangled name might change in the future and your existing archives will no longer be compatible)
  2. Use a different type for encoding and decoding (as nathan's comment mentions — why not use an array?)

If appropriate for your use case (i.e. you don’t have backwards-compatibility concerns) and you can work with Swift 4, the new Codable API might also be worth looking into instead of using NSKeyedArchiver.

Itai Ferber
  • 28,308
  • 5
  • 77
  • 83
  • Another solution could be to move to Swift 4 and use `Codable` instead of `NSCoding`. – Charles Srstka Sep 15 '17 at 23:21
  • 1
    Another thing to add is that option 1 is not really a good option, because Swift doesn't make any guarantees that the mangling format will remain stable over successive versions of the language. If the mangling format changes in a future release, all of your existing archives will suddenly become incompatible with your implementation, making this a poor choice. I would conclude that `NSCoding` and generic parameters are, sadly, mutually exclusive. – Charles Srstka Sep 15 '17 at 23:43
  • @CharlesSrstka Correct, and worth mentioning. You can, however, have a non-generic subclass of an instantiated generic class with an `@objc` name, so they’re not 100% mutually exclusive, just mostly. – Itai Ferber Sep 15 '17 at 23:51