2017-09-15 6 views
0

Je crée l'application, où j'ai besoin d'avoir une liste de 10 recherches récentes et de la sauvegarder (pour afficher des informations cohérentes entre les lancements d'applications).NSUserDefaults et ma propre implémentation de liste chaînée

Pour cela, j'ai créé mon implémentation de liste chaînée (classes LinkedList et Node) et une sorte de classe wrapper qui maintient la liste des 10 dernières chaînes. J'ai fait en sorte que toutes ces 3 classes soient conformes au protocole NSCoding et cela fonctionne quand il est temps de l'enregistrer en tant que NSUserDefaults. Malheureusement, lorsque je tente de le charger, l'application se bloque avec l'erreur:

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'

C'est le code de tous les 3 classes:

classe 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") 
} 
} 

classe 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") 
} 
} 

classe 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 
     } 
    } 

Où est le problème?

+1

Pourquoi ne pas simplement utiliser un tableau? – nathan

Répondre

0

Le message d'erreur que vous obtenez:

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'

suggère que NSKeyedUnarchiver est d'avoir du mal à trouver la classe avec le nom donné. Comme vous pouvez le constater, le nom Objective-C de votre classe est assez tronqué (_TtGC26Informacje_o_organizacjach4NodeSS_), grâce à l'utilisation d'une fonctionnalité Swift-only (dans ce cas, les génériques). La chose est que, AFAIK, ce mangling n'est pas stable, et vous avez besoin d'un nom de classe dans votre format d'archive que vous pouvez garantir ne changera pas dans le futur. Donc, ce que je ferais est de fournir un nom Objective-C stable en utilisant l'attribut @objc(), pour s'assurer que le nom de la classe restera constant. Peu importe le nom, tant qu'il est unique et ne change pas.

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

Je ne peux pas garantir que cela va résoudre le problème, mais il pourrait, et il est quelque chose que vous devez faire, peu importe si vous allez compter sur NSCoding.

EDIT: Il s'avère que @objc ne fonctionne pas sur une classe avec des paramètres génériques. Donc, la réponse est simplement que le support NSCoding et un paramètre générique s'excluent mutuellement. Vous aurez besoin de sacrifier l'un ou l'autre (heureusement, Swift 4 a Codable que vous pourriez probablement utiliser au lieu de NSCoding).

+0

Votre réponse est pour la plupart correcte, mais malheureusement, les sous-classes génériques de 'NSObject' ne peuvent pas avoir un nom' @ objc': 'erreur: les sous-classes génériques des classes '@objc' ne peuvent pas avoir un @objc explicite car elles ne sont pas directement visible depuis Objective-C' –

+0

Ah, vous avez raison. Dans ce cas, les paramètres "NSCoding" et génériques ne sont probablement pas compatibles entre eux. –

0

Le nom de classe que vous voyez dans le message d'erreur, _TtGC26Informacje_o_organizacjach4NodeSS_, est un nom de classe Swift tronqué. Toutes les classes génériques, même celles héritant de NSObject, obtiennent ce nom tronqué lors de l'exécution, et c'est le nom que l'environnement d'exécution Objective-C (et donc NSKeyedArchiver/NSKeyedUnarchiver) voit.

Le problème est que ces noms de classe sont générés dynamiquement lorsque votre classe générique est instanciée. Cela signifie que si vous essayez de décoder l'un de vos types de liste liés avant d'instancier une instance de ce type, la classe n'aura jamais été enregistrée avec le runtime Objective-C, et vous obtiendrez le message d'erreur que vous voyez, parce que la classe n'existe pas.

Pour la plupart des types, la réponse de Charles est correcte - adopter un nom explicite @objc pour la classe qui remplace le mangling et lui donne un nom stable. Cependant, les classes génériques ne peuvent pas avoir @objc noms qui leur sont attribués, puisque chaque type instancié est sa propre classe:

importation Fondation

@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() 

Cela signifie que si vous instancier votre classe avant tenter de décompressez, il ne sera tout simplement pas disponible dans l'exécution.

Vos options sont malheureusement un peu limité:

  1. D'une certaine façon instancier une instance de la classe avant de désarchiver (PAS recommandé que le nom mutilée pourrait changer à l'avenir et vos archives existantes ne seront plus compatible)
  2. utilisez un type différent pour le codage et le décodage (comme commentaire de nathan mentionne - pourquoi ne pas utiliser un tableau)

Si approprié pour votre cas d'utilisation (c.-à-d. vous n'avez pas de problèmes de compatibilité ascendante) et vous pouvez travailler avec Swift 4, la nouvelle API Codable pourrait également être intéressante à regarder au lieu d'utiliser NSKeyedArchiver.

+0

Une autre solution pourrait être de passer à Swift 4 et d'utiliser 'Codable' au lieu de' NSCoding'. –

+1

Une autre chose à ajouter est que l'option 1 n'est pas vraiment une bonne option, car Swift ne donne aucune garantie que le format de mangling restera stable sur les versions successives du langage. Si le format de modification change dans une prochaine version, toutes vos archives existantes deviendront soudainement incompatibles avec votre implémentation, ce qui en fera un mauvais choix. Je conclurais que 'NSCoding' et les paramètres génériques sont, malheureusement, mutuellement exclusifs. –

+0

@CharlesSrstka Correct, et mérite d'être mentionné. Vous pouvez, cependant, avoir une sous-classe non générique d'une classe générique instanciée avec un nom '@ objc', donc ils ne sont pas 100% mutuellement exclusifs, juste la plupart du temps. –